feat(base): cell focus ring, click-to-focus, and gridcell ARIA

This commit is contained in:
Philipinho
2026-06-15 02:31:32 +01:00
parent bb7fddfc54
commit dec87947cf
@@ -1,12 +1,14 @@
import { memo, useCallback } from "react"; import { memo, useCallback, useMemo } from "react";
import { Cell } from "@tanstack/react-table"; import { Cell } from "@tanstack/react-table";
import { Popover, Tooltip } from "@mantine/core"; import { Popover, Tooltip } from "@mantine/core";
import { IconArrowsDiagonal } from "@tabler/icons-react"; import { IconArrowsDiagonal } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom, type PrimitiveAtom } from "jotai";
import { IBaseRow, EditingCell } from "@/ee/base/types/base.types"; import { selectAtom } from "jotai/utils";
import { IBaseRow, EditingCell, FocusedCell } from "@/ee/base/types/base.types";
import { import {
editingCellAtomFamily, editingCellAtomFamily,
focusedCellAtomFamily,
activeFormulaEditorAtomFamily, activeFormulaEditorAtomFamily,
FormulaEditorTarget, FormulaEditorTarget,
} from "@/ee/base/atoms/base-atoms"; } from "@/ee/base/atoms/base-atoms";
@@ -24,6 +26,7 @@ import classes from "@/ee/base/styles/grid.module.css";
type GridCellProps = { type GridCellProps = {
cell: Cell<IBaseRow, unknown>; cell: Cell<IBaseRow, unknown>;
rowIndex: number; rowIndex: number;
colIndex?: number;
onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void; onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void;
pageId: string; pageId: string;
}; };
@@ -31,6 +34,7 @@ type GridCellProps = {
export const GridCell = memo(function GridCell({ export const GridCell = memo(function GridCell({
cell, cell,
rowIndex, rowIndex,
colIndex,
onCellUpdate, onCellUpdate,
pageId, pageId,
}: GridCellProps) { }: GridCellProps) {
@@ -44,6 +48,18 @@ export const GridCell = memo(function GridCell({
activeFormulaEditorAtomFamily(pageId), activeFormulaEditorAtomFamily(pageId),
) as unknown as [FormulaEditorTarget, (val: FormulaEditorTarget) => void]; ) as unknown as [FormulaEditorTarget, (val: FormulaEditorTarget) => void];
const setFocusedCell = useSetAtom(focusedCellAtomFamily(pageId) as PrimitiveAtom<FocusedCell>);
const isFocused = useAtomValue(
useMemo(
() =>
selectAtom(
focusedCellAtomFamily(pageId),
(fc) => fc?.rowId === cell.row.id && fc?.propertyId === property?.id,
),
[pageId, cell.row.id, property?.id],
),
);
const { t } = useTranslation(); const { t } = useTranslation();
const editable = useBaseEditable(); const editable = useBaseEditable();
const readOnly = !editable; const readOnly = !editable;
@@ -74,6 +90,21 @@ export const GridCell = memo(function GridCell({
setEditingCell({ rowId, propertyId: property.id }); setEditingCell({ rowId, propertyId: property.id });
}, [property, isRowNumber, rowId, readOnly, setEditingCell, setActiveFormulaEditor]); }, [property, isRowNumber, rowId, readOnly, setEditingCell, setActiveFormulaEditor]);
const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!property) return;
setFocusedCell({ rowId, propertyId: property.id });
(e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({
preventScroll: true,
});
},
[property, rowId, setFocusedCell],
);
const cellReadOnly = property
? readOnly || isSystemPropertyType(property.type)
: false;
const closeFormulaEditor = useCallback( const closeFormulaEditor = useCallback(
() => setActiveFormulaEditor(null), () => setActiveFormulaEditor(null),
[setActiveFormulaEditor], [setActiveFormulaEditor],
@@ -122,12 +153,17 @@ export const GridCell = memo(function GridCell({
const cellInner = ( const cellInner = (
<div <div
className={`${classes.cell} ${isPinned ? classes.cellPinned : ""} ${isEditing ? classes.cellEditing : ""} ${property.isPrimary ? classes.primaryCell : ""}`} id={`base-cell-${rowId}-${property.id}`}
role="gridcell"
aria-colindex={colIndex != null ? colIndex + 1 : undefined}
aria-readonly={cellReadOnly || undefined}
className={`${classes.cell} ${isPinned ? classes.cellPinned : ""} ${isEditing ? classes.cellEditing : ""} ${isFocused && !isEditing ? classes.cellFocused : ""} ${property.isPrimary ? classes.primaryCell : ""}`}
style={ style={
isPinned isPinned
? ({ "--pin-offset": `${pinOffset}px` } as React.CSSProperties) ? ({ "--pin-offset": `${pinOffset}px` } as React.CSSProperties)
: undefined : undefined
} }
onClick={handleClick}
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}
> >
<CellComponent <CellComponent
@@ -209,6 +245,7 @@ gridCellPropsEqual);
function gridCellPropsEqual(prev: GridCellProps, next: GridCellProps) { function gridCellPropsEqual(prev: GridCellProps, next: GridCellProps) {
if ( if (
prev.rowIndex !== next.rowIndex || prev.rowIndex !== next.rowIndex ||
prev.colIndex !== next.colIndex ||
prev.pageId !== next.pageId || prev.pageId !== next.pageId ||
prev.onCellUpdate !== next.onCellUpdate prev.onCellUpdate !== next.onCellUpdate
) { ) {