From 6cff2cb3660985fd1234dea116aa62552bf786d9 Mon Sep 17 00:00:00 2001 From: Evangeline Ireland Date: Wed, 10 Jul 2024 16:02:06 -0700 Subject: [PATCH] [#185315714] - focus on newly created attribute (#1328) * Goes into input mode on newly added attribute. Moves handling of new attribute to the collection table. * Scrolls table to newly created attribute * Fixes cypress to account for new behavior when adding a new attribute * Fixes blurring when clicking on the attr input component * Fix column header highlight on select or when a new attribute is added. * Highlights content of selected header when Rename is selected. Adds a wait before typing a new name in Cypress tests. * Moves attrIdToEdit to the CollectionTableModel * PR Fixes --- v3/cypress/support/elements/table-tile.ts | 3 +- .../case-table/collection-table-model.ts | 6 ++ .../case-table/collection-table.tsx | 24 ++++++- .../case-table/collection-title.tsx | 26 ++----- .../components/case-table/column-header.tsx | 72 +++++++++++++++++-- v3/src/components/case-table/use-columns.tsx | 2 +- 6 files changed, 106 insertions(+), 27 deletions(-) diff --git a/v3/cypress/support/elements/table-tile.ts b/v3/cypress/support/elements/table-tile.ts index f5bd11356b..934587d649 100644 --- a/v3/cypress/support/elements/table-tile.ts +++ b/v3/cypress/support/elements/table-tile.ts @@ -95,7 +95,7 @@ export const TableTileElements = { this.getAttributeMenuItem(item).click({ force: true }) }, renameColumnName(newName) { - cy.get("[data-testid=column-name-input]").type(newName) + cy.get("[data-testid=column-name-input]").wait(250).type(newName) }, // Edit Attribute Property Dialog enterAttributeName(name) { @@ -330,6 +330,7 @@ export const TableTileElements = { addNewAttribute(collectionIndex = 1) { this.getCollection(collectionIndex).find("[data-testid=collection-add-attribute-icon-button] svg") .click({force:true}) + cy.get("[data-testid=column-name-input]").type("{enter}") }, deleteAttrbute(attributeName, collectionIndex = 1) { this.openAttributeMenu(attributeName, collectionIndex) diff --git a/v3/src/components/case-table/collection-table-model.ts b/v3/src/components/case-table/collection-table-model.ts index c42ad60880..5724d638bb 100644 --- a/v3/src/components/case-table/collection-table-model.ts +++ b/v3/src/components/case-table/collection-table-model.ts @@ -12,6 +12,8 @@ export class CollectionTableModel { collectionId: string // RDG grid element @observable element: HTMLDivElement | null = null + // attribute id of the cell being edited + @observable attrIdToEdit: string | undefined = undefined // tracks current scrollTop value for grid @observable scrollTop = 0 // tracks the last user- or programmatically-set scrollTop value @@ -167,6 +169,10 @@ export class CollectionTableModel { this.element = element } + @action setAttrIdToEdit(attrId: string | undefined) { + this.attrIdToEdit = attrId + } + @action syncScrollTopFromEvent(event: React.UIEvent) { const { scrollTop } = event.currentTarget this.lastScrollStep = this.scrollStep diff --git a/v3/src/components/case-table/collection-table.tsx b/v3/src/components/case-table/collection-table.tsx index 67a222c9ea..998afaaa25 100644 --- a/v3/src/components/case-table/collection-table.tsx +++ b/v3/src/components/case-table/collection-table.tsx @@ -21,6 +21,10 @@ import { IDataSet } from "../../models/data/data-set" import { useCaseTableModel } from "./use-case-table-model" import { useCollectionTableModel } from "./use-collection-table-model" import { mstReaction } from "../../utilities/mst-reaction" +import { IAttribute } from "../../models/data/attribute" +import { uniqueName } from "../../utilities/js-utils" +import { t } from "../../utilities/translation/translate" +import { createAttributesNotification } from "../../models/data/data-set-notifications" import "react-data-grid/lib/styles.css" import styles from "./case-table-shared.scss" @@ -128,6 +132,24 @@ export const CollectionTable = observer(function CollectionTable(props: IProps) } }, [columns, caseTableModel]) + const handleAddNewAttribute = () => { + let attribute: IAttribute | undefined + data?.applyModelChange(() => { + const newAttrName = uniqueName(t("DG.CaseTable.defaultAttrName"), + (aName: string) => !data.attributes.find(attr => aName === attr.name) + ) + attribute = data.addAttribute({ name: newAttrName }, { collection: collectionId }) + if (attribute) { + collectionTableModel?.setAttrIdToEdit(attribute.id) + } + }, { + notifications: () => createAttributesNotification(attribute ? [attribute] : [], data), + undoStringKey: "DG.Undo.caseTable.createAttribute", + redoStringKey: "DG.Redo.caseTable.createAttribute" + }) + gridRef.current?.selectCell({idx: columns.length, rowIdx: -1}) + } + const rows = collectionTableModel?.rows if (!data || !rows || !visibleAttributes.length) return null @@ -135,7 +157,7 @@ export const CollectionTable = observer(function CollectionTable(props: IProps)
- + void +} + +export const CollectionTitle = observer(function CollectionTitle({onAddNewAttribute}: IProps) { const data = useDataSetContext() const collectionId = useCollectionContext() const collection = data?.getCollection(collectionId) @@ -96,20 +98,6 @@ export const CollectionTitle = observer(function CollectionTitle() { setIsEditing(false) } - const handleAddNewAttribute = () => { - let attribute: IAttribute | undefined - data?.applyModelChange(() => { - const newAttrName = uniqueName(t("DG.CaseTable.defaultAttrName"), - (aName: string) => !data.attributes.find(attr => aName === attr.name) - ) - attribute = data.addAttribute({ name: newAttrName }, { collection: collectionId }) - }, { - notifications: () => createAttributesNotification(attribute ? [attribute] : [], data), - undoStringKey: "DG.Undo.caseTable.createAttribute", - redoStringKey: "DG.Redo.caseTable.createAttribute" - }) - } - const casesStr = t(caseCount === 1 ? "DG.DataContext.singleCaseName" : "DG.DataContext.pluralCaseName") const addIconClass = clsx("add-icon", { focused: isTileInFocus }) @@ -125,7 +113,7 @@ export const CollectionTitle = observer(function CollectionTitle() {
) diff --git a/v3/src/components/case-table/column-header.tsx b/v3/src/components/case-table/column-header.tsx index 7fd2b3bce4..d2382b6785 100644 --- a/v3/src/components/case-table/column-header.tsx +++ b/v3/src/components/case-table/column-header.tsx @@ -1,6 +1,6 @@ import { Tooltip, Menu, MenuButton, Input, VisuallyHidden } from "@chakra-ui/react" import { useDndContext } from "@dnd-kit/core" -import React, { useEffect, useRef, useState } from "react" +import React, { useCallback, useEffect, useRef, useState } from "react" import { useDataSetContext } from "../../hooks/use-data-set-context" import { IUseDraggableAttribute, useDraggableAttribute } from "../../hooks/use-drag-drop" import { useInstanceIdContext } from "../../hooks/use-instance-id-context" @@ -11,18 +11,22 @@ import { CaseTablePortal } from "./case-table-portal" import { kIndexColumnKey, TRenderHeaderCellProps } from "./case-table-types" import { ColumnHeaderDivider } from "./column-header-divider" import { useRdgCellFocus } from "./use-rdg-cell-focus" +import { useCollectionTableModel } from "./use-collection-table-model" -export function ColumnHeader({ column }: Pick) { +export function ColumnHeader({ column }: TRenderHeaderCellProps) { const { active } = useDndContext() const data = useDataSetContext() + const collectionTableModel = useCollectionTableModel() const instanceId = useInstanceIdContext() || "table" const menuButtonRef = useRef(null) + const inputRef = useRef(null) const [contentElt, setContentElt] = useState(null) const cellElt: HTMLDivElement | null = contentElt?.closest(".rdg-cell") ?? null const isMenuOpen = useRef(false) const [editingAttrId, setEditingAttrId] = useState("") const [editingAttrName, setEditingAttrName] = useState("") const [modalIsOpen, setModalIsOpen] = useState(false) + const [isFocused, setIsFocused] = useState(false) const onCloseRef = useRef<() => void>() // disable tooltips when there is an active drag in progress const dragging = !!active @@ -38,10 +42,39 @@ export function ColumnHeader({ column }: Pick) setDragNodeRef(elt?.closest(".rdg-cell") || null) } + const updateAriaSelectedAttribute = useCallback((isSelected: "true" | "false") => { + if (cellElt) { + cellElt.setAttribute("aria-selected", isSelected) + } + }, [cellElt]) + + useEffect(() => { + const timer = setTimeout(() => { + if (inputRef.current && editingAttrId === column.key) { + inputRef.current.focus() + inputRef.current.select() + } + }, 100) // delay to ensure the input is rendered + + return () => clearTimeout(timer) + }, [column.key, editingAttrId]) + useEffect(() => { onCloseRef.current?.() }, [dragging]) + useEffect(() => { + if (collectionTableModel?.attrIdToEdit === column.key) { + setEditingAttrId(column.key) + setEditingAttrName(column.name as string) + updateAriaSelectedAttribute("true") + } else { + setEditingAttrId("") + setEditingAttrName("") + updateAriaSelectedAttribute("false") + } + }, [collectionTableModel?.attrIdToEdit, cellElt, column.key, column.name, updateAriaSelectedAttribute]) + // focus our content when the cell is focused useRdgCellFocus(cellElt, menuButtonRef.current) @@ -87,6 +120,7 @@ export function ColumnHeader({ column }: Pick) } setEditingAttrId("") setEditingAttrName("") + collectionTableModel?.setAttrIdToEdit?.(undefined) } const handleRenameAttribute = () => { setEditingAttrId(column.key) @@ -97,6 +131,31 @@ export function ColumnHeader({ column }: Pick) setModalIsOpen(open) } + const handleInputBlur = (e: any) => { + if (isFocused) { + handleClose(true) + } + } + + const handleInputClick = (e: React.MouseEvent) => { + setIsFocused(false) + const input = inputRef.current + if (input) { + const { selectionStart, selectionEnd } = input + if (selectionStart != null && selectionEnd != null) { + // Because the input value is automatically selected when user creates a new attribute, + // we deselect current selection and place cursor at the position of the click + if (selectionStart === selectionEnd) { + input.setSelectionRange(selectionStart, selectionEnd) + } + } + } + } + + const handleFocus = () => { + setIsFocused(true) + } + const units = attribute?.units ? ` (${attribute.units})` : "" const description = attribute?.description ? `: ${attribute.description}` : "" return ( @@ -105,6 +164,8 @@ export function ColumnHeader({ column }: Pick) const disableTooltip = dragging || isOpen || modalIsOpen || editingAttrId === column.key isMenuOpen.current = isOpen onCloseRef.current = onClose + // ensure selected header is styled correctly. + if (isMenuOpen.current) updateAriaSelectedAttribute("true") return ( )
{ editingAttrId - ? setEditingAttrName(event.target.value)} - onKeyDown={handleInputKeyDown} onBlur={()=>handleClose(true)} onFocus={(e) => e.target.select()} + ? setEditingAttrName(event.target.value)} + onKeyDown={handleInputKeyDown} onBlur={handleInputBlur} onFocus={handleFocus} /> : <> { // which leads to undesirable browser behavior. width: kDefaultColumnWidth, resizable: true, - headerCellClass: "codap-column-header", + headerCellClass: `codap-column-header`, renderHeaderCell: ColumnHeader, cellClass: "codap-data-cell", renderCell: RenderCell,