Skip to content

Commit

Permalink
[#185315714] - focus on newly created attribute (#1328)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
eireland authored Jul 10, 2024
1 parent faf295a commit 6cff2cb
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 27 deletions.
3 changes: 2 additions & 1 deletion v3/cypress/support/elements/table-tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions v3/src/components/case-table/collection-table-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -167,6 +169,10 @@ export class CollectionTableModel {
this.element = element
}

@action setAttrIdToEdit(attrId: string | undefined) {
this.attrIdToEdit = attrId
}

@action syncScrollTopFromEvent(event: React.UIEvent<HTMLDivElement, UIEvent>) {
const { scrollTop } = event.currentTarget
this.lastScrollStep = this.scrollStep
Expand Down
24 changes: 23 additions & 1 deletion v3/src/components/case-table/collection-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -128,14 +132,32 @@ 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

return (
<div className={`collection-table collection-${collectionId}`}>
<CollectionTableSpacer onDrop={handleNewCollectionDrop} />
<div className="collection-table-and-title" ref={setNodeRef}>
<CollectionTitle />
<CollectionTitle onAddNewAttribute={handleAddNewAttribute}/>
<DataGrid ref={gridRef} className="rdg-light" data-testid="collection-table-grid" renderers={renderers}
columns={columns} rows={rows} headerRowHeight={+styles.headerRowHeight} rowKeyGetter={rowKey}
rowHeight={+styles.bodyRowHeight} selectedRows={selectedRows} onSelectedRowsChange={setSelectedRows}
Expand Down
26 changes: 7 additions & 19 deletions v3/src/components/case-table/collection-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import AddIcon from "../../assets/icons/icon-add-circle.svg"
import { useCollectionContext } from "../../hooks/use-collection-context"
import { useDataSetContext } from "../../hooks/use-data-set-context"
import { useTileModelContext } from "../../hooks/use-tile-model-context"
import { IAttribute } from "../../models/data/attribute"
import { createAttributesNotification, updateCollectionNotification } from "../../models/data/data-set-notifications"
import { uniqueName } from "../../utilities/js-utils"
import { updateCollectionNotification } from "../../models/data/data-set-notifications"
import { t } from "../../utilities/translation/translate"

export const CollectionTitle = observer(function CollectionTitle() {
interface IProps {
onAddNewAttribute: () => void
}

export const CollectionTitle = observer(function CollectionTitle({onAddNewAttribute}: IProps) {
const data = useDataSetContext()
const collectionId = useCollectionContext()
const collection = data?.getCollection(collectionId)
Expand Down Expand Up @@ -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 })

Expand All @@ -125,7 +113,7 @@ export const CollectionTitle = observer(function CollectionTitle() {
</div>
<Button className="add-attribute-icon-button" title={t("DG.TableController.newAttributeTooltip")}
data-testid={"collection-add-attribute-icon-button"} style={addIconStyle} >
<AddIcon className={addIconClass} onClick={handleAddNewAttribute} />
<AddIcon className={addIconClass} onClick={onAddNewAttribute} />
</Button>
</div>
)
Expand Down
72 changes: 67 additions & 5 deletions v3/src/components/case-table/column-header.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<TRenderHeaderCellProps, "column">) {
export function ColumnHeader({ column }: TRenderHeaderCellProps) {
const { active } = useDndContext()
const data = useDataSetContext()
const collectionTableModel = useCollectionTableModel()
const instanceId = useInstanceIdContext() || "table"
const menuButtonRef = useRef<HTMLButtonElement | null>(null)
const inputRef = useRef<HTMLInputElement | null>(null)
const [contentElt, setContentElt] = useState<HTMLDivElement | null>(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
Expand All @@ -38,10 +42,39 @@ export function ColumnHeader({ column }: Pick<TRenderHeaderCellProps, "column">)
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)

Expand Down Expand Up @@ -87,6 +120,7 @@ export function ColumnHeader({ column }: Pick<TRenderHeaderCellProps, "column">)
}
setEditingAttrId("")
setEditingAttrName("")
collectionTableModel?.setAttrIdToEdit?.(undefined)
}
const handleRenameAttribute = () => {
setEditingAttrId(column.key)
Expand All @@ -97,6 +131,31 @@ export function ColumnHeader({ column }: Pick<TRenderHeaderCellProps, "column">)
setModalIsOpen(open)
}

const handleInputBlur = (e: any) => {
if (isFocused) {
handleClose(true)
}
}

const handleInputClick = (e: React.MouseEvent<HTMLInputElement>) => {
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 (
Expand All @@ -105,6 +164,8 @@ export function ColumnHeader({ column }: Pick<TRenderHeaderCellProps, "column">)
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 (
<Tooltip label={`${column.name ?? ""} ${description}`} h="20px" fontSize="12px"
color="white" openDelay={1000} placement="bottom" bottom="15px" left="15px"
Expand All @@ -113,9 +174,10 @@ export function ColumnHeader({ column }: Pick<TRenderHeaderCellProps, "column">)
<div className="codap-column-header-content" ref={setCellRef} {...attributes} {...listeners}
data-testid="codap-column-header-content">
{ editingAttrId
? <Input value={editingAttrName} data-testid="column-name-input" size="xs" autoFocus={true}
variant="unstyled" onChange={event => setEditingAttrName(event.target.value)}
onKeyDown={handleInputKeyDown} onBlur={()=>handleClose(true)} onFocus={(e) => e.target.select()}
? <Input ref={inputRef} value={editingAttrName} data-testid="column-name-input" size="xs"
autoFocus={true} variant="unstyled" onClick={handleInputClick}
onChange={event => setEditingAttrName(event.target.value)}
onKeyDown={handleInputKeyDown} onBlur={handleInputBlur} onFocus={handleFocus}
/>
: <>
<MenuButton className="codap-attribute-button" ref={menuButtonRef}
Expand Down
2 changes: 1 addition & 1 deletion v3/src/components/case-table/use-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const useColumns = ({ data, indexColumn }: IUseColumnsProps) => {
// 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,
Expand Down

0 comments on commit 6cff2cb

Please sign in to comment.