diff --git a/apps/client/src/ee/base/components/grid/grid-container.tsx b/apps/client/src/ee/base/components/grid/grid-container.tsx index 336d0e413..e12030c04 100644 --- a/apps/client/src/ee/base/components/grid/grid-container.tsx +++ b/apps/client/src/ee/base/components/grid/grid-container.tsx @@ -125,7 +125,7 @@ export function GridContainer({ const editingCellRef = useRef(editingCell); editingCellRef.current = editingCell; - const { selectionCount, clear: clearSelection } = useRowSelection(pageId); + const { selectionCount, clear: clearSelection, toggle: toggleRow } = useRowSelection(pageId); const { deleteSelected } = useDeleteSelectedRows(pageId); const { t } = useTranslation(); @@ -323,6 +323,17 @@ export function GridContainer({ [editable, properties, setPendingTypeInsert, setEditingCell, openEditor], ); + const toggleRowSelection = useCallback( + (rowId: string) => { + toggleRow(rowId, { + shiftKey: false, + rowIndex: rowIdsRef.current.indexOf(rowId), + orderedRowIds: rowIdsRef.current, + }); + }, + [toggleRow], + ); + const prevEditingRef = useRef(editingCell); useEffect(() => { const prev = prevEditingRef.current; @@ -373,6 +384,7 @@ export function GridContainer({ selectionCount, clearSelection, deleteSelected, + toggleRowSelection, }); const activeCell = editingCell ?? focusedCell; diff --git a/apps/client/src/ee/base/components/grid/grid-header-cell.tsx b/apps/client/src/ee/base/components/grid/grid-header-cell.tsx index 77773e97c..2bd5b84c4 100644 --- a/apps/client/src/ee/base/components/grid/grid-header-cell.tsx +++ b/apps/client/src/ee/base/components/grid/grid-header-cell.tsx @@ -57,10 +57,12 @@ export const GridHeaderCell = memo(function GridHeaderCell({ const isRowNumber = header.column.id === "__row_number"; const isPinned = header.column.getIsPinned(); const pinOffset = isPinned ? header.column.getStart("left") : undefined; - const { selectionCount } = useRowSelection(pageId); + const { selectionCount, toggleAll } = useRowSelection(pageId); const hasSelection = selectionCount > 0; const editable = useBaseEditable(); const isHeaderInteractive = editable && !!property && !isRowNumber; + const isRowNumberHeaderInteractive = + isRowNumber && editable && loadedRowIds.length > 0; const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void]; const menuOpened = activePropertyMenu === header.column.id; @@ -209,9 +211,10 @@ export const GridHeaderCell = memo(function GridHeaderCell({ return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); - handleHeaderClick(); + if (isRowNumber) { + if (isRowNumberHeaderInteractive) toggleAll(loadedRowIds); + } else { + handleHeaderClick(); + } } }} data-dragging={isDragging || undefined} diff --git a/apps/client/src/ee/base/components/grid/row-number-cell.tsx b/apps/client/src/ee/base/components/grid/row-number-cell.tsx index 56a8b21da..1b9c7bf87 100644 --- a/apps/client/src/ee/base/components/grid/row-number-cell.tsx +++ b/apps/client/src/ee/base/components/grid/row-number-cell.tsx @@ -1,7 +1,11 @@ -import { memo, useCallback } from "react"; +import { memo, useCallback, useMemo } from "react"; import { Checkbox } from "@mantine/core"; import { IconGripVertical } from "@tabler/icons-react"; +import { useAtomValue, useSetAtom, type PrimitiveAtom } from "jotai"; +import { selectAtom } from "jotai/utils"; import { useRowSelection } from "@/ee/base/hooks/use-row-selection"; +import { focusedCellAtomFamily } from "@/ee/base/atoms/base-atoms"; +import { FocusedCell } from "@/ee/base/types/base.types"; import { useBaseEditable } from "@/ee/base/context/base-editable"; import { useGridRowOrder } from "@/ee/base/context/grid-row-order"; import classes from "@/ee/base/styles/grid.module.css"; @@ -26,6 +30,30 @@ export const RowNumberCell = memo(function RowNumberCell({ const editable = useBaseEditable(); const getOrderedRowIds = useGridRowOrder(); + const setFocusedCell = useSetAtom( + focusedCellAtomFamily(pageId) as PrimitiveAtom, + ); + const isFocused = useAtomValue( + useMemo( + () => + selectAtom( + focusedCellAtomFamily(pageId), + (fc) => fc?.rowId === rowId && fc?.propertyId === "__row_number", + ), + [pageId, rowId], + ), + ); + + const handleCellClick = useCallback( + (e: React.MouseEvent) => { + setFocusedCell({ rowId, propertyId: "__row_number" }); + (e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({ + preventScroll: true, + }); + }, + [rowId, setFocusedCell], + ); + const handleCheckboxChange = useCallback( (e: React.ChangeEvent) => { const nativeEvent = e.nativeEvent as MouseEvent; @@ -40,12 +68,15 @@ export const RowNumberCell = memo(function RowNumberCell({ return (
{editable && ( @@ -60,6 +91,7 @@ export const RowNumberCell = memo(function RowNumberCell({ checked={selected} onChange={handleCheckboxChange} aria-label="Select row" + tabIndex={-1} /> )} diff --git a/apps/client/src/ee/base/components/grid/row-number-header-cell.tsx b/apps/client/src/ee/base/components/grid/row-number-header-cell.tsx index 4a2130c2b..d79605650 100644 --- a/apps/client/src/ee/base/components/grid/row-number-header-cell.tsx +++ b/apps/client/src/ee/base/components/grid/row-number-header-cell.tsx @@ -42,6 +42,7 @@ export const RowNumberHeaderCell = memo(function RowNumberHeaderCell({ indeterminate={indeterminate} onChange={() => toggleAll(loadedRowIds)} aria-label="Select all loaded rows" + tabIndex={-1} /> diff --git a/apps/client/src/ee/base/hooks/use-grid-keyboard-nav.ts b/apps/client/src/ee/base/hooks/use-grid-keyboard-nav.ts index eae0adcbb..3d4949fe2 100644 --- a/apps/client/src/ee/base/hooks/use-grid-keyboard-nav.ts +++ b/apps/client/src/ee/base/hooks/use-grid-keyboard-nav.ts @@ -24,6 +24,7 @@ type UseGridKeyboardNavOptions = { selectionCount: number; clearSelection: () => void; deleteSelected: () => void | Promise; + toggleRowSelection: (rowId: string) => void; }; const isPrintableKey = (e: KeyboardEvent) => @@ -50,6 +51,7 @@ export function useGridKeyboardNav({ selectionCount, clearSelection, deleteSelected, + toggleRowSelection, }: UseGridKeyboardNavOptions) { const getColIds = useCallback( () => @@ -60,6 +62,11 @@ export function useGridKeyboardNav({ [table], ); + const getNavColIds = useCallback( + () => table.getVisibleLeafColumns().map((col) => col.id), + [table], + ); + const getRowIds = useCallback( () => table.getRowModel().rows.map((row) => row.id), [table], @@ -187,35 +194,35 @@ export function useGridKeyboardNav({ case "ArrowUp": e.preventDefault(); { - const next = computeNextCell(getRowIds(), getColIds(), focusedCell, -1, 0, false); + const next = computeNextCell(getRowIds(), getNavColIds(), focusedCell, -1, 0, false); if (next) goFocused(next); } break; case "ArrowDown": e.preventDefault(); { - const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 1, 0, false); + const next = computeNextCell(getRowIds(), getNavColIds(), focusedCell, 1, 0, false); if (next) goFocused(next); } break; case "ArrowLeft": e.preventDefault(); { - const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 0, -1, false); + const next = computeNextCell(getRowIds(), getNavColIds(), focusedCell, 0, -1, false); if (next) goFocused(next); } break; case "ArrowRight": e.preventDefault(); { - const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 0, 1, false); + const next = computeNextCell(getRowIds(), getNavColIds(), focusedCell, 0, 1, false); if (next) goFocused(next); } break; case "Tab": { const next = computeNextCell( getRowIds(), - getColIds(), + getNavColIds(), focusedCell, 0, e.shiftKey ? -1 : 1, @@ -230,10 +237,17 @@ export function useGridKeyboardNav({ case "Enter": case "F2": e.preventDefault(); - openEditor(focusedCell); + if (focusedCell.propertyId === "__row_number") { + toggleRowSelection(focusedCell.rowId); + } else { + openEditor(focusedCell); + } break; default: { - if (e.key === " " && propertyType(focusedCell.propertyId) === "checkbox") { + if (e.key === " " && focusedCell.propertyId === "__row_number") { + e.preventDefault(); + toggleRowSelection(focusedCell.rowId); + } else if (e.key === " " && propertyType(focusedCell.propertyId) === "checkbox") { e.preventDefault(); openEditor(focusedCell); } else if (isPrintableKey(e)) { @@ -248,6 +262,7 @@ export function useGridKeyboardNav({ focusedCell, getRowIds, getColIds, + getNavColIds, goEditing, goFocused, setEditingCell, @@ -259,6 +274,7 @@ export function useGridKeyboardNav({ selectionCount, clearSelection, deleteSelected, + toggleRowSelection, ], ); diff --git a/apps/client/src/ee/base/styles/grid.module.css b/apps/client/src/ee/base/styles/grid.module.css index a07d95971..2b744cd7c 100644 --- a/apps/client/src/ee/base/styles/grid.module.css +++ b/apps/client/src/ee/base/styles/grid.module.css @@ -498,6 +498,12 @@ .rowSelected .rowNumberCheckbox { display: inline-flex; } +.bodyGrid:focus-within .cellFocused .rowNumberIndex { + display: none; +} +.bodyGrid:focus-within .cellFocused .rowNumberCheckbox { + display: inline-flex; +} .rowSelected .cell { background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6)); } @@ -534,6 +540,13 @@ display: inline-flex; } +.headerCell:focus-visible .rowNumberHeaderHash { + display: none; +} +.headerCell:focus-visible .rowNumberHeaderCheckbox { + display: inline-flex; +} + .selectionActionBarWrapper { position: fixed; left: 50%;