diff --git a/README.md b/README.md index 2940542996..51738fc4d6 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ An array of rows, the rows data can be of any type. ###### `ref?: Maybe>` -Optional ref for imperative APIs like scrolling/selecting a cell. See [`DataGridHandle`](#datagridhandle). +Optional ref for imperative APIs like scrolling to or focusing a cell. See [`DataGridHandle`](#datagridhandle). ###### `topSummaryRows?: Maybe` @@ -510,11 +510,9 @@ function MyGrid() { } ``` -###### `onFill?: Maybe<(event: FillEvent) => R>` - ###### `onCellMouseDown?: CellMouseEventHandler` -Callback triggered when a pointer becomes active in a cell. The default behavior is to select the cell. Call `preventGridDefault` to prevent the default behavior. +Callback triggered when a pointer becomes active in a cell. The default behavior is to focus the cell. Call `preventGridDefault` to prevent the default behavior. ```tsx function onCellMouseDown(args: CellMouseArgs, event: CellMouseEvent) { @@ -545,7 +543,7 @@ This event can be used to open cell editor on single click ```tsx function onCellClick(args: CellMouseArgs, event: CellMouseEvent) { if (args.column.key === 'id') { - args.selectCell(true); + args.setPosition(true); } } ``` @@ -589,7 +587,7 @@ A function called when keydown event is triggered on a cell. This event can be u ```tsx function onCellKeyDown(args: CellKeyDownArgs, event: CellKeyboardEvent) { - if (args.mode === 'SELECT' && event.key === 'Enter') { + if (args.mode === 'ACTIVE' && event.key === 'Enter') { event.preventGridDefault(); } } @@ -599,7 +597,7 @@ function onCellKeyDown(args: CellKeyDownArgs, event: CellKeyboardEvent) { ```tsx function onCellKeyDown(args: CellKeyDownArgs, event: CellKeyboardEvent) { - if (args.mode === 'SELECT' && event.key === 'Tab') { + if (args.mode === 'ACTIVE' && event.key === 'Tab') { event.preventGridDefault(); } } @@ -617,15 +615,13 @@ Callback triggered when content is pasted into a cell. Return the updated row; the grid will call `onRowsChange` with it. -###### `onSelectedCellChange?: Maybe<(args: CellSelectArgs) => void>` +###### `onActivePositionChange?: Maybe<(args: PositionChangeArgs) => void>` -Triggered when the selected cell is changed. +Triggered when the active position changes. -Arguments: +See the [`PositionChangeArgs`](#positionchangeargstrow-tsummaryrow) type in the Types section below. -- `args.rowIdx`: `number` - row index -- `args.row`: `R | undefined` - row object of the currently selected cell -- `args.column`: `CalculatedColumn` - column object of the currently selected cell +###### `onFill?: Maybe<(event: FillEvent) => R>` ###### `onScroll?: Maybe<(event: React.UIEvent) => void>` @@ -1373,7 +1369,7 @@ Control whether cells can be edited with `renderEditCell`. ##### `colSpan?: Maybe<(args: ColSpanArgs) => Maybe>` -Function to determine how many columns this cell should span. Returns the number of columns to span, or `undefined` for no spanning. See the `ColSpanArgs` type in the Types section below. +Function to determine how many columns this cell should span. Returns the number of columns to span, or `undefined` for no spanning. See the [`ColSpanArgs`](#colspanargstrow-tsummaryrow) type in the Types section below. **Example:** @@ -1586,7 +1582,7 @@ interface RenderEditCellProps { row: TRow; rowIdx: number; onRowChange: (row: TRow, commitChanges?: boolean) => void; - onClose: (commitChanges?: boolean, shouldFocusCell?: boolean) => void; + onClose: (commitChanges?: boolean, shouldFocus?: boolean) => void; } ``` @@ -1642,17 +1638,17 @@ Props passed to custom row renderers. ```tsx interface RenderRowProps { row: TRow; - viewportColumns: readonly CalculatedColumn[]; + iterateOverViewportColumnsForRow: IterateOverViewportColumnsForRow; rowIdx: number; - selectedCellIdx: number | undefined; - isRowSelected: boolean; + activeCellIdx: number | undefined; isRowSelectionDisabled: boolean; + isRowSelected: boolean; gridRowStart: number; - lastFrozenColumnIndex: number; draggedOverCellIdx: number | undefined; - selectedCellEditor: ReactElement> | undefined; + activeCellEditor: ReactElement> | undefined; onRowChange: (column: CalculatedColumn, rowIdx: number, newRow: TRow) => void; rowClass: Maybe<(row: TRow, rowIdx: number) => Maybe>; + isTreeGrid: boolean; // ... and event handlers } ``` @@ -1661,7 +1657,7 @@ interface RenderRowProps { Props passed to the cell renderer when using `renderers.renderCell`. -Shares a base type with row render props (DOM props and cell event handlers) but only includes cell-specific fields like `column`, `row`, `rowIdx`, `colSpan`, and selection state. +Shares a base type with row render props (DOM props and cell event handlers) but only includes cell-specific fields like `column`, `row`, `rowIdx`, `colSpan`, and position state. #### `Renderers` @@ -1683,29 +1679,17 @@ Arguments passed to cell mouse event handlers. ```tsx interface CellMouseArgs { + /** The column object of the cell. */ column: CalculatedColumn; + /** The row object of the cell. */ row: TRow; + /** The row index of the cell. */ rowIdx: number; - selectCell: (enableEditor?: boolean) => void; + /** Function to manually focus the cell. Pass `true` to immediately start editing. */ + setPosition: (enableEditor?: boolean) => void; } ``` -##### `column: CalculatedColumn` - -The column object of the cell. - -##### `row: TRow` - -The row object of the cell. - -##### `rowIdx: number` - -The row index of the cell. - -##### `selectCell: (enableEditor?: boolean) => void` - -Function to manually select the cell. Pass `true` to immediately start editing. - **Example:** ```tsx @@ -1713,7 +1697,7 @@ import type { CellMouseArgs, CellMouseEvent } from 'react-data-grid'; function onCellClick(args: CellMouseArgs, event: CellMouseEvent) { console.log('Clicked cell at row', args.rowIdx, 'column', args.column.key); - args.selectCell(true); // Select and start editing + args.setPosition(true); // Focus and start editing } ``` @@ -1746,7 +1730,7 @@ import type { CellMouseArgs, CellMouseEvent } from 'react-data-grid'; function onCellClick(args: CellMouseArgs, event: CellMouseEvent) { if (args.column.key === 'actions') { - event.preventGridDefault(); // Prevent cell selection + event.preventGridDefault(); // Prevent cell focus } } ``` @@ -1773,30 +1757,30 @@ type CellClipboardEvent = React.ClipboardEvent; #### `CellKeyDownArgs` -Arguments passed to the `onCellKeyDown` handler. The shape differs based on whether the cell is in SELECT or EDIT mode. +Arguments passed to the `onCellKeyDown` handler. The shape differs based on whether the cell is in ACTIVE or EDIT mode. -**SELECT mode:** +**ACTIVE mode:** ```tsx -interface SelectCellKeyDownArgs { - mode: 'SELECT'; - column: CalculatedColumn; - row: TRow; +interface ActiveCellKeyDownArgs { + mode: 'ACTIVE'; + column: CalculatedColumn | undefined; + row: TRow | undefined; rowIdx: number; - selectCell: (position: Position, options?: SelectCellOptions) => void; + setPosition: (position: Position, options?: SetPositionOptions) => void; } ``` **EDIT mode:** ```tsx -interface EditCellKeyDownArgs { +interface EditCellKeyDownArgs { mode: 'EDIT'; column: CalculatedColumn; row: TRow; rowIdx: number; navigate: () => void; - onClose: (commitChanges?: boolean, shouldFocusCell?: boolean) => void; + onClose: (commitChanges?: boolean, shouldFocus?: boolean) => void; } ``` @@ -1813,15 +1797,24 @@ function onCellKeyDown(args: CellKeyDownArgs, event: CellKeyboardEvent) { } ``` -#### `CellSelectArgs` +#### `PositionChangeArgs` -Arguments passed to `onSelectedCellChange`. +Arguments passed to `onActivePositionChange`. ```tsx -interface CellSelectArgs { +interface PositionChangeArgs { + /** row index of the active position */ rowIdx: number; + /** + * row object of the active position, + * undefined if the active position is on a header or summary row + */ row: TRow | undefined; - column: CalculatedColumn; + /** + * column object of the active position, + * undefined if the active position is a row instead of a cell + */ + column: CalculatedColumn | undefined; } ``` @@ -1853,9 +1846,9 @@ Arguments passed to the `colSpan` function. ```tsx type ColSpanArgs = - | { type: 'HEADER' } - | { type: 'ROW'; row: TRow } - | { type: 'SUMMARY'; row: TSummaryRow }; + | { readonly type: 'HEADER' } + | { readonly type: 'ROW'; readonly row: TRow } + | { readonly type: 'SUMMARY'; readonly row: TSummaryRow }; ``` **Example:** @@ -1988,14 +1981,14 @@ interface Position { } ``` -#### `SelectCellOptions` +#### `SetPositionOptions` -Options for programmatically selecting a cell. +Options for programmatically updating the grid's active position. ```tsx -interface SelectCellOptions { +interface SetPositionOptions { enableEditor?: Maybe; - shouldFocusCell?: Maybe; + shouldFocus?: Maybe; } ``` @@ -2053,8 +2046,8 @@ Handle type assigned to a grid's `ref` for programmatic grid control. ```tsx interface DataGridHandle { element: HTMLDivElement | null; - scrollToCell: (position: Partial) => void; - selectCell: (position: Position, options?: SelectCellOptions) => void; + scrollToCell: (position: PartialPosition) => void; + setPosition: (position: Position, options?: SetPositionOptions) => void; } ``` diff --git a/src/Cell.tsx b/src/Cell.tsx index 253d4bbd1a..dada982c10 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -16,7 +16,7 @@ const cellDraggedOverClassname = `rdg-cell-dragged-over ${cellDraggedOver}`; function Cell({ column, colSpan, - isCellSelected, + isCellActive, isDraggedOver, row, rowIdx, @@ -30,11 +30,11 @@ function Cell({ onContextMenu, onCellContextMenu, onRowChange, - selectCell, + setPosition, style, ...props }: CellRendererProps) { - const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellSelected); + const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellActive); const { cellClass } = column; className = getCellClassname( @@ -47,8 +47,8 @@ function Cell({ ); const isEditable = isCellEditableUtil(column, row); - function selectCellWrapper(enableEditor?: boolean) { - selectCell({ rowIdx, idx: column.idx }, { enableEditor }); + function setPositionWrapper(enableEditor?: boolean) { + setPosition({ rowIdx, idx: column.idx }, { enableEditor }); } function handleMouseEvent( @@ -58,7 +58,7 @@ function Cell({ let eventHandled = false; if (eventHandler) { const cellEvent = createCellEvent(event); - eventHandler({ rowIdx, row, column, selectCell: selectCellWrapper }, cellEvent); + eventHandler({ rowIdx, row, column, setPosition: setPositionWrapper }, cellEvent); eventHandled = cellEvent.isGridDefaultPrevented(); } return eventHandled; @@ -68,7 +68,7 @@ function Cell({ onMouseDown?.(event); if (!handleMouseEvent(event, onCellMouseDown)) { // select cell if the event is not prevented - selectCellWrapper(); + setPositionWrapper(); } } @@ -81,7 +81,7 @@ function Cell({ onDoubleClick?.(event); if (!handleMouseEvent(event, onCellDoubleClick)) { // go into edit mode if the event is not prevented - selectCellWrapper(true); + setPositionWrapper(true); } } @@ -91,7 +91,7 @@ function Cell({ } function handleRowChange(newRow: R) { - onRowChange(column, newRow); + onRowChange(column, rowIdx, newRow); } return ( @@ -99,7 +99,7 @@ function Cell({ role="gridcell" aria-colindex={column.idx + 1} // aria-colindex is 1-based aria-colspan={colSpan} - aria-selected={isCellSelected} + aria-selected={isCellActive} aria-readonly={!isEditable || undefined} tabIndex={tabIndex} className={className} diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 71ee3dc72a..ffccd65457 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -23,10 +23,10 @@ import { getCellStyle, getColSpan, getLeftRightKey, - getNextSelectedCellPosition, + getNextActivePosition, + isCellEditableUtil, isCtrlKeyHeldDown, isDefaultCellInput, - isSelectedCellEditable, renderMeasuringCells, scrollIntoView, sign @@ -40,7 +40,7 @@ import type { CellMouseEventHandler, CellNavigationMode, CellPasteArgs, - CellSelectArgs, + PositionChangeArgs, Column, ColumnOrColumnGroup, ColumnWidths, @@ -50,7 +50,7 @@ import type { Position, Renderers, RowsChangeData, - SelectCellOptions, + SetPositionOptions, SelectHeaderRowEvent, SelectRowEvent, SortColumn @@ -72,8 +72,8 @@ import { cellDragHandleClassname, cellDragHandleFrozenClassname } from './style/ import { rootClassname, viewportDraggingClassname } from './style/core'; import SummaryRow from './SummaryRow'; -export interface SelectCellState extends Position { - readonly mode: 'SELECT'; +interface ActiveCellState extends Position { + readonly mode: 'ACTIVE'; } interface EditCellState extends Position { @@ -97,7 +97,7 @@ export type DefaultColumnOptions = Pick< export interface DataGridHandle { element: HTMLDivElement | null; scrollToCell: (position: PartialPosition) => void; - selectCell: (position: Position, options?: SelectCellOptions) => void; + setPosition: (position: Position, options?: SetPositionOptions) => void; } type SharedDivProps = Pick< @@ -168,7 +168,6 @@ export interface DataGridProps extends Sha onSortColumnsChange?: Maybe<(sortColumns: SortColumn[]) => void>; /** Default options applied to all columns */ defaultColumnOptions?: Maybe, NoInfer>>; - onFill?: Maybe<(event: FillEvent>) => NoInfer>; /** * Event props @@ -193,14 +192,15 @@ export interface DataGridProps extends Sha onCellPaste?: Maybe< (args: CellPasteArgs, NoInfer>, event: CellClipboardEvent) => NoInfer >; - /** Function called whenever cell selection is changed */ - onSelectedCellChange?: Maybe<(args: CellSelectArgs, NoInfer>) => void>; + /** Function called whenever the active position is changed */ + onActivePositionChange?: Maybe<(args: PositionChangeArgs, NoInfer>) => void>; /** Callback triggered when the grid is scrolled */ onScroll?: Maybe<(event: React.UIEvent) => void>; /** Callback triggered when column is resized */ onColumnResize?: Maybe<(column: CalculatedColumn, width: number) => void>; /** Callback triggered when columns are reordered */ onColumnsReorder?: Maybe<(sourceColumnKey: string, targetColumnKey: string) => void>; + onFill?: Maybe<(event: FillEvent>) => NoInfer>; /** * Toggles and modes @@ -262,7 +262,7 @@ export function DataGrid(props: DataGridPr onCellDoubleClick, onCellContextMenu, onCellKeyDown, - onSelectedCellChange, + onActivePositionChange, onScroll, onColumnResize, onColumnsReorder, @@ -319,7 +319,7 @@ export function DataGrid(props: DataGridPr const [isDragging, setIsDragging] = useState(false); const [draggedOverRowIdx, setDraggedOverRowIdx] = useState(undefined); const [scrollToPosition, setScrollToPosition] = useState(null); - const [shouldFocusCell, setShouldFocusCell] = useState(false); + const [shouldFocusPosition, setShouldFocusPosition] = useState(false); const [previousRowIdx, setPreviousRowIdx] = useState(-1); const isColumnWidthsControlled = @@ -369,8 +369,8 @@ export function DataGrid(props: DataGridPr const mainHeaderRowIdx = minRowIdx + groupedColumnHeaderRowsCount; const maxRowIdx = rows.length + bottomSummaryRowsCount - 1; - const [selectedPosition, setSelectedPosition] = useState( - (): SelectCellState | EditCellState => ({ idx: -1, rowIdx: minRowIdx - 1, mode: 'SELECT' }) + const [activePosition, setActivePosition] = useState>( + getInitialActivePosition ); /** @@ -432,7 +432,19 @@ export function DataGrid(props: DataGridPr enableVirtualization }); - const viewportColumns = useViewportColumns({ + const maxColIdx = columns.length - 1; + const { + isPositionInActiveBounds: activePositionIsInActiveBounds, + isPositionInViewport: activePositionIsInViewport, + isRowInActiveBounds: activePositionIsRow, + isCellInViewport: activePositionIsCellInViewport + } = validatePosition(activePosition); + + const { + viewportColumns, + iterateOverViewportColumnsForRow, + iterateOverViewportColumnsForRowOutsideOfViewport + } = useViewportColumns({ columns, colSpanColumns, colOverscanStartIdx, @@ -457,11 +469,6 @@ export function DataGrid(props: DataGridPr setIsColumnResizing ); - const minColIdx = isTreeGrid ? -1 : 0; - const maxColIdx = columns.length - 1; - const selectedCellIsWithinSelectionBounds = isCellWithinSelectionBounds(selectedPosition); - const selectedCellIsWithinViewportBounds = isCellWithinViewportBounds(selectedPosition); - /** * The identity of the wrapper function is stable so it won't break memoization */ @@ -476,22 +483,22 @@ export function DataGrid(props: DataGridPr const selectHeaderRowLatest = useLatestFunc(selectHeaderRow); const selectRowLatest = useLatestFunc(selectRow); const handleFormatterRowChangeLatest = useLatestFunc(updateRow); - const selectCellLatest = useLatestFunc(selectCell); + const setPositionLatest = useLatestFunc(setPosition); const selectHeaderCellLatest = useLatestFunc(selectHeaderCell); /** * effects */ useLayoutEffect(() => { - if (shouldFocusCell) { - if (selectedPosition.idx === -1) { + if (shouldFocusPosition) { + if (activePositionIsRow) { focusRow(gridRef.current!); } else { focusCell(gridRef.current!); } - setShouldFocusCell(false); + setShouldFocusPosition(false); } - }, [shouldFocusCell, selectedPosition.idx, gridRef]); + }, [shouldFocusPosition, activePositionIsRow, gridRef]); useImperativeHandle( ref, @@ -499,19 +506,17 @@ export function DataGrid(props: DataGridPr element: gridRef.current, scrollToCell({ idx, rowIdx }) { const scrollToIdx = - idx !== undefined && idx > lastFrozenColumnIndex && idx < columns.length - ? idx - : undefined; + idx != null && idx > lastFrozenColumnIndex && idx < columns.length ? idx : undefined; const scrollToRowIdx = - rowIdx !== undefined && isRowIdxWithinViewportBounds(rowIdx) + rowIdx != null && validatePosition({ idx: 0, rowIdx }).isPositionInViewport ? rowIdx + headerAndTopSummaryRowsCount : undefined; - if (scrollToIdx !== undefined || scrollToRowIdx !== undefined) { + if (scrollToIdx != null || scrollToRowIdx != null) { setScrollToPosition({ idx: scrollToIdx, rowIdx: scrollToRowIdx }); } }, - selectCell + setPosition }) ); @@ -575,19 +580,18 @@ export function DataGrid(props: DataGridPr } function handleKeyDown(event: KeyboardEvent) { - const { idx, rowIdx, mode } = selectedPosition; + const { idx, rowIdx, mode } = activePosition; if (mode === 'EDIT') return; - if (onCellKeyDown && isRowIdxWithinViewportBounds(rowIdx)) { - const row = rows[rowIdx]; + if (onCellKeyDown && activePositionIsInViewport) { const cellEvent = createCellEvent(event); onCellKeyDown( { - mode: 'SELECT', - row, + mode: 'ACTIVE', + row: rows[rowIdx], column: columns[idx], rowIdx, - selectCell + setPosition }, cellEvent ); @@ -642,30 +646,30 @@ export function DataGrid(props: DataGridPr } function commitEditorChanges() { - if (selectedPosition.mode !== 'EDIT') return; - updateRow(columns[selectedPosition.idx], selectedPosition.rowIdx, selectedPosition.row); + if (activePosition.mode !== 'EDIT') return; + updateRow(columns[activePosition.idx], activePosition.rowIdx, activePosition.row); } function handleCellCopy(event: CellClipboardEvent) { - if (!selectedCellIsWithinViewportBounds) return; - const { idx, rowIdx } = selectedPosition; + if (!activePositionIsCellInViewport) return; + const { idx, rowIdx } = activePosition; onCellCopy?.({ row: rows[rowIdx], column: columns[idx] }, event); } function handleCellPaste(event: CellClipboardEvent) { - if (!onCellPaste || !onRowsChange || !isCellEditable(selectedPosition)) { + if (!onCellPaste || !onRowsChange || !isCellEditable(activePosition)) { return; } - const { idx, rowIdx } = selectedPosition; + const { idx, rowIdx } = activePosition; const column = columns[idx]; const updatedRow = onCellPaste({ row: rows[rowIdx], column }, event); updateRow(column, rowIdx, updatedRow); } function handleCellInput(event: KeyboardEvent) { - if (!selectedCellIsWithinViewportBounds) return; - const row = rows[selectedPosition.rowIdx]; + if (!activePositionIsCellInViewport) return; + const row = rows[activePosition.rowIdx]; const { key, shiftKey } = event; // Select the row on Shift + Space @@ -678,8 +682,8 @@ export function DataGrid(props: DataGridPr return; } - if (isCellEditable(selectedPosition) && isDefaultCellInput(event, onCellPaste != null)) { - setSelectedPosition(({ idx, rowIdx }) => ({ + if (isCellEditable(activePosition) && isDefaultCellInput(event, onCellPaste != null)) { + setActivePosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'EDIT', @@ -720,7 +724,7 @@ export function DataGrid(props: DataGridPr setDraggedOverRowIdx(overRowIdx); const ariaRowIndex = headerAndTopSummaryRowsCount + overRowIdx + 1; const el = gridEl.querySelector( - `:scope > [aria-rowindex="${ariaRowIndex}"] > [aria-colindex="${selectedPosition.idx + 1}"]` + `:scope > [aria-rowindex="${ariaRowIndex}"] > [aria-colindex="${activePosition.idx + 1}"]` ); scrollIntoView(el); } @@ -729,7 +733,7 @@ export function DataGrid(props: DataGridPr setIsDragging(false); if (draggedOverRowIdx === undefined) return; - const { rowIdx } = selectedPosition; + const { rowIdx } = activePosition; const [startRowIndex, endRowIndex] = rowIdx < draggedOverRowIdx ? [rowIdx + 1, draggedOverRowIdx + 1] @@ -745,13 +749,13 @@ export function DataGrid(props: DataGridPr function handleDragHandleDoubleClick(event: React.MouseEvent) { event.stopPropagation(); - updateRows(selectedPosition.rowIdx + 1, rows.length); + updateRows(activePosition.rowIdx + 1, rows.length); } function updateRows(startRowIdx: number, endRowIdx: number) { if (onRowsChange == null) return; - const { rowIdx, idx } = selectedPosition; + const { rowIdx, idx } = activePosition; const column = columns[idx]; const sourceRow = rows[rowIdx]; const updatedRows = [...rows]; @@ -774,66 +778,87 @@ export function DataGrid(props: DataGridPr /** * utils */ - function isColIdxWithinSelectionBounds(idx: number) { - return idx >= minColIdx && idx <= maxColIdx; + function getInitialActivePosition(): ActiveCellState { + return { idx: -1, rowIdx: minRowIdx - 1, mode: 'ACTIVE' }; } - function isRowIdxWithinViewportBounds(rowIdx: number) { - return rowIdx >= 0 && rowIdx < rows.length; - } + /** + * Returns whether the given position represents a valid cell or row position in the grid. + * Active bounds: any valid position in the grid + * Viewport: any valid position in the grid outside of header rows and summary rows + * Row selection is only allowed in TreeDataGrid + */ + function validatePosition({ idx, rowIdx }: Position) { + // check column position + const isColumnPositionAllColumns = isTreeGrid && idx === -1; + const isColumnPositionInActiveBounds = idx >= 0 && idx <= maxColIdx; - function isCellWithinSelectionBounds({ idx, rowIdx }: Position): boolean { - return rowIdx >= minRowIdx && rowIdx <= maxRowIdx && isColIdxWithinSelectionBounds(idx); - } + // check row position + const isRowPositionInActiveBounds = rowIdx >= minRowIdx && rowIdx <= maxRowIdx; + const isRowPositionInViewport = rowIdx >= 0 && rowIdx < rows.length; - function isCellWithinEditBounds({ idx, rowIdx }: Position): boolean { - return isRowIdxWithinViewportBounds(rowIdx) && idx >= 0 && idx <= maxColIdx; - } + // row status + const isRowInActiveBounds = isColumnPositionAllColumns && isRowPositionInActiveBounds; + const isRowInViewport = isColumnPositionAllColumns && isRowPositionInViewport; + + // cell status + const isCellInActiveBounds = isColumnPositionInActiveBounds && isRowPositionInActiveBounds; + const isCellInViewport = isColumnPositionInActiveBounds && isRowPositionInViewport; - function isCellWithinViewportBounds({ idx, rowIdx }: Position): boolean { - return isRowIdxWithinViewportBounds(rowIdx) && isColIdxWithinSelectionBounds(idx); + // position status + const isPositionInActiveBounds = isRowInActiveBounds || isCellInActiveBounds; + const isPositionInViewport = isRowInViewport || isCellInViewport; + + return { + isPositionInActiveBounds, + isPositionInViewport, + isRowInActiveBounds, + isRowInViewport, + isCellInActiveBounds, + isCellInViewport + }; } function isCellEditable(position: Position): boolean { return ( - isCellWithinEditBounds(position) && - isSelectedCellEditable({ columns, rows, selectedPosition: position }) + validatePosition(position).isCellInViewport && + isCellEditableUtil(columns[position.idx], rows[position.rowIdx]) ); } - function selectCell(position: Position, options?: SelectCellOptions): void { - if (!isCellWithinSelectionBounds(position)) return; + function setPosition(position: Position, options?: SetPositionOptions): void { + const { isPositionInActiveBounds } = validatePosition(position); + if (!isPositionInActiveBounds) return; commitEditorChanges(); - const samePosition = isSamePosition(selectedPosition, position); + const samePosition = isSamePosition(activePosition, position); if (options?.enableEditor && isCellEditable(position)) { const row = rows[position.rowIdx]; - setSelectedPosition({ ...position, mode: 'EDIT', row, originalRow: row }); + setActivePosition({ ...position, mode: 'EDIT', row, originalRow: row }); } else if (samePosition) { // Avoid re-renders if the selected cell state is the same scrollIntoView(getCellToScroll(gridRef.current!)); } else { - setShouldFocusCell(options?.shouldFocusCell === true); - setSelectedPosition({ ...position, mode: 'SELECT' }); + setShouldFocusPosition(options?.shouldFocus === true); + setActivePosition({ ...position, mode: 'ACTIVE' }); } - if (onSelectedCellChange && !samePosition) { - onSelectedCellChange({ + if (onActivePositionChange && !samePosition) { + onActivePositionChange({ rowIdx: position.rowIdx, - row: isRowIdxWithinViewportBounds(position.rowIdx) ? rows[position.rowIdx] : undefined, + row: rows[position.rowIdx], column: columns[position.idx] }); } } function selectHeaderCell({ idx, rowIdx }: Position): void { - selectCell({ rowIdx: minRowIdx + rowIdx - 1, idx }); + setPosition({ rowIdx: minRowIdx + rowIdx - 1, idx }); } function getNextPosition(key: string, ctrlKey: boolean, shiftKey: boolean): Position { - const { idx, rowIdx } = selectedPosition; - const isRowSelected = selectedCellIsWithinSelectionBounds && idx === -1; + const { idx, rowIdx } = activePosition; switch (key) { case 'ArrowUp': { @@ -860,24 +885,24 @@ export function DataGrid(props: DataGridPr return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; case 'Home': // If row is selected then move focus to the first header row's cell. - if (isRowSelected || ctrlKey) return { idx: 0, rowIdx: minRowIdx }; + if (activePositionIsRow || ctrlKey) return { idx: 0, rowIdx: minRowIdx }; return { idx: 0, rowIdx }; case 'End': // If row is selected then move focus to the last row. - if (isRowSelected) return { idx, rowIdx: maxRowIdx }; + if (activePositionIsRow) return { idx, rowIdx: maxRowIdx }; return { idx: maxColIdx, rowIdx: ctrlKey ? maxRowIdx : rowIdx }; case 'PageUp': { - if (selectedPosition.rowIdx === minRowIdx) return selectedPosition; + if (rowIdx === minRowIdx) return activePosition; const nextRowY = getRowTop(rowIdx) + getRowHeight(rowIdx) - clientHeight; return { idx, rowIdx: nextRowY > 0 ? findRowIdx(nextRowY) : 0 }; } case 'PageDown': { - if (selectedPosition.rowIdx >= rows.length) return selectedPosition; + if (rowIdx >= rows.length) return activePosition; const nextRowY = getRowTop(rowIdx) + clientHeight; return { idx, rowIdx: nextRowY < totalRowHeight ? findRowIdx(nextRowY) : rows.length - 1 }; } default: - return selectedPosition; + return activePosition; } } @@ -891,7 +916,7 @@ export function DataGrid(props: DataGridPr maxColIdx, minRowIdx, maxRowIdx, - selectedPosition + activePosition }) ) { commitEditorChanges(); @@ -907,9 +932,9 @@ export function DataGrid(props: DataGridPr const ctrlKey = isCtrlKeyHeldDown(event); const nextPosition = getNextPosition(key, ctrlKey, shiftKey); - if (isSamePosition(selectedPosition, nextPosition)) return; + if (isSamePosition(activePosition, nextPosition)) return; - const nextSelectedCellPosition = getNextSelectedCellPosition({ + const nextActivePosition = getNextActivePosition({ moveUp: key === 'ArrowUp', moveNext: key === rightKey || (key === 'Tab' && !shiftKey), columns, @@ -922,36 +947,32 @@ export function DataGrid(props: DataGridPr maxRowIdx, lastFrozenColumnIndex, cellNavigationMode, - currentPosition: selectedPosition, + activePosition, nextPosition, - isCellWithinBounds: isCellWithinSelectionBounds + nextPositionIsCellInActiveBounds: validatePosition(nextPosition).isCellInActiveBounds }); - selectCell(nextSelectedCellPosition, { shouldFocusCell: true }); + setPosition(nextActivePosition, { shouldFocus: true }); } function getDraggedOverCellIdx(currentRowIdx: number): number | undefined { if (draggedOverRowIdx === undefined) return; - const { rowIdx } = selectedPosition; + const { rowIdx } = activePosition; const isDraggedOver = rowIdx < draggedOverRowIdx ? rowIdx < currentRowIdx && currentRowIdx <= draggedOverRowIdx : rowIdx > currentRowIdx && currentRowIdx >= draggedOverRowIdx; - return isDraggedOver ? selectedPosition.idx : undefined; + return isDraggedOver ? activePosition.idx : undefined; } function getDragHandle() { - if ( - onFill == null || - selectedPosition.mode === 'EDIT' || - !isCellWithinViewportBounds(selectedPosition) - ) { + if (onFill == null || activePosition.mode === 'EDIT' || !activePositionIsCellInViewport) { return; } - const { idx, rowIdx } = selectedPosition; + const { idx, rowIdx } = activePosition; const column = columns[idx]; if (column.renderEditCell == null || column.editable === false) { return; @@ -991,42 +1012,39 @@ export function DataGrid(props: DataGridPr function getCellEditor(rowIdx: number) { if ( - !isCellWithinViewportBounds(selectedPosition) || - selectedPosition.rowIdx !== rowIdx || - selectedPosition.mode === 'SELECT' + !activePositionIsCellInViewport || + activePosition.rowIdx !== rowIdx || + activePosition.mode === 'ACTIVE' ) { return; } - const { idx, row } = selectedPosition; + const { idx, row } = activePosition; const column = columns[idx]; const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'ROW', row }); const closeOnExternalRowChange = column.editorOptions?.closeOnExternalRowChange ?? true; - const closeEditor = (shouldFocusCell: boolean) => { - setShouldFocusCell(shouldFocusCell); - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })); + const closeEditor = (shouldFocus: boolean) => { + setShouldFocusPosition(shouldFocus); + setActivePosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'ACTIVE' })); }; - const onRowChange = (row: R, commitChanges: boolean, shouldFocusCell: boolean) => { + const onRowChange = (row: R, commitChanges: boolean, shouldFocus: boolean) => { if (commitChanges) { // Prevents two issues when editor is closed by clicking on a different cell // // Otherwise commitEditorChanges may be called before the cell state is changed to // SELECT and this results in onRowChange getting called twice. flushSync(() => { - updateRow(column, selectedPosition.rowIdx, row); - closeEditor(shouldFocusCell); + updateRow(column, activePosition.rowIdx, row); + closeEditor(shouldFocus); }); } else { - setSelectedPosition((position) => ({ ...position, row })); + setActivePosition((position) => ({ ...position, row })); } }; - if ( - closeOnExternalRowChange && - rows[selectedPosition.rowIdx] !== selectedPosition.originalRow - ) { + if (closeOnExternalRowChange && rows[activePosition.rowIdx] !== activePosition.originalRow) { // Discard changes if rows are updated from outside closeEditor(false); } @@ -1046,74 +1064,49 @@ export function DataGrid(props: DataGridPr ); } - function getRowViewportColumns(rowIdx: number) { - // idx can be -1 if grouping is enabled - const selectedColumn = selectedPosition.idx === -1 ? undefined : columns[selectedPosition.idx]; - if ( - selectedColumn !== undefined && - selectedPosition.rowIdx === rowIdx && - !viewportColumns.includes(selectedColumn) - ) { - // Add the selected column to viewport columns if the cell is not within the viewport - return selectedPosition.idx > colOverscanEndIdx - ? [...viewportColumns, selectedColumn] - : [ - ...viewportColumns.slice(0, lastFrozenColumnIndex + 1), - selectedColumn, - ...viewportColumns.slice(lastFrozenColumnIndex + 1) - ]; + function* iterateOverViewportRowIdx() { + const activeRowIdx = activePosition.rowIdx; + + if (activePositionIsInViewport && activeRowIdx < rowOverscanStartIdx) { + yield activeRowIdx; + } + for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { + yield rowIdx; + } + if (activePositionIsInViewport && activeRowIdx > rowOverscanEndIdx) { + yield activeRowIdx; } - return viewportColumns; } function getViewportRows() { - const rowElements: React.ReactNode[] = []; - - const { idx: selectedIdx, rowIdx: selectedRowIdx } = selectedPosition; - - const startRowIdx = - selectedCellIsWithinViewportBounds && selectedRowIdx < rowOverscanStartIdx - ? rowOverscanStartIdx - 1 - : rowOverscanStartIdx; - const endRowIdx = - selectedCellIsWithinViewportBounds && selectedRowIdx > rowOverscanEndIdx - ? rowOverscanEndIdx + 1 - : rowOverscanEndIdx; - - for (let viewportRowIdx = startRowIdx; viewportRowIdx <= endRowIdx; viewportRowIdx++) { - const isRowOutsideViewport = - viewportRowIdx === rowOverscanStartIdx - 1 || viewportRowIdx === rowOverscanEndIdx + 1; - const rowIdx = isRowOutsideViewport ? selectedRowIdx : viewportRowIdx; - - let rowColumns = viewportColumns; - const selectedColumn = selectedIdx === -1 ? undefined : columns[selectedIdx]; - if (selectedColumn !== undefined) { - if (isRowOutsideViewport) { - // if the row is outside the viewport then only render the selected cell - rowColumns = [selectedColumn]; - } else { - // if the row is within the viewport and cell is not, add the selected column to viewport columns - rowColumns = getRowViewportColumns(rowIdx); + const { idx: activeIdx, rowIdx: activeRowIdx } = activePosition; + + return iterateOverViewportRowIdx() + .map((rowIdx) => { + const isActiveRow = rowIdx === activeRowIdx; + + // if the row is outside the viewport then only render its active column, if any + const iterateOverColumns = + isActiveRow && (rowIdx < rowOverscanStartIdx || rowIdx > rowOverscanEndIdx) + ? iterateOverViewportColumnsForRowOutsideOfViewport + : iterateOverViewportColumnsForRow; + + const row = rows[rowIdx]; + const gridRowStart = headerAndTopSummaryRowsCount + rowIdx + 1; + let key: K | number = rowIdx; + let isRowSelected = false; + if (typeof rowKeyGetter === 'function') { + key = rowKeyGetter(row); + isRowSelected = selectedRows?.has(key) ?? false; } - } - const row = rows[rowIdx]; - const gridRowStart = headerAndTopSummaryRowsCount + rowIdx + 1; - let key: K | number = rowIdx; - let isRowSelected = false; - if (typeof rowKeyGetter === 'function') { - key = rowKeyGetter(row); - isRowSelected = selectedRows?.has(key) ?? false; - } - - rowElements.push( - renderRow(key, { + return renderRow(key, { // aria-rowindex is 1 based 'aria-rowindex': headerAndTopSummaryRowsCount + rowIdx + 1, 'aria-selected': isSelectable ? isRowSelected : undefined, rowIdx, row, - viewportColumns: rowColumns, + iterateOverViewportColumnsForRow: iterateOverColumns, isRowSelectionDisabled: isRowSelectionDisabled?.(row) ?? false, isRowSelected, onCellMouseDown: onCellMouseDownLatest, @@ -1122,23 +1115,20 @@ export function DataGrid(props: DataGridPr onCellContextMenu: onCellContextMenuLatest, rowClass, gridRowStart, - selectedCellIdx: selectedRowIdx === rowIdx ? selectedIdx : undefined, + activeCellIdx: isActiveRow ? activeIdx : undefined, draggedOverCellIdx: getDraggedOverCellIdx(rowIdx), - lastFrozenColumnIndex, onRowChange: handleFormatterRowChangeLatest, - selectCell: selectCellLatest, - selectedCellEditor: getCellEditor(rowIdx), + setPosition: setPositionLatest, + activeCellEditor: getCellEditor(rowIdx), isTreeGrid - }) - ); - } - - return rowElements; + }); + }) + .toArray(); } // Reset the positions if the current values are no longer valid. This can happen if a column or row is removed - if (selectedPosition.idx > maxColIdx || selectedPosition.rowIdx > maxRowIdx) { - setSelectedPosition({ idx: -1, rowIdx: minRowIdx - 1, mode: 'SELECT' }); + if (activePosition.idx > maxColIdx || activePosition.rowIdx > maxRowIdx) { + setActivePosition(getInitialActivePosition()); setDraggedOverRowIdx(undefined); } @@ -1206,28 +1196,27 @@ export function DataGrid(props: DataGridPr key={index} rowIdx={index + 1} level={-groupedColumnHeaderRowsCount + index} - columns={getRowViewportColumns(minRowIdx + index)} - selectedCellIdx={ - selectedPosition.rowIdx === minRowIdx + index ? selectedPosition.idx : undefined + iterateOverViewportColumnsForRow={iterateOverViewportColumnsForRow} + activeCellIdx={ + activePosition.rowIdx === minRowIdx + index ? activePosition.idx : undefined } - selectCell={selectHeaderCellLatest} + setPosition={selectHeaderCellLatest} /> ))} @@ -1239,7 +1228,7 @@ export function DataGrid(props: DataGridPr {topSummaryRows?.map((row, rowIdx) => { const gridRowStart = headerRowsCount + 1 + rowIdx; const summaryRowIdx = mainHeaderRowIdx + 1 + rowIdx; - const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx; + const isSummaryRowActive = activePosition.rowIdx === summaryRowIdx; const top = headerRowsHeight + summaryRowHeight * rowIdx; return ( @@ -1251,11 +1240,10 @@ export function DataGrid(props: DataGridPr row={row} top={top} bottom={undefined} - viewportColumns={getRowViewportColumns(summaryRowIdx)} - lastFrozenColumnIndex={lastFrozenColumnIndex} - selectedCellIdx={isSummaryRowSelected ? selectedPosition.idx : undefined} + iterateOverViewportColumnsForRow={iterateOverViewportColumnsForRow} + activeCellIdx={isSummaryRowActive ? activePosition.idx : undefined} isTop - selectCell={selectCellLatest} + setPosition={setPositionLatest} isTreeGrid={isTreeGrid} /> ); @@ -1266,7 +1254,7 @@ export function DataGrid(props: DataGridPr {bottomSummaryRows?.map((row, rowIdx) => { const gridRowStart = headerAndTopSummaryRowsCount + rows.length + rowIdx + 1; const summaryRowIdx = rows.length + rowIdx; - const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx; + const isSummaryRowActive = activePosition.rowIdx === summaryRowIdx; const top = clientHeight > totalRowHeight ? gridHeight - summaryRowHeight * (bottomSummaryRows.length - rowIdx) @@ -1285,11 +1273,10 @@ export function DataGrid(props: DataGridPr row={row} top={top} bottom={bottom} - viewportColumns={getRowViewportColumns(summaryRowIdx)} - lastFrozenColumnIndex={lastFrozenColumnIndex} - selectedCellIdx={isSummaryRowSelected ? selectedPosition.idx : undefined} + iterateOverViewportColumnsForRow={iterateOverViewportColumnsForRow} + activeCellIdx={isSummaryRowActive ? activePosition.idx : undefined} isTop={false} - selectCell={selectCellLatest} + setPosition={setPositionLatest} isTreeGrid={isTreeGrid} /> ); diff --git a/src/EditCell.tsx b/src/EditCell.tsx index 35b5638007..26825b2360 100644 --- a/src/EditCell.tsx +++ b/src/EditCell.tsx @@ -57,8 +57,8 @@ interface EditCellProps Omit, 'onRowChange' | 'onClose'>, SharedCellRendererProps { rowIdx: number; - onRowChange: (row: R, commitChanges: boolean, shouldFocusCell: boolean) => void; - closeEditor: (shouldFocusCell: boolean) => void; + onRowChange: (row: R, commitChanges: boolean, shouldFocus: boolean) => void; + closeEditor: (shouldFocus: boolean) => void; navigate: (event: React.KeyboardEvent) => void; onKeyDown: Maybe<(args: EditCellKeyDownArgs, event: CellKeyboardEvent) => void>; } @@ -166,11 +166,11 @@ export default function EditCell({ } } - function onClose(commitChanges = false, shouldFocusCell = true) { + function onClose(commitChanges = false, shouldFocus = true) { if (commitChanges) { - onRowChange(row, true, shouldFocusCell); + onRowChange(row, true, shouldFocus); } else { - closeEditor(shouldFocusCell); + closeEditor(shouldFocus); } } diff --git a/src/GroupCell.tsx b/src/GroupCell.tsx index 41a2bbf5d3..8354f167c0 100644 --- a/src/GroupCell.tsx +++ b/src/GroupCell.tsx @@ -12,7 +12,7 @@ interface GroupCellProps { isExpanded: boolean; column: CalculatedColumn; row: GroupRow; - isCellSelected: boolean; + isCellActive: boolean; groupColumnIndex: number; isGroupByColumn: boolean; } @@ -22,14 +22,14 @@ function GroupCell({ groupKey, childRows, isExpanded, - isCellSelected, + isCellActive, column, row, groupColumnIndex, isGroupByColumn, toggleGroup: toggleGroupWrapper }: GroupCellProps) { - const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellSelected); + const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellActive); function toggleGroup() { toggleGroupWrapper(id); @@ -43,7 +43,7 @@ function GroupCell({ key={column.key} role="gridcell" aria-colindex={column.idx + 1} - aria-selected={isCellSelected} + aria-selected={isCellActive} // tabIndex={undefined} prevents clicks on the cell // from stealing focus from the row. // onMouseDown={preventDefault} would break mousewheel clicks diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 7c2c453c6b..88aeb47203 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -3,11 +3,11 @@ import { css } from 'ecij'; import { RowSelectionContext, type RowSelectionContextValue } from './hooks'; import { classnames } from './utils'; -import type { BaseRenderRowProps, GroupRow } from './types'; +import type { BaseRenderRowProps, GroupRow, Omit } from './types'; import { SELECT_COLUMN_KEY } from './Columns'; import GroupCell from './GroupCell'; import { cell, cellFrozen } from './style/cell'; -import { rowClassname, rowSelectedClassname } from './style/row'; +import { rowClassname, rowActiveClassname } from './style/row'; const groupRow = css` @layer rdg.GroupedRow { @@ -24,7 +24,10 @@ const groupRow = css` const groupRowClassname = `rdg-group-row ${groupRow}`; -interface GroupRowRendererProps extends BaseRenderRowProps { +interface GroupRowRendererProps extends Omit< + BaseRenderRowProps, + 'isRowSelectionDisabled' +> { row: GroupRow; groupBy: readonly string[]; toggleGroup: (expandedGroupId: unknown) => void; @@ -34,22 +37,21 @@ function GroupedRow({ className, row, rowIdx, - viewportColumns, - selectedCellIdx, + iterateOverViewportColumnsForRow, + activeCellIdx, isRowSelected, - selectCell, + setPosition, gridRowStart, groupBy, toggleGroup, - isRowSelectionDisabled: _isRowSelectionDisabled, ...props }: GroupRowRendererProps) { - const isPositionOnRow = selectedCellIdx === -1; - // Select is always the first column - const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? row.level + 1 : row.level; + const isPositionOnRow = activeCellIdx === -1; + + let idx = row.level; function handleSelectGroup() { - selectCell({ rowIdx, idx: -1 }, { shouldFocusCell: true }); + setPosition({ rowIdx, idx: -1 }, { shouldFocus: true }); } const selectionValue = useMemo( @@ -70,28 +72,37 @@ function GroupedRow({ rowClassname, groupRowClassname, `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, - isPositionOnRow && rowSelectedClassname, + isPositionOnRow && rowActiveClassname, className )} onMouseDown={handleSelectGroup} style={{ gridRowStart }} {...props} > - {viewportColumns.map((column) => ( - - ))} + {iterateOverViewportColumnsForRow(activeCellIdx) + .map(([column], index) => { + // Select is always the first column + if (index === 0 && column.key === SELECT_COLUMN_KEY) { + idx += 1; + } + + return ( + + ); + }) + .toArray()} ); diff --git a/src/GroupedColumnHeaderCell.tsx b/src/GroupedColumnHeaderCell.tsx index 7f3b4182d5..0cb549ff5f 100644 --- a/src/GroupedColumnHeaderCell.tsx +++ b/src/GroupedColumnHeaderCell.tsx @@ -6,27 +6,27 @@ import { cellClassname } from './style/cell'; type SharedGroupedColumnHeaderRowProps = Pick< GroupedColumnHeaderRowProps, - 'rowIdx' | 'selectCell' + 'rowIdx' | 'setPosition' >; interface GroupedColumnHeaderCellProps extends SharedGroupedColumnHeaderRowProps { column: CalculatedColumnParent; - isCellSelected: boolean; + isCellActive: boolean; } export default function GroupedColumnHeaderCell({ column, rowIdx, - isCellSelected, - selectCell + isCellActive, + setPosition }: GroupedColumnHeaderCellProps) { - const { tabIndex, onFocus } = useRovingTabIndex(isCellSelected); + const { tabIndex, onFocus } = useRovingTabIndex(isCellActive); const { colSpan } = column; const rowSpan = getHeaderCellRowSpan(column, rowIdx); const index = column.idx + 1; function onMouseDown() { - selectCell({ idx: column.idx, rowIdx }); + setPosition({ idx: column.idx, rowIdx }); } return ( @@ -35,7 +35,7 @@ export default function GroupedColumnHeaderCell({ aria-colindex={index} aria-colspan={colSpan} aria-rowspan={rowSpan} - aria-selected={isCellSelected} + aria-selected={isCellActive} tabIndex={tabIndex} className={classnames(cellClassname, column.headerCellClass)} style={{ diff --git a/src/GroupedColumnHeaderRow.tsx b/src/GroupedColumnHeaderRow.tsx index 358338d9c5..0f0d56472a 100644 --- a/src/GroupedColumnHeaderRow.tsx +++ b/src/GroupedColumnHeaderRow.tsx @@ -1,28 +1,28 @@ import { memo } from 'react'; -import type { CalculatedColumn, CalculatedColumnParent, Position } from './types'; +import type { CalculatedColumnParent, IterateOverViewportColumnsForRow, Position } from './types'; import GroupedColumnHeaderCell from './GroupedColumnHeaderCell'; import { headerRowClassname } from './HeaderRow'; export interface GroupedColumnHeaderRowProps { rowIdx: number; level: number; - columns: readonly CalculatedColumn[]; - selectCell: (position: Position) => void; - selectedCellIdx: number | undefined; + iterateOverViewportColumnsForRow: IterateOverViewportColumnsForRow; + activeCellIdx: number | undefined; + setPosition: (position: Position) => void; } function GroupedColumnHeaderRow({ rowIdx, level, - columns, - selectedCellIdx, - selectCell + iterateOverViewportColumnsForRow, + activeCellIdx, + setPosition }: GroupedColumnHeaderRowProps) { const cells = []; const renderedParents = new Set>(); - for (const column of columns) { + for (const [column] of iterateOverViewportColumnsForRow(activeCellIdx)) { if (column.parent === undefined) continue; let { parent } = column; @@ -40,8 +40,8 @@ function GroupedColumnHeaderRow({ key={idx} column={parent} rowIdx={rowIdx} - isCellSelected={selectedCellIdx === idx} - selectCell={selectCell} + isCellActive={activeCellIdx === idx} + setPosition={setPosition} /> ); } diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index 40333f76ac..457a139410 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -68,7 +68,7 @@ type SharedHeaderRowProps = Pick< HeaderRowProps, | 'sortColumns' | 'onSortColumnsChange' - | 'selectCell' + | 'setPosition' | 'onColumnResize' | 'onColumnResizeEnd' | 'shouldFocusGrid' @@ -80,7 +80,7 @@ export interface HeaderCellProps extends SharedHeaderRowProps { column: CalculatedColumn; colSpan: number | undefined; rowIdx: number; - isCellSelected: boolean; + isCellActive: boolean; draggedColumnKey: string | undefined; setDraggedColumnKey: (draggedColumnKey: string | undefined) => void; } @@ -89,13 +89,13 @@ export default function HeaderCell({ column, colSpan, rowIdx, - isCellSelected, + isCellActive, onColumnResize, onColumnResizeEnd, onColumnsReorder, sortColumns, onSortColumnsChange, - selectCell, + setPosition, shouldFocusGrid, direction, draggedColumnKey, @@ -105,8 +105,8 @@ export default function HeaderCell({ const dragImageRef = useRef(null); const isDragging = draggedColumnKey === column.key; const rowSpan = getHeaderCellRowSpan(column, rowIdx); - // set the tabIndex to 0 when there is no selected cell so grid can receive focus - const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(shouldFocusGrid || isCellSelected); + // set the tabIndex to 0 when there is no active cell so grid can receive focus + const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(shouldFocusGrid || isCellActive); const sortIndex = sortColumns?.findIndex((sort) => sort.columnKey === column.key); const sortColumn = sortIndex !== undefined && sortIndex > -1 ? sortColumns![sortIndex] : undefined; @@ -164,13 +164,13 @@ export default function HeaderCell({ function handleFocus(event: React.FocusEvent) { onFocus?.(event); if (shouldFocusGrid) { - // Select the first header cell if there is no selected cell - selectCell({ idx: 0, rowIdx }); + // Select the first header cell if there is no active cell + setPosition({ idx: 0, rowIdx }); } } function onMouseDown() { - selectCell({ idx: column.idx, rowIdx }); + setPosition({ idx: column.idx, rowIdx }); } function onClick(event: React.MouseEvent) { @@ -288,7 +288,7 @@ export default function HeaderCell({ aria-colindex={column.idx + 1} aria-colspan={colSpan} aria-rowspan={rowSpan} - aria-selected={isCellSelected} + aria-selected={isCellActive} aria-sort={ariaSort} tabIndex={tabIndex} className={className} diff --git a/src/HeaderRow.tsx b/src/HeaderRow.tsx index 3c9fe6217a..eca67abd47 100644 --- a/src/HeaderRow.tsx +++ b/src/HeaderRow.tsx @@ -1,12 +1,19 @@ import { memo, useState } from 'react'; import { css } from 'ecij'; -import { classnames, getColSpan } from './utils'; -import type { CalculatedColumn, Direction, Maybe, Position, ResizedWidth } from './types'; +import { classnames } from './utils'; +import type { + CalculatedColumn, + Direction, + IterateOverViewportColumnsForRow, + Maybe, + Position, + ResizedWidth +} from './types'; import type { DataGridProps } from './DataGrid'; import HeaderCell from './HeaderCell'; import { cell, cellFrozen } from './style/cell'; -import { rowSelectedClassname } from './style/row'; +import { rowActiveClassname } from './style/row'; type SharedDataGridProps = Pick< DataGridProps, @@ -15,12 +22,11 @@ type SharedDataGridProps = Pick< export interface HeaderRowProps extends SharedDataGridProps { rowIdx: number; - columns: readonly CalculatedColumn[]; + iterateOverViewportColumnsForRow: IterateOverViewportColumnsForRow; onColumnResize: (column: CalculatedColumn, width: ResizedWidth) => void; onColumnResizeEnd: () => void; - selectCell: (position: Position) => void; - lastFrozenColumnIndex: number; - selectedCellIdx: number | undefined; + activeCellIdx: number | undefined; + setPosition: (position: Position) => void; shouldFocusGrid: boolean; direction: Direction; headerRowClass: Maybe; @@ -49,49 +55,41 @@ export const headerRowClassname = `rdg-header-row ${headerRow}`; function HeaderRow({ headerRowClass, rowIdx, - columns, + iterateOverViewportColumnsForRow, onColumnResize, onColumnResizeEnd, onColumnsReorder, sortColumns, onSortColumnsChange, - lastFrozenColumnIndex, - selectedCellIdx, - selectCell, + activeCellIdx, + setPosition, shouldFocusGrid, direction }: HeaderRowProps) { const [draggedColumnKey, setDraggedColumnKey] = useState(); - const isPositionOnRow = selectedCellIdx === -1; + const isPositionOnRow = activeCellIdx === -1; - const cells = []; - for (let index = 0; index < columns.length; index++) { - const column = columns[index]; - const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'HEADER' }); - if (colSpan !== undefined) { - index += colSpan - 1; - } - - cells.push( + const cells = iterateOverViewportColumnsForRow(activeCellIdx, { type: 'HEADER' }) + .map(([column, colSpan], index) => ( key={column.key} column={column} colSpan={colSpan} rowIdx={rowIdx} - isCellSelected={selectedCellIdx === column.idx} + isCellActive={activeCellIdx === column.idx} onColumnResize={onColumnResize} onColumnResizeEnd={onColumnResizeEnd} onColumnsReorder={onColumnsReorder} onSortColumnsChange={onSortColumnsChange} sortColumns={sortColumns} - selectCell={selectCell} + setPosition={setPosition} shouldFocusGrid={shouldFocusGrid && index === 0} direction={direction} draggedColumnKey={draggedColumnKey} setDraggedColumnKey={setDraggedColumnKey} /> - ); - } + )) + .toArray(); return (
({ aria-rowindex={rowIdx} // aria-rowindex is 1 based className={classnames( headerRowClassname, - isPositionOnRow && rowSelectedClassname, + isPositionOnRow && rowActiveClassname, headerRowClass )} > diff --git a/src/Row.tsx b/src/Row.tsx index d38ea2bc5d..d1c2f93d15 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -1,23 +1,22 @@ import { memo, useMemo } from 'react'; -import { RowSelectionContext, useLatestFunc, type RowSelectionContextValue } from './hooks'; -import { classnames, getColSpan } from './utils'; -import type { CalculatedColumn, RenderRowProps } from './types'; +import { RowSelectionContext, type RowSelectionContextValue } from './hooks'; +import { classnames } from './utils'; +import type { RenderRowProps } from './types'; import { useDefaultRenderers } from './DataGridDefaultRenderersContext'; -import { rowClassname, rowSelectedClassname } from './style/row'; +import { rowClassname, rowActiveClassname } from './style/row'; function Row({ className, rowIdx, gridRowStart, - selectedCellIdx, + activeCellIdx, isRowSelectionDisabled, isRowSelected, draggedOverCellIdx, - lastFrozenColumnIndex, row, - viewportColumns, - selectedCellEditor, + iterateOverViewportColumnsForRow, + activeCellEditor, isTreeGrid, onCellMouseDown, onCellClick, @@ -25,59 +24,47 @@ function Row({ onCellContextMenu, rowClass, onRowChange, - selectCell, + setPosition, style, ...props }: RenderRowProps) { const renderCell = useDefaultRenderers()!.renderCell!; - const handleRowChange = useLatestFunc((column: CalculatedColumn, newRow: R) => { - onRowChange(column, rowIdx, newRow); - }); - - const isPositionOnRow = selectedCellIdx === -1; + const isPositionOnRow = activeCellIdx === -1; className = classnames( rowClassname, `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, - isPositionOnRow && rowSelectedClassname, + isPositionOnRow && rowActiveClassname, rowClass?.(row, rowIdx), className ); - const cells = []; - - for (let index = 0; index < viewportColumns.length; index++) { - const column = viewportColumns[index]; - const { idx } = column; - const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'ROW', row }); - if (colSpan !== undefined) { - index += colSpan - 1; - } + const cells = iterateOverViewportColumnsForRow(activeCellIdx, { type: 'ROW', row }) + .map(([column, colSpan]) => { + const { idx } = column; + const isCellActive = activeCellIdx === idx; - const isCellSelected = selectedCellIdx === idx; + if (isCellActive && activeCellEditor) { + return activeCellEditor; + } - if (isCellSelected && selectedCellEditor) { - cells.push(selectedCellEditor); - } else { - cells.push( - renderCell(column.key, { - column, - colSpan, - row, - rowIdx, - isDraggedOver: draggedOverCellIdx === idx, - isCellSelected, - onCellMouseDown, - onCellClick, - onCellDoubleClick, - onCellContextMenu, - onRowChange: handleRowChange, - selectCell - }) - ); - } - } + return renderCell(column.key, { + column, + colSpan, + row, + rowIdx, + isDraggedOver: draggedOverCellIdx === idx, + isCellActive, + onCellMouseDown, + onCellClick, + onCellDoubleClick, + onCellContextMenu, + onRowChange, + setPosition + }); + }) + .toArray(); const selectionValue = useMemo( (): RowSelectionContextValue => ({ isRowSelected, isRowSelectionDisabled }), diff --git a/src/ScrollToCell.tsx b/src/ScrollToCell.tsx index 8b3ebda3c4..6f6601a750 100644 --- a/src/ScrollToCell.tsx +++ b/src/ScrollToCell.tsx @@ -1,10 +1,11 @@ import { useLayoutEffect, useRef } from 'react'; import { scrollIntoView } from './utils'; +import type { Maybe } from './types'; export interface PartialPosition { - readonly idx?: number | undefined; - readonly rowIdx?: number | undefined; + readonly idx?: Maybe; + readonly rowIdx?: Maybe; } export default function ScrollToCell({ @@ -34,8 +35,8 @@ export default function ScrollToCell({
); diff --git a/src/SummaryCell.tsx b/src/SummaryCell.tsx index d44738ed03..8e86942de5 100644 --- a/src/SummaryCell.tsx +++ b/src/SummaryCell.tsx @@ -6,7 +6,7 @@ import type { CellRendererProps } from './types'; type SharedCellRendererProps = Pick< CellRendererProps, - 'rowIdx' | 'column' | 'colSpan' | 'isCellSelected' | 'selectCell' + 'rowIdx' | 'column' | 'colSpan' | 'isCellActive' | 'setPosition' >; interface SummaryCellProps extends SharedCellRendererProps { @@ -18,10 +18,10 @@ function SummaryCell({ colSpan, row, rowIdx, - isCellSelected, - selectCell + isCellActive, + setPosition }: SummaryCellProps) { - const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellSelected); + const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellActive); const { summaryCellClass } = column; const className = getCellClassname( column, @@ -29,7 +29,7 @@ function SummaryCell({ ); function onMouseDown() { - selectCell({ rowIdx, idx: column.idx }); + setPosition({ rowIdx, idx: column.idx }); } return ( @@ -37,7 +37,7 @@ function SummaryCell({ role="gridcell" aria-colindex={column.idx + 1} aria-colspan={colSpan} - aria-selected={isCellSelected} + aria-selected={isCellActive} tabIndex={tabIndex} className={className} style={getCellStyle(column, colSpan)} diff --git a/src/SummaryRow.tsx b/src/SummaryRow.tsx index f400fdf315..ddc7ecf88f 100644 --- a/src/SummaryRow.tsx +++ b/src/SummaryRow.tsx @@ -1,19 +1,24 @@ import { memo } from 'react'; import { css } from 'ecij'; -import { classnames, getColSpan } from './utils'; +import { classnames } from './utils'; import type { RenderRowProps } from './types'; import { bottomSummaryRowClassname, rowClassname, - rowSelectedClassname, + rowActiveClassname, topSummaryRowClassname } from './style/row'; import SummaryCell from './SummaryCell'; type SharedRenderRowProps = Pick< RenderRowProps, - 'viewportColumns' | 'rowIdx' | 'gridRowStart' | 'selectCell' | 'isTreeGrid' + | 'iterateOverViewportColumnsForRow' + | 'rowIdx' + | 'gridRowStart' + | 'setPosition' + | 'activeCellIdx' + | 'isTreeGrid' >; interface SummaryRowProps extends SharedRenderRowProps { @@ -21,8 +26,6 @@ interface SummaryRowProps extends SharedRenderRowProps { row: SR; top: number | undefined; bottom: number | undefined; - lastFrozenColumnIndex: number; - selectedCellIdx: number | undefined; isTop: boolean; } @@ -39,41 +42,30 @@ function SummaryRow({ rowIdx, gridRowStart, row, - viewportColumns, + iterateOverViewportColumnsForRow, + activeCellIdx, + setPosition, top, bottom, - lastFrozenColumnIndex, - selectedCellIdx, isTop, - selectCell, isTreeGrid, 'aria-rowindex': ariaRowIndex }: SummaryRowProps) { - const isPositionOnRow = selectedCellIdx === -1; + const isPositionOnRow = activeCellIdx === -1; - const cells = []; - - for (let index = 0; index < viewportColumns.length; index++) { - const column = viewportColumns[index]; - const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'SUMMARY', row }); - if (colSpan !== undefined) { - index += colSpan - 1; - } - - const isCellSelected = selectedCellIdx === column.idx; - - cells.push( + const cells = iterateOverViewportColumnsForRow(activeCellIdx, { type: 'SUMMARY', row }) + .map(([column, colSpan]) => ( key={column.key} column={column} colSpan={colSpan} row={row} rowIdx={rowIdx} - isCellSelected={isCellSelected} - selectCell={selectCell} + isCellActive={activeCellIdx === column.idx} + setPosition={setPosition} /> - ); - } + )) + .toArray(); return (
({ `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, summaryRowClassname, isTop ? topSummaryRowClassname : bottomSummaryRowClassname, - isPositionOnRow && rowSelectedClassname + isPositionOnRow && rowActiveClassname )} style={{ gridRowStart, diff --git a/src/TreeDataGrid.tsx b/src/TreeDataGrid.tsx index bafb2e0f10..58c19aaf90 100644 --- a/src/TreeDataGrid.tsx +++ b/src/TreeDataGrid.tsx @@ -297,7 +297,7 @@ export function TreeDataGrid({ if (event.isGridDefaultPrevented()) return; if (args.mode === 'EDIT') return; - const { column, rowIdx, selectCell } = args; + const { column, rowIdx, setPosition } = args; const idx = column?.idx ?? -1; const row = rows[rowIdx]; @@ -320,7 +320,7 @@ export function TreeDataGrid({ const parentRowAndIndex = getParentRowAndIndex(row); if (parentRowAndIndex !== undefined) { event.preventGridDefault(); - selectCell({ idx, rowIdx: parentRowAndIndex[1] }); + setPosition({ idx, rowIdx: parentRowAndIndex[1] }); } } } @@ -377,9 +377,9 @@ export function TreeDataGrid({ onCellDoubleClick, onCellContextMenu, onRowChange, - lastFrozenColumnIndex, draggedOverCellIdx, - selectedCellEditor, + activeCellEditor, + isRowSelectionDisabled, isTreeGrid, ...rowProps }: RenderRowProps @@ -416,9 +416,9 @@ export function TreeDataGrid({ onCellDoubleClick, onCellContextMenu, onRowChange, - lastFrozenColumnIndex, draggedOverCellIdx, - selectedCellEditor, + activeCellEditor, + isRowSelectionDisabled, isTreeGrid }); } diff --git a/src/hooks/useRovingTabIndex.ts b/src/hooks/useRovingTabIndex.ts index 869b83e796..74a144b457 100644 --- a/src/hooks/useRovingTabIndex.ts +++ b/src/hooks/useRovingTabIndex.ts @@ -1,11 +1,11 @@ import { useState } from 'react'; // https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_general_within -export function useRovingTabIndex(isSelected: boolean) { +export function useRovingTabIndex(isActive: boolean) { // https://www.w3.org/WAI/ARIA/apg/patterns/grid/#keyboardinteraction-settingfocusandnavigatinginsidecells const [isChildFocused, setIsChildFocused] = useState(false); - if (isChildFocused && !isSelected) { + if (isChildFocused && !isActive) { setIsChildFocused(false); } @@ -28,11 +28,11 @@ export function useRovingTabIndex(isSelected: boolean) { } } - const isFocusable = isSelected && !isChildFocused; + const isFocusable = isActive && !isChildFocused; return { tabIndex: isFocusable ? 0 : -1, - childTabIndex: isSelected ? 0 : -1, - onFocus: isSelected ? onFocus : undefined + childTabIndex: isActive ? 0 : -1, + onFocus: isActive ? onFocus : undefined }; } diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index c79d085399..c14b3ff1be 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -1,7 +1,14 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { getColSpan } from '../utils'; -import type { CalculatedColumn, Maybe } from '../types'; +import type { + CalculatedColumn, + ColSpanArgs, + IterateOverViewportColumns, + IterateOverViewportColumnsForRow, + Maybe, + ViewportColumnWithColSpan +} from '../types'; interface ViewportColumnsArgs { columns: readonly CalculatedColumn[]; @@ -32,63 +39,45 @@ export function useViewportColumns({ const startIdx = useMemo(() => { if (colOverscanStartIdx === 0) return 0; - let startIdx = colOverscanStartIdx; - - const updateStartIdx = (colIdx: number, colSpan: number | undefined) => { - if (colSpan !== undefined && colIdx + colSpan > colOverscanStartIdx) { - startIdx = colIdx; - return true; - } - return false; - }; - - for (const column of colSpanColumns) { + function* iterateOverRowsForColSpanArgs(): Generator> { // check header row - const colIdx = column.idx; - if (colIdx >= startIdx) break; - if (updateStartIdx(colIdx, getColSpan(column, lastFrozenColumnIndex, { type: 'HEADER' }))) { - break; + yield { type: 'HEADER' }; + + // check top summary rows + if (topSummaryRows != null) { + for (const row of topSummaryRows) { + yield { type: 'SUMMARY', row }; + } } // check viewport rows for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { - const row = rows[rowIdx]; - if ( - updateStartIdx(colIdx, getColSpan(column, lastFrozenColumnIndex, { type: 'ROW', row })) - ) { - break; - } + yield { type: 'ROW', row: rows[rowIdx] }; } - // check summary rows - if (topSummaryRows != null) { - for (const row of topSummaryRows) { - if ( - updateStartIdx( - colIdx, - getColSpan(column, lastFrozenColumnIndex, { type: 'SUMMARY', row }) - ) - ) { - break; - } + // check bottom summary rows + if (bottomSummaryRows != null) { + for (const row of bottomSummaryRows) { + yield { type: 'SUMMARY', row }; } } + } - if (bottomSummaryRows != null) { - for (const row of bottomSummaryRows) { - if ( - updateStartIdx( - colIdx, - getColSpan(column, lastFrozenColumnIndex, { type: 'SUMMARY', row }) - ) - ) { - break; - } + for (const column of colSpanColumns) { + if (column.frozen) continue; + const colIdx = column.idx; + if (colIdx >= colOverscanStartIdx) break; + + for (const args of iterateOverRowsForColSpanArgs()) { + const colSpan = getColSpan(column, lastFrozenColumnIndex, args); + + if (colSpan !== undefined && colIdx + colSpan > colOverscanStartIdx) { + return colIdx; } } } - return startIdx; + return colOverscanStartIdx; }, [ rowOverscanStartIdx, rowOverscanEndIdx, @@ -100,15 +89,67 @@ export function useViewportColumns({ colSpanColumns ]); - return useMemo((): readonly CalculatedColumn[] => { - const viewportColumns: CalculatedColumn[] = []; - for (let colIdx = 0; colIdx <= colOverscanEndIdx; colIdx++) { - const column = columns[colIdx]; + const iterateOverViewportColumns = useCallback>( + function* (activeColumnIdx): Generator> { + for (let colIdx = 0; colIdx <= lastFrozenColumnIndex; colIdx++) { + yield columns[colIdx]; + } - if (colIdx < startIdx && !column.frozen) continue; - viewportColumns.push(column); - } + if (columns.length === lastFrozenColumnIndex + 1) return; + + if (activeColumnIdx > lastFrozenColumnIndex && activeColumnIdx < startIdx) { + yield columns[activeColumnIdx]; + } + + for (let colIdx = startIdx; colIdx <= colOverscanEndIdx; colIdx++) { + yield columns[colIdx]; + } + + if (activeColumnIdx > colOverscanEndIdx && activeColumnIdx < columns.length) { + yield columns[activeColumnIdx]; + } + }, + [startIdx, colOverscanEndIdx, columns, lastFrozenColumnIndex] + ); + + const iterateOverViewportColumnsForRow = useCallback>( + function* (activeColumnIdx = -1, args): Generator> { + const iterator = iterateOverViewportColumns(activeColumnIdx); + + for (const column of iterator) { + let colSpan = args && getColSpan(column, lastFrozenColumnIndex, args); + + yield [column, colSpan]; + + // skip columns covered by colSpan + while (colSpan !== undefined && colSpan > 1) { + iterator.next(); + colSpan--; + } + } + }, + [iterateOverViewportColumns, lastFrozenColumnIndex] + ); + + const iterateOverViewportColumnsForRowOutsideOfViewport = useCallback< + IterateOverViewportColumnsForRow + >( + function* (activeColumnIdx = -1, args): Generator> { + if (activeColumnIdx >= 0 && activeColumnIdx < columns.length) { + const column = columns[activeColumnIdx]; + yield [column, args && getColSpan(column, lastFrozenColumnIndex, args)]; + } + }, + [columns, lastFrozenColumnIndex] + ); + + const viewportColumns = useMemo((): readonly CalculatedColumn[] => { + return iterateOverViewportColumns(-1).toArray(); + }, [iterateOverViewportColumns]); - return viewportColumns; - }, [startIdx, colOverscanEndIdx, columns]); + return { + viewportColumns, + iterateOverViewportColumnsForRow, + iterateOverViewportColumnsForRowOutsideOfViewport + } as const; } diff --git a/src/index.ts b/src/index.ts index ce5f9900fa..7a474ca5cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,8 @@ import './style/layers.css'; export { DataGrid, - type DataGridProps, type DataGridHandle, + type DataGridProps, type DefaultColumnOptions } from './DataGrid'; export { TreeDataGrid, type TreeDataGridProps } from './TreeDataGrid'; @@ -15,7 +15,7 @@ export * from './cellRenderers'; export { renderTextEditor } from './editors/renderTextEditor'; export { default as renderHeaderCell } from './renderHeaderCell'; export { renderSortIcon, renderSortPriority } from './sortStatus'; -export { useRowSelection, useHeaderRowSelection } from './hooks'; +export { useHeaderRowSelection, useRowSelection } from './hooks'; export type { CalculatedColumn, CalculatedColumnOrColumnGroup, @@ -27,7 +27,6 @@ export type { CellMouseEvent, CellPasteArgs, CellRendererProps, - CellSelectArgs, ColSpanArgs, Column, ColumnGroup, @@ -36,6 +35,7 @@ export type { ColumnWidths, Direction, FillEvent, + PositionChangeArgs, RenderCellProps, RenderCheckboxProps, RenderEditCellProps, @@ -49,9 +49,9 @@ export type { RenderSummaryCellProps, RowHeightArgs, RowsChangeData, - SelectCellOptions, SelectHeaderRowEvent, SelectRowEvent, + SetPositionOptions, SortColumn, SortDirection } from './types'; diff --git a/src/style/row.ts b/src/style/row.ts index 7eaf1a43b0..a7feeb7743 100644 --- a/src/style/row.ts +++ b/src/style/row.ts @@ -49,7 +49,7 @@ export const row = css` export const rowClassname = `rdg-row ${row}`; -export const rowSelectedClassname = 'rdg-row-selected'; +export const rowActiveClassname = 'rdg-row-active'; export const topSummaryRowClassname = 'rdg-top-summary-row'; diff --git a/src/types.ts b/src/types.ts index 3eb61268aa..dafff8b6b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -150,7 +150,7 @@ export interface RenderEditCellProps { row: TRow; rowIdx: number; onRowChange: (row: TRow, commitChanges?: boolean) => void; - onClose: (commitChanges?: boolean, shouldFocusCell?: boolean) => void; + onClose: (commitChanges?: boolean, shouldFocus?: boolean) => void; } export interface RenderHeaderCellProps { @@ -168,7 +168,7 @@ interface BaseCellRendererProps 'onCellMouseDown' | 'onCellClick' | 'onCellDoubleClick' | 'onCellContextMenu' > { rowIdx: number; - selectCell: (position: Position, options?: SelectCellOptions) => void; + setPosition: (position: Position, options?: SetPositionOptions) => void; } export interface CellRendererProps extends BaseCellRendererProps< @@ -179,8 +179,8 @@ export interface CellRendererProps extends BaseCellRendererPr row: TRow; colSpan: number | undefined; isDraggedOver: boolean; - isCellSelected: boolean; - onRowChange: (column: CalculatedColumn, newRow: TRow) => void; + isCellActive: boolean; + onRowChange: (column: CalculatedColumn, rowIdx: number, newRow: TRow) => void; } export type CellEvent> = E & { @@ -195,18 +195,22 @@ export type CellKeyboardEvent = CellEvent>; export type CellClipboardEvent = React.ClipboardEvent; export interface CellMouseArgs { + /** The column object of the cell. */ column: CalculatedColumn; + /** The row object of the cell. */ row: TRow; + /** The row index of the cell. */ rowIdx: number; - selectCell: (enableEditor?: boolean) => void; + /** Function to manually focus the cell. Pass `true` to immediately start editing. */ + setPosition: (enableEditor?: boolean) => void; } -interface SelectCellKeyDownArgs { - mode: 'SELECT'; +interface ActiveCellKeyDownArgs { + mode: 'ACTIVE'; column: CalculatedColumn | undefined; - row: TRow; + row: TRow | undefined; rowIdx: number; - selectCell: (position: Position, options?: SelectCellOptions) => void; + setPosition: (position: Position, options?: SetPositionOptions) => void; } export interface EditCellKeyDownArgs { @@ -215,30 +219,53 @@ export interface EditCellKeyDownArgs { row: TRow; rowIdx: number; navigate: () => void; - onClose: (commitChanges?: boolean, shouldFocusCell?: boolean) => void; + onClose: (commitChanges?: boolean, shouldFocus?: boolean) => void; } export type CellKeyDownArgs = - | SelectCellKeyDownArgs + | ActiveCellKeyDownArgs | EditCellKeyDownArgs; -export interface CellSelectArgs { +export interface PositionChangeArgs { + /** row index of the active position */ rowIdx: number; + /** + * row object of the active position, + * undefined if the active position is on a header or summary row + */ row: TRow | undefined; - column: CalculatedColumn; + /** + * column object of the active position, + * undefined if the active position is a row instead of a cell + */ + column: CalculatedColumn | undefined; } export type CellMouseEventHandler = Maybe< (args: CellMouseArgs, NoInfer>, event: CellMouseEvent) => void >; +export type IterateOverViewportColumns = ( + activeColumnIdx: number +) => IteratorObject>; + +export type ViewportColumnWithColSpan = [ + column: CalculatedColumn, + colSpan: number | undefined +]; + +export type IterateOverViewportColumnsForRow = ( + activeIdx: number | undefined, + args?: ColSpanArgs +) => IteratorObject>; + export interface BaseRenderRowProps extends BaseCellRendererProps< TRow, TSummaryRow > { - viewportColumns: readonly CalculatedColumn[]; + iterateOverViewportColumnsForRow: IterateOverViewportColumnsForRow; rowIdx: number; - selectedCellIdx: number | undefined; + activeCellIdx: number | undefined; isRowSelectionDisabled: boolean; isRowSelected: boolean; gridRowStart: number; @@ -249,9 +276,8 @@ export interface RenderRowProps extends BaseRenderR TSummaryRow > { row: TRow; - lastFrozenColumnIndex: number; draggedOverCellIdx: number | undefined; - selectedCellEditor: ReactElement> | undefined; + activeCellEditor: ReactElement> | undefined; onRowChange: (column: CalculatedColumn, rowIdx: number, newRow: TRow) => void; rowClass: Maybe<(row: TRow, rowIdx: number) => Maybe>; isTreeGrid: boolean; @@ -307,9 +333,9 @@ export type CellNavigationMode = 'NONE' | 'CHANGE_ROW'; export type SortDirection = 'ASC' | 'DESC'; export type ColSpanArgs = - | { type: 'HEADER' } - | { type: 'ROW'; row: TRow } - | { type: 'SUMMARY'; row: TSummaryRow }; + | { readonly type: 'HEADER' } + | { readonly type: 'ROW'; readonly row: TRow } + | { readonly type: 'SUMMARY'; readonly row: TSummaryRow }; export type RowHeightArgs = | { type: 'ROW'; row: TRow } @@ -341,9 +367,9 @@ export interface Renderers { noRowsFallback?: Maybe; } -export interface SelectCellOptions { +export interface SetPositionOptions { enableEditor?: Maybe; - shouldFocusCell?: Maybe; + shouldFocus?: Maybe; } export interface ColumnWidth { diff --git a/src/utils/selectedCellUtils.ts b/src/utils/activePositionUtils.ts similarity index 83% rename from src/utils/selectedCellUtils.ts rename to src/utils/activePositionUtils.ts index db13f05da6..85f125e5d2 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/activePositionUtils.ts @@ -7,22 +7,6 @@ import type { } from '../types'; import { getColSpan } from './colSpanUtils'; -interface IsSelectedCellEditableOpts { - selectedPosition: Position; - columns: readonly CalculatedColumn[]; - rows: readonly R[]; -} - -export function isSelectedCellEditable({ - selectedPosition, - columns, - rows -}: IsSelectedCellEditableOpts): boolean { - const column = columns[selectedPosition.idx]; - const row = rows[selectedPosition.rowIdx]; - return isCellEditableUtil(column, row); -} - // https://github.com/vercel/next.js/issues/56480 export function isCellEditableUtil(column: CalculatedColumn, row: R): boolean { return ( @@ -31,7 +15,7 @@ export function isCellEditableUtil(column: CalculatedColumn, row: ); } -interface GetNextSelectedCellPositionOpts { +interface GetNextPositionOpts { moveUp: boolean; moveNext: boolean; cellNavigationMode: CellNavigationMode; @@ -43,13 +27,13 @@ interface GetNextSelectedCellPositionOpts { minRowIdx: number; mainHeaderRowIdx: number; maxRowIdx: number; - currentPosition: Position; + activePosition: Position; nextPosition: Position; + nextPositionIsCellInActiveBounds: boolean; lastFrozenColumnIndex: number; - isCellWithinBounds: (position: Position) => boolean; } -function getSelectedCellColSpan({ +function getCellColSpan({ rows, topSummaryRows, bottomSummaryRows, @@ -58,7 +42,7 @@ function getSelectedCellColSpan({ lastFrozenColumnIndex, column }: Pick< - GetNextSelectedCellPositionOpts, + GetNextPositionOpts, 'rows' | 'topSummaryRows' | 'bottomSummaryRows' | 'lastFrozenColumnIndex' | 'mainHeaderRowIdx' > & { rowIdx: number; @@ -95,7 +79,7 @@ function getSelectedCellColSpan({ return undefined; } -export function getNextSelectedCellPosition({ +export function getNextActivePosition({ moveUp, moveNext, cellNavigationMode, @@ -107,21 +91,21 @@ export function getNextSelectedCellPosition({ minRowIdx, mainHeaderRowIdx, maxRowIdx, - currentPosition: { idx: currentIdx, rowIdx: currentRowIdx }, + activePosition: { idx: activeIdx, rowIdx: activeRowIdx }, nextPosition, - lastFrozenColumnIndex, - isCellWithinBounds -}: GetNextSelectedCellPositionOpts): Position { + nextPositionIsCellInActiveBounds, + lastFrozenColumnIndex +}: GetNextPositionOpts): Position { let { idx: nextIdx, rowIdx: nextRowIdx } = nextPosition; const columnsCount = columns.length; const setColSpan = (moveNext: boolean) => { - // If a cell within the colspan range is selected then move to the + // If a cell within the colspan range is active then move to the // previous or the next cell depending on the navigation direction for (const column of colSpanColumns) { const colIdx = column.idx; if (colIdx > nextIdx) break; - const colSpan = getSelectedCellColSpan({ + const colSpan = getCellColSpan({ rows, topSummaryRows, bottomSummaryRows, @@ -173,13 +157,13 @@ export function getNextSelectedCellPosition({ // keep the current position if there is no parent matching the new row position if (!found) { - nextIdx = currentIdx; - nextRowIdx = currentRowIdx; + nextIdx = activeIdx; + nextRowIdx = activeRowIdx; } } }; - if (isCellWithinBounds(nextPosition)) { + if (nextPositionIsCellInActiveBounds) { setColSpan(moveNext); if (nextRowIdx < mainHeaderRowIdx) { @@ -232,7 +216,7 @@ interface CanExitGridOpts { maxColIdx: number; minRowIdx: number; maxRowIdx: number; - selectedPosition: Position; + activePosition: Position; shiftKey: boolean; } @@ -240,7 +224,7 @@ export function canExitGrid({ maxColIdx, minRowIdx, maxRowIdx, - selectedPosition: { rowIdx, idx }, + activePosition: { rowIdx, idx }, shiftKey }: CanExitGridOpts): boolean { // Exit the grid if we're at the first or last cell of the grid diff --git a/src/utils/colSpanUtils.ts b/src/utils/colSpanUtils.ts index e986ba90b3..da83a5c6d3 100644 --- a/src/utils/colSpanUtils.ts +++ b/src/utils/colSpanUtils.ts @@ -5,7 +5,10 @@ export function getColSpan( lastFrozenColumnIndex: number, args: ColSpanArgs ): number | undefined { - const colSpan = typeof column.colSpan === 'function' ? column.colSpan(args) : 1; + if (typeof column.colSpan !== 'function') return undefined; + + const colSpan = column.colSpan(args); + if ( Number.isInteger(colSpan) && colSpan! > 1 && @@ -14,5 +17,6 @@ export function getColSpan( ) { return colSpan!; } + return undefined; } diff --git a/src/utils/index.ts b/src/utils/index.ts index f6bd990888..ad576ba528 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,11 +1,11 @@ import type { CalculatedColumn, CalculatedColumnOrColumnGroup, Maybe } from '../types'; +export * from './activePositionUtils'; export * from './colSpanUtils'; export * from './domUtils'; export * from './eventUtils'; export * from './keyboardUtils'; export * from './renderMeasuringCells'; -export * from './selectedCellUtils'; export * from './styleUtils'; export const { min, max, floor, sign, abs } = Math; diff --git a/test/browser/TreeDataGrid.test.tsx b/test/browser/TreeDataGrid.test.tsx index 3689661e96..8588f7c0c6 100644 --- a/test/browser/TreeDataGrid.test.tsx +++ b/test/browser/TreeDataGrid.test.tsx @@ -3,7 +3,7 @@ import { page, userEvent } from 'vitest/browser'; import type { Column } from '../../src'; import { renderTextEditor, SelectColumn, TreeDataGrid } from '../../src'; -import { rowSelectedClassname } from '../../src/style/row'; +import { rowActiveClassname } from '../../src/style/row'; import { getCellsAtRowIndex, getRowWithCell, testCount, testRowCount } from './utils'; const treeGrid = page.getTreeGrid(); @@ -11,7 +11,7 @@ const headerRow = treeGrid.getHeaderRow(); const headerCells = headerRow.getHeaderCell(); const headerCheckbox = headerRow.getSelectAllCheckbox(); const rows = treeGrid.getRow(); -const selectedCell = treeGrid.getSelectedCell(); +const activeCell = treeGrid.getActiveCell(); interface Row { id: number; @@ -214,9 +214,9 @@ test('should toggle group using keyboard', async () => { const groupCell = page.getCell({ name: '2021' }); await userEvent.click(groupCell); await testRowCount(7); - // clicking on the group cell selects the row - await expect.element(selectedCell).not.toBeInTheDocument(); - await expect.element(getRowWithCell(groupCell)).toHaveClass(rowSelectedClassname); + // clicking on the group cell focuses the row + await expect.element(activeCell).not.toBeInTheDocument(); + await expect.element(getRowWithCell(groupCell)).toHaveClass(rowActiveClassname); await userEvent.keyboard('{arrowright}{arrowright}{enter}'); await testRowCount(5); await userEvent.keyboard('{enter}'); @@ -320,65 +320,65 @@ test('cell navigation in a treegrid', async () => { const groupCell1 = row1.getCell({ name: 'USA' }); await expect.element(document.body).toHaveFocus(); await expect.element(row1).toHaveAttribute('tabIndex', '-1'); - await expect.element(row1).not.toHaveClass(rowSelectedClassname); + await expect.element(row1).not.toHaveClass(rowActiveClassname); await userEvent.click(groupCell1); await expect.element(row1).toHaveFocus(); await expect.element(row1).toHaveAttribute('tabIndex', '0'); - await expect.element(row1).toHaveClass(rowSelectedClassname); + await expect.element(row1).toHaveClass(rowActiveClassname); await userEvent.keyboard('{arrowup}'); await expect.element(topSummaryRow).toHaveFocus(); await expect.element(topSummaryRow).toHaveAttribute('tabIndex', '0'); - await expect.element(topSummaryRow).toHaveClass(rowSelectedClassname); + await expect.element(topSummaryRow).toHaveClass(rowActiveClassname); - // header row does not get selected + // header row does not get focused await userEvent.keyboard('{arrowup}'); await expect.element(headerCheckbox).toHaveFocus(); await expect.element(headerCheckbox).toHaveAttribute('tabIndex', '0'); - await expect.element(headerRow).not.toHaveClass(rowSelectedClassname); + await expect.element(headerRow).not.toHaveClass(rowActiveClassname); - // header row cannot get selected + // header row cannot get focused await userEvent.keyboard('{arrowleft}'); await expect.element(headerCheckbox).toHaveFocus(); await expect.element(headerCheckbox).toHaveAttribute('tabIndex', '0'); - await expect.element(headerRow).not.toHaveClass(rowSelectedClassname); + await expect.element(headerRow).not.toHaveClass(rowActiveClassname); await userEvent.keyboard('{arrowdown}'); await expect.element(topSummaryRow.getCell().nth(0)).toHaveFocus(); await expect.element(topSummaryRow.getCell().nth(0)).toHaveAttribute('tabIndex', '0'); - await expect.element(topSummaryRow).not.toHaveClass(rowSelectedClassname); + await expect.element(topSummaryRow).not.toHaveClass(rowActiveClassname); - // can select summary row + // can focus summary row await userEvent.keyboard('{arrowleft}'); await expect.element(topSummaryRow).toHaveFocus(); await expect.element(topSummaryRow).toHaveAttribute('tabIndex', '0'); - await expect.element(topSummaryRow).toHaveClass(rowSelectedClassname); + await expect.element(topSummaryRow).toHaveClass(rowActiveClassname); const groupCell2 = page.getCell({ name: '2021' }); await userEvent.click(groupCell2); await expect.element(row3).toHaveFocus(); await expect.element(row3).toHaveAttribute('tabIndex', '0'); - // select cell + // focus cell const cells = getCellsAtRowIndex(5); await userEvent.click(cells.nth(1)); await expect.element(cells.nth(1)).toHaveAttribute('aria-selected', 'true'); await expect.element(cells.nth(1)).toHaveFocus(); await expect.element(cells.nth(1)).toHaveAttribute('tabIndex', '0'); - // select the previous cell + // focus the previous cell await userEvent.keyboard('{arrowleft}'); await expect.element(cells.nth(1)).toHaveAttribute('aria-selected', 'false'); await expect.element(cells.nth(0)).toHaveAttribute('aria-selected', 'true'); - // if the first cell is selected then arrowleft should select the row + // if the first cell is focused then arrowleft should focus the row await userEvent.keyboard('{arrowleft}'); await expect.element(cells.nth(0)).toHaveAttribute('aria-selected', 'false'); - await expect.element(rows.nth(4)).toHaveClass(rowSelectedClassname); + await expect.element(rows.nth(4)).toHaveClass(rowActiveClassname); await expect.element(rows.nth(4)).toHaveFocus(); - // if the row is selected then arrowright should select the first cell on the same row + // if the row is focused then arrowright should focus the first cell on the same row await userEvent.keyboard('{arrowright}'); await expect.element(cells.nth(0)).toHaveAttribute('aria-selected', 'true'); @@ -394,20 +394,20 @@ test('cell navigation in a treegrid', async () => { await userEvent.keyboard('{arrowright}'); await testRowCount(7); - // left arrow on a collapsed group should select the parent group - await expect.element(rows.nth(1)).not.toHaveClass(rowSelectedClassname); + // left arrow on a collapsed group should focus the parent group + await expect.element(rows.nth(1)).not.toHaveClass(rowActiveClassname); await userEvent.keyboard('{arrowleft}{arrowleft}'); - await expect.element(rows.nth(1)).toHaveClass(rowSelectedClassname); + await expect.element(rows.nth(1)).toHaveClass(rowActiveClassname); await userEvent.keyboard('{end}'); - await expect.element(rows.nth(5)).toHaveClass(rowSelectedClassname); + await expect.element(rows.nth(5)).toHaveClass(rowActiveClassname); await userEvent.keyboard('{home}'); await expect.element(headerCheckbox).toHaveFocus(); await expect.element(headerCheckbox).toHaveAttribute('tabIndex', '0'); - await expect.element(headerRow).not.toHaveClass(rowSelectedClassname); + await expect.element(headerRow).not.toHaveClass(rowActiveClassname); - // collpase parent group + // collapse parent group await userEvent.keyboard('{arrowdown}{arrowdown}{arrowleft}{arrowleft}'); await expect.element(page.getCell({ name: '2021' })).not.toBeInTheDocument(); await testRowCount(4); @@ -453,9 +453,9 @@ test('update row using cell renderer', async () => { await userEvent.click(page.getCell({ name: '2021' })); await userEvent.click(page.getCell({ name: 'USA' })); await userEvent.keyboard('{arrowright}{arrowright}'); - await expect.element(selectedCell).toHaveTextContent('value: 2'); + await expect.element(activeCell).toHaveTextContent('value: 2'); await userEvent.click(page.getByRole('button', { name: 'value: 2' })); - await expect.element(selectedCell).toHaveTextContent('value: 12'); + await expect.element(activeCell).toHaveTextContent('value: 12'); }); test('custom renderGroupCell', async () => { diff --git a/test/browser/column/grouping.test.ts b/test/browser/column/grouping.test.ts index ac4696b7e1..069b473f62 100644 --- a/test/browser/column/grouping.test.ts +++ b/test/browser/column/grouping.test.ts @@ -254,8 +254,8 @@ test('grouping', async () => { test('keyboard navigation', async () => { await setup({ columns, rows: [{}] }, true); - // no initial selection - await expect.element(grid.getSelectedCell()).not.toBeInTheDocument(); + // no initial active position + await expect.element(grid.getActiveCell()).not.toBeInTheDocument(); await tabIntoGrid(); await validateCellPosition(0, 3); diff --git a/test/browser/column/renderEditCell.test.tsx b/test/browser/column/renderEditCell.test.tsx index 2294e72ce9..c48d070cda 100644 --- a/test/browser/column/renderEditCell.test.tsx +++ b/test/browser/column/renderEditCell.test.tsx @@ -88,7 +88,7 @@ describe('Editor', () => { ]); }); - it('should scroll to the editor if selected cell is not in the viewport', async () => { + it('should scroll to the editor if active cell is not in the viewport', async () => { const rows: Row[] = []; for (let i = 0; i < 99; i++) { rows.push({ col1: i, col2: `${i}` }); @@ -96,16 +96,16 @@ describe('Editor', () => { await page.render(); await userEvent.click(getCellsAtRowIndex(0).nth(0)); - const selectedRowCells = getRowWithCell(page.getSelectedCell()).getCell(); - await testCount(selectedRowCells, 2); + const activeRowCells = getRowWithCell(page.getActiveCell()).getCell(); + await testCount(activeRowCells, 2); await scrollGrid({ top: 2001 }); - await testCount(selectedRowCells, 1); + await testCount(activeRowCells, 1); const editor = grid.getByRole('spinbutton', { name: 'col1-editor' }); await expect.element(editor).not.toBeInTheDocument(); await expect.element(grid).toHaveProperty('scrollTop', 2001); // TODO: await userEvent.keyboard('123'); fails in FF await userEvent.keyboard('{enter}123'); - await testCount(selectedRowCells, 2); + await testCount(activeRowCells, 2); await expect.element(editor).toHaveValue(123); await expect.element(grid).toHaveProperty('scrollTop', 0); }); @@ -194,7 +194,7 @@ describe('Editor', () => { await page.render( { - if (args.mode === 'SELECT' && event.key === 'x') { + if (args.mode === 'ACTIVE' && event.key === 'x') { event.preventGridDefault(); } }} @@ -273,7 +273,7 @@ describe('Editor', () => { await scrollGrid({ top: 1500 }); await userEvent.click(page.getCell({ name: 'name43' })); - await expect.element(page.getSelectedCell()).toHaveTextContent(/^name43$/); + await expect.element(page.getActiveCell()).toHaveTextContent(/^name43$/); await scrollGrid({ top: 0 }); await expect.element(page.getCell({ name: 'name0abc' })).toBeVisible(); }); diff --git a/test/browser/copyPaste.test.tsx b/test/browser/copyPaste.test.tsx index e9ace61216..4faccc1315 100644 --- a/test/browser/copyPaste.test.tsx +++ b/test/browser/copyPaste.test.tsx @@ -133,5 +133,5 @@ test('should not start editing when pressing ctrl+', async () => { await setup(); await userEvent.click(getCellsAtRowIndex(1).nth(0)); await userEvent.keyboard('{Control>}b'); - await expect.element(page.getSelectedCell()).not.toHaveClass('rdg-editor-container'); + await expect.element(page.getActiveCell()).not.toHaveClass('rdg-editor-container'); }); diff --git a/test/browser/direction.test.ts b/test/browser/direction.test.ts index 6c31733aa9..870eece8cf 100644 --- a/test/browser/direction.test.ts +++ b/test/browser/direction.test.ts @@ -4,7 +4,7 @@ import type { Column } from '../../src'; import { setup, tabIntoGrid } from './utils'; const grid = page.getGrid(); -const selectedCell = grid.getSelectedCell(); +const activeCell = grid.getActiveCell(); interface Row { id: number; @@ -28,25 +28,25 @@ test('should use left to right direction by default', async () => { await setup({ rows, columns }, true); await expect.element(grid).toHaveAttribute('dir', 'ltr'); await tabIntoGrid(); - await expect.element(selectedCell).toHaveTextContent('ID'); + await expect.element(activeCell).toHaveTextContent('ID'); await userEvent.keyboard('{ArrowRight}'); - await expect.element(selectedCell).toHaveTextContent('Name'); + await expect.element(activeCell).toHaveTextContent('Name'); }); test('should use left to right direction if direction prop is set to ltr', async () => { await setup({ rows, columns, direction: 'ltr' }, true); await expect.element(grid).toHaveAttribute('dir', 'ltr'); await tabIntoGrid(); - await expect.element(selectedCell).toHaveTextContent('ID'); + await expect.element(activeCell).toHaveTextContent('ID'); await userEvent.keyboard('{ArrowRight}'); - await expect.element(selectedCell).toHaveTextContent('Name'); + await expect.element(activeCell).toHaveTextContent('Name'); }); test('should use right to left direction if direction prop is set to rtl', async () => { await setup({ rows, columns, direction: 'rtl' }, true); await expect.element(grid).toHaveAttribute('dir', 'rtl'); await tabIntoGrid(); - await expect.element(selectedCell).toHaveTextContent('ID'); + await expect.element(activeCell).toHaveTextContent('ID'); await userEvent.keyboard('{ArrowLeft}'); - await expect.element(selectedCell).toHaveTextContent('Name'); + await expect.element(activeCell).toHaveTextContent('Name'); }); diff --git a/test/browser/events.test.tsx b/test/browser/events.test.tsx index 574001b9cb..b205f93b42 100644 --- a/test/browser/events.test.tsx +++ b/test/browser/events.test.tsx @@ -74,7 +74,7 @@ describe('Events', () => { onCellClick={(args, event) => { if (args.column.key === 'col2') { event.preventGridDefault(); - args.selectCell(true); + args.setPosition(true); } }} /> @@ -118,66 +118,66 @@ describe('Events', () => { ); }); - it('should call onSelectedCellChange when cell selection is changed', async () => { - const onSelectedCellChange = vi.fn(); + it('should call onActivePositionChange when cell selection is changed', async () => { + const onActivePositionChange = vi.fn(); - await page.render(); + await page.render(); - expect(onSelectedCellChange).not.toHaveBeenCalled(); + expect(onActivePositionChange).not.toHaveBeenCalled(); // Selected by click await userEvent.click(page.getCell({ name: 'a1' })); - expect(onSelectedCellChange).toHaveBeenLastCalledWith({ + expect(onActivePositionChange).toHaveBeenLastCalledWith({ column: expect.objectContaining(columns[1]), row: rows[0], rowIdx: 0 }); - expect(onSelectedCellChange).toHaveBeenCalledOnce(); + expect(onActivePositionChange).toHaveBeenCalledOnce(); // Selected by double click await userEvent.dblClick(page.getCell({ name: '1' })); - expect(onSelectedCellChange).toHaveBeenLastCalledWith({ + expect(onActivePositionChange).toHaveBeenLastCalledWith({ column: expect.objectContaining(columns[0]), row: rows[0], rowIdx: 0 }); - expect(onSelectedCellChange).toHaveBeenCalledTimes(2); + expect(onActivePositionChange).toHaveBeenCalledTimes(2); // Selected by right-click await userEvent.click(page.getCell({ name: '2' }), { button: 'right' }); - expect(onSelectedCellChange).toHaveBeenLastCalledWith({ + expect(onActivePositionChange).toHaveBeenLastCalledWith({ column: expect.objectContaining(columns[0]), row: rows[1], rowIdx: 1 }); - expect(onSelectedCellChange).toHaveBeenCalledTimes(3); + expect(onActivePositionChange).toHaveBeenCalledTimes(3); // Selected by ←↑→↓ keys await userEvent.keyboard('{ArrowUp}'); - expect(onSelectedCellChange).toHaveBeenLastCalledWith({ + expect(onActivePositionChange).toHaveBeenLastCalledWith({ column: expect.objectContaining(columns[0]), row: rows[0], rowIdx: 0 }); - expect(onSelectedCellChange).toHaveBeenCalledTimes(4); + expect(onActivePositionChange).toHaveBeenCalledTimes(4); // Selected by tab key await userEvent.keyboard('{Tab}'); - expect(onSelectedCellChange).toHaveBeenLastCalledWith({ + expect(onActivePositionChange).toHaveBeenLastCalledWith({ column: expect.objectContaining(columns[1]), row: rows[0], rowIdx: 0 }); - expect(onSelectedCellChange).toHaveBeenCalledTimes(5); + expect(onActivePositionChange).toHaveBeenCalledTimes(5); // go to the header row await userEvent.keyboard('{ArrowUp}'); - expect(onSelectedCellChange).toHaveBeenLastCalledWith({ + expect(onActivePositionChange).toHaveBeenLastCalledWith({ column: expect.objectContaining(columns[1]), row: undefined, rowIdx: -1 }); - expect(onSelectedCellChange).toHaveBeenCalledTimes(6); + expect(onActivePositionChange).toHaveBeenCalledTimes(6); }); }); @@ -187,7 +187,7 @@ type EventProps = Pick< | 'onCellClick' | 'onCellDoubleClick' | 'onCellContextMenu' - | 'onSelectedCellChange' + | 'onActivePositionChange' >; function EventTest(props: EventProps) { diff --git a/test/browser/keyboardNavigation.test.tsx b/test/browser/keyboardNavigation.test.tsx index 91cf9f3a01..3989d49b38 100644 --- a/test/browser/keyboardNavigation.test.tsx +++ b/test/browser/keyboardNavigation.test.tsx @@ -11,7 +11,7 @@ import { validateCellPosition } from './utils'; -const selectedCell = page.getSelectedCell(); +const activeCell = page.getActiveCell(); type Row = undefined; @@ -32,8 +32,8 @@ const columns: readonly Column[] = [ test('keyboard navigation', async () => { await setup({ columns, rows, topSummaryRows, bottomSummaryRows }, true); - // no initial selection - await expect.element(selectedCell).not.toBeInTheDocument(); + // no initial active position + await expect.element(activeCell).not.toBeInTheDocument(); // tab into the grid await tabIntoGrid(); @@ -95,12 +95,12 @@ test('keyboard navigation', async () => { await userEvent.keyboard('{PageUp}'); await validateCellPosition(0, 0); - // tab at the end of a row selects the first cell on the next row + // tab at the end of a row focuses the first cell on the next row await userEvent.keyboard('{end}'); await userEvent.tab(); await validateCellPosition(0, 1); - // shift tab should select the last cell of the previous row + // shift tab should focus the last cell of the previous row await userEvent.tab({ shift: true }); await validateCellPosition(6, 0); }); @@ -136,8 +136,8 @@ test('grid enter/exit', async () => { const beforeButton = page.getByRole('button', { name: 'Before' }); const afterButton = page.getByRole('button', { name: 'After' }); - // no initial selection - await expect.element(selectedCell).not.toBeInTheDocument(); + // no initial active position + await expect.element(activeCell).not.toBeInTheDocument(); // tab into the grid await tabIntoGrid(); @@ -153,18 +153,18 @@ test('grid enter/exit', async () => { await userEvent.keyboard('{arrowdown}{arrowdown}'); await validateCellPosition(0, 2); - // tab should select the last selected cell + // tab should focus the last active cell // click outside the grid await userEvent.click(beforeButton); await userEvent.tab(); await userEvent.keyboard('{arrowdown}'); await validateCellPosition(0, 3); - // shift+tab should select the last selected cell + // shift+tab should focus the last active cell await userEvent.click(afterButton); await userEvent.tab({ shift: true }); await validateCellPosition(0, 3); - await expect.element(selectedCell.getByRole('checkbox')).toHaveFocus(); + await expect.element(activeCell.getByRole('checkbox')).toHaveFocus(); // tab tabs out of the grid if we are at the last cell await userEvent.keyboard('{Control>}{end}{/Control}'); @@ -179,15 +179,15 @@ test('navigation with focusable cell renderer', async () => { await validateCellPosition(0, 1); // cell should not set tabIndex to 0 if it contains a focusable cell renderer - await expect.element(selectedCell).toHaveAttribute('tabIndex', '-1'); - const checkbox = selectedCell.getByRole('checkbox'); + await expect.element(activeCell).toHaveAttribute('tabIndex', '-1'); + const checkbox = activeCell.getByRole('checkbox'); await expect.element(checkbox).toHaveFocus(); await expect.element(checkbox).toHaveAttribute('tabIndex', '0'); await userEvent.tab(); await validateCellPosition(1, 1); // cell should set tabIndex to 0 if it does not have focusable cell renderer - await expect.element(selectedCell).toHaveAttribute('tabIndex', '0'); + await expect.element(activeCell).toHaveAttribute('tabIndex', '0'); }); test('navigation when header and summary rows have focusable elements', async () => { @@ -249,12 +249,12 @@ test('navigation when header and summary rows have focusable elements', async () await userEvent.tab({ shift: true }); await userEvent.tab({ shift: true }); await validateCellPosition(1, 2); - await expect.element(selectedCell).toHaveFocus(); + await expect.element(activeCell).toHaveFocus(); }); -test('navigation when selected cell not in the viewport', async () => { +test('navigation when active cell not in the viewport', async () => { const columns: Column[] = [SelectColumn]; - const selectedRowCells = getRowWithCell(selectedCell).getCell(); + const activeRowCells = getRowWithCell(activeCell).getCell(); for (let i = 0; i < 99; i++) { columns.push({ key: `col${i}`, name: `col${i}`, frozen: i < 5 }); } @@ -264,12 +264,12 @@ test('navigation when selected cell not in the viewport', async () => { await userEvent.keyboard('{Control>}{end}{/Control}{arrowup}{arrowup}'); await validateCellPosition(99, 100); - await expect.element(selectedRowCells).not.toHaveLength(1); + await expect.element(activeRowCells).not.toHaveLength(1); await scrollGrid({ top: 0 }); - await testCount(selectedRowCells, 1); + await testCount(activeRowCells, 1); await userEvent.keyboard('{arrowup}'); await validateCellPosition(99, 99); - await expect.element(selectedRowCells).not.toHaveLength(1); + await expect.element(activeRowCells).not.toHaveLength(1); await scrollGrid({ left: 0 }); await userEvent.keyboard('{arrowdown}'); @@ -284,7 +284,7 @@ test('navigation when selected cell not in the viewport', async () => { await validateCellPosition(6, 100); }); -test('reset selected cell when column is removed', async () => { +test('reset active cell when column is removed', async () => { const columns: readonly Column[] = [ { key: '1', name: '1' }, { key: '2', name: '2' } @@ -303,10 +303,10 @@ test('reset selected cell when column is removed', async () => { await rerender(); - await expect.element(selectedCell).not.toBeInTheDocument(); + await expect.element(activeCell).not.toBeInTheDocument(); }); -test('reset selected cell when row is removed', async () => { +test('reset active cell when row is removed', async () => { const columns: readonly Column[] = [ { key: '1', name: '1' }, { key: '2', name: '2' } @@ -325,7 +325,7 @@ test('reset selected cell when row is removed', async () => { await rerender(); - await expect.element(selectedCell).not.toBeInTheDocument(); + await expect.element(activeCell).not.toBeInTheDocument(); }); test('should not change the left and right arrow behavior for right to left languages', async () => { diff --git a/test/browser/utils.tsx b/test/browser/utils.tsx index 38f0026a93..0674a62e78 100644 --- a/test/browser/utils.tsx +++ b/test/browser/utils.tsx @@ -43,7 +43,7 @@ export function getCellsAtRowIndex(rowIdx: number) { } export async function validateCellPosition(columnIdx: number, rowIdx: number) { - const cell = page.getSelectedCell(); + const cell = page.getActiveCell(); const row = page.getRow().or(page.getHeaderRow()).filter({ has: cell }); await expect.element(cell).toHaveAttribute('aria-colindex', `${columnIdx + 1}`); await expect.element(row).toHaveAttribute('aria-rowindex', `${rowIdx + 1}`); diff --git a/test/setupBrowser.ts b/test/setupBrowser.ts index 04518e1f6e..d327bd4dca 100644 --- a/test/setupBrowser.ts +++ b/test/setupBrowser.ts @@ -18,7 +18,7 @@ declare module 'vitest/browser' { getRow: (opts?: LocatorByRoleOptions) => Locator; getCell: (opts?: LocatorByRoleOptions) => Locator; getSelectAllCheckbox: () => Locator; - getSelectedCell: () => Locator; + getActiveCell: () => Locator; getDragHandle: () => Locator; getBySelector: (selector: string) => Locator; } @@ -55,7 +55,7 @@ locators.extend({ return this.getByRole('checkbox', { name: 'Select All' }); }, - getSelectedCell() { + getActiveCell() { return this.getCell({ selected: true }).or(this.getHeaderCell({ selected: true })); }, diff --git a/website/routes/AllFeatures.tsx b/website/routes/AllFeatures.tsx index 6c907a8d28..2b4116331e 100644 --- a/website/routes/AllFeatures.tsx +++ b/website/routes/AllFeatures.tsx @@ -300,7 +300,7 @@ function AllFeatures() { onCellClick={(args, event) => { if (args.column.key === 'title') { event.preventGridDefault(); - args.selectCell(true); + args.setPosition(true); } }} onCellKeyDown={(_, event) => { diff --git a/website/routes/CellNavigation.tsx b/website/routes/CellNavigation.tsx index 39e3ad550d..f2828de679 100644 --- a/website/routes/CellNavigation.tsx +++ b/website/routes/CellNavigation.tsx @@ -85,7 +85,7 @@ function CellNavigation() { function handleCellKeyDown(args: CellKeyDownArgs, event: CellKeyboardEvent) { if (args.mode === 'EDIT') return; - const { column, rowIdx, selectCell } = args; + const { column, rowIdx, setPosition } = args; const idx = column?.idx ?? -1; const { key, shiftKey } = event; @@ -96,10 +96,10 @@ function CellNavigation() { const loopOverNavigation = () => { if ((key === 'ArrowRight' || (key === 'Tab' && !shiftKey)) && idx === columns.length - 1) { - selectCell({ rowIdx, idx: 0 }); + setPosition({ rowIdx, idx: 0 }); preventDefault(); } else if ((key === 'ArrowLeft' || (key === 'Tab' && shiftKey)) && idx === 0) { - selectCell({ rowIdx, idx: columns.length - 1 }); + setPosition({ rowIdx, idx: columns.length - 1 }); preventDefault(); } }; @@ -108,15 +108,15 @@ function CellNavigation() { if (key === 'ArrowRight' && idx === columns.length - 1) { if (rows.length === 0) return; if (rowIdx === -1) { - selectCell({ rowIdx: 0, idx: 0 }); + setPosition({ rowIdx: 0, idx: 0 }); } else { if (rowIdx === rows.length - 1) return; - selectCell({ rowIdx: rowIdx + 1, idx: 0 }); + setPosition({ rowIdx: rowIdx + 1, idx: 0 }); } preventDefault(); } else if (key === 'ArrowLeft' && idx === 0) { if (rowIdx === -1) return; - selectCell({ rowIdx: rowIdx - 1, idx: columns.length - 1 }); + setPosition({ rowIdx: rowIdx - 1, idx: columns.length - 1 }); preventDefault(); } }; @@ -128,7 +128,7 @@ function CellNavigation() { } else { newRowIdx = shiftKey ? rowIdx - 1 : rowIdx === rows.length - 1 ? -1 : rowIdx + 1; } - selectCell({ rowIdx: newRowIdx, idx }); + setPosition({ rowIdx: newRowIdx, idx }); preventDefault(); };