diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 8c760ceb81..ebaeb25d86 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -1,11 +1,4 @@ -import { - useCallback, - useImperativeHandle, - useLayoutEffect, - useMemo, - useRef, - useState -} from 'react'; +import { useCallback, useImperativeHandle, useLayoutEffect, useMemo, useState } from 'react'; import type { Key, KeyboardEvent } from 'react'; import { flushSync } from 'react-dom'; @@ -76,13 +69,7 @@ import type { PartialPosition } from './ScrollToCell'; import ScrollToCell from './ScrollToCell'; import { default as defaultRenderSortStatus } from './sortStatus'; import { cellDragHandleClassname, cellDragHandleFrozenClassname } from './style/cell'; -import { - focusSinkClassname, - focusSinkHeaderAndSummaryClassname, - rootClassname, - viewportDraggingClassname -} from './style/core'; -import { rowSelected, rowSelectedWithFrozenCell } from './style/row'; +import { rootClassname, viewportDraggingClassname } from './style/core'; import SummaryRow from './SummaryRow'; export interface SelectCellState extends Position { @@ -386,11 +373,6 @@ export function DataGrid(props: DataGridPr (): SelectCellState | EditCellState => ({ idx: -1, rowIdx: minRowIdx - 1, mode: 'SELECT' }) ); - /** - * refs - */ - const focusSinkRef = useRef(null); - /** * computed values */ @@ -499,37 +481,19 @@ export function DataGrid(props: DataGridPr const selectCellLatest = useLatestFunc(selectCell); const selectHeaderCellLatest = useLatestFunc(selectHeaderCell); - /** - * callbacks - */ - const focusCell = useCallback( - (shouldScroll = true) => { - const cell = getCellToScroll(gridRef.current!); - if (cell === null) return; - - if (shouldScroll) { - scrollIntoView(cell); - } - - cell.focus({ preventScroll: true }); - }, - [gridRef] - ); - /** * effects */ useLayoutEffect(() => { if (shouldFocusCell) { - if (focusSinkRef.current !== null && selectedPosition.idx === -1) { - focusSinkRef.current.focus({ preventScroll: true }); - scrollIntoView(focusSinkRef.current); + if (selectedPosition.idx === -1) { + focusRow(gridRef.current!); } else { - focusCell(); + focusCell(gridRef.current!); } setShouldFocusCell(false); } - }, [shouldFocusCell, focusCell, selectedPosition.idx]); + }, [shouldFocusCell, selectedPosition.idx, gridRef]); useImperativeHandle( ref, @@ -632,9 +596,13 @@ export function DataGrid(props: DataGridPr if (cellEvent.isGridDefaultPrevented()) return; } - if (!(event.target instanceof Element)) return; - const isCellEvent = event.target.closest('.rdg-cell') !== null; - const isRowEvent = isTreeGrid && event.target === focusSinkRef.current; + const { target } = event; + + if (!(target instanceof Element)) return; + + const isCellEvent = target.closest('.rdg-cell') !== null; + const isRowEvent = isTreeGrid && target.role === 'row'; + if (!isCellEvent && !isRowEvent) return; switch (event.key) { @@ -734,7 +702,7 @@ export function DataGrid(props: DataGridPr function handleDragHandlePointerDown(event: React.PointerEvent) { // keep the focus on the cell event.preventDefault(); - if (event.pointerType === 'mouse' && event.buttons !== 1) { + if (event.pointerType === 'mouse' && event.button !== 0) { return; } setDragging(true); @@ -774,7 +742,7 @@ export function DataGrid(props: DataGridPr function handleDragHandleClick() { // keep the focus on the cell but do not scroll - focusCell(false); + focusCell(gridRef.current!, false); } function handleDragHandleDoubleClick(event: React.MouseEvent) { @@ -870,20 +838,32 @@ export function DataGrid(props: DataGridPr const isRowSelected = selectedCellIsWithinSelectionBounds && idx === -1; switch (key) { - case 'ArrowUp': - return { idx, rowIdx: rowIdx - 1 }; + case 'ArrowUp': { + const nextRowIdx = rowIdx - 1; + return { + // avoid selecting header rows + idx: idx === -1 && nextRowIdx < -topSummaryRowsCount ? 0 : idx, + rowIdx: nextRowIdx + }; + } case 'ArrowDown': return { idx, rowIdx: rowIdx + 1 }; - case leftKey: - return { idx: idx - 1, rowIdx }; + case leftKey: { + const nextIdx = idx - 1; + return { + // avoid selecting header rows + idx: rowIdx < -topSummaryRowsCount && nextIdx < 0 ? 0 : nextIdx, + rowIdx + }; + } case rightKey: return { idx: idx + 1, rowIdx }; case 'Tab': return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; case 'Home': - // If row is selected then move focus to the first row - if (isRowSelected) return { idx, rowIdx: minRowIdx }; - return { idx: 0, rowIdx: ctrlKey ? minRowIdx : rowIdx }; + // If row is selected then move focus to the first header row's cell. + if (isRowSelected || 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 }; @@ -1149,7 +1129,8 @@ export function DataGrid(props: DataGridPr lastFrozenColumnIndex, onRowChange: handleFormatterRowChangeLatest, selectCell: selectCellLatest, - selectedCellEditor: getCellEditor(rowIdx) + selectedCellEditor: getCellEditor(rowIdx), + isTreeGrid }) ); } @@ -1179,9 +1160,6 @@ export function DataGrid(props: DataGridPr templateRows += ` repeat(${bottomSummaryRowsCount}, ${summaryRowHeight}px)`; } - const isGroupRowFocused = - selectedPosition.idx === -1 && selectedPosition.rowIdx !== minRowIdx - 1; - return (
(props: DataGridPr )} style={{ ...style, - // set scrollPadding to correctly position non-sticky cells after scrolling - scrollPaddingInlineStart: - selectedPosition.idx > lastFrozenColumnIndex || scrollToPosition?.idx !== undefined - ? `${totalFrozenColumnWidth}px` - : undefined, - scrollPaddingBlock: - isRowIdxWithinViewportBounds(selectedPosition.rowIdx) || - scrollToPosition?.rowIdx !== undefined - ? `${headerRowsHeight + topSummaryRowsCount * summaryRowHeight}px ${ - bottomSummaryRowsCount * summaryRowHeight - }px` - : undefined, + // set scrollPadding to correctly scroll to non-sticky cells/rows + scrollPaddingInlineStart: totalFrozenColumnWidth, + scrollPaddingBlockStart: headerRowsHeight + topSummaryRowsCount * summaryRowHeight, + scrollPaddingBlockEnd: bottomSummaryRowsCount * summaryRowHeight, gridTemplateColumns, gridTemplateRows: templateRows, '--rdg-header-row-height': `${headerRowHeight}px`, @@ -1289,6 +1259,7 @@ export function DataGrid(props: DataGridPr selectedCellIdx={isSummaryRowSelected ? selectedPosition.idx : undefined} isTop selectCell={selectCellLatest} + isTreeGrid={isTreeGrid} /> ); })} @@ -1322,6 +1293,7 @@ export function DataGrid(props: DataGridPr selectedCellIdx={isSummaryRowSelected ? selectedPosition.idx : undefined} isTop={false} selectCell={selectCellLatest} + isTreeGrid={isTreeGrid} /> ); })} @@ -1334,24 +1306,6 @@ export function DataGrid(props: DataGridPr {/* render empty cells that span only 1 column so we can safely measure column widths, regardless of colSpan */} {renderMeasuringCells(viewportColumns)} - {/* extra div is needed for row navigation in a treegrid */} - {isTreeGrid && ( -
- )} - {scrollToPosition !== null && ( (props: DataGridPr ); } +function getRowToScroll(gridEl: HTMLDivElement) { + return gridEl.querySelector(':scope > [role="row"][tabindex="0"]'); +} + function getCellToScroll(gridEl: HTMLDivElement) { return gridEl.querySelector(':scope > [role="row"] > [tabindex="0"]'); } @@ -1370,3 +1328,21 @@ function getCellToScroll(gridEl: HTMLDivElement) { function isSamePosition(p1: Position, p2: Position) { return p1.idx === p2.idx && p1.rowIdx === p2.rowIdx; } + +function focusElement(element: HTMLDivElement | null, shouldScroll: boolean) { + if (element === null) return; + + if (shouldScroll) { + scrollIntoView(element); + } + + element.focus({ preventScroll: true }); +} + +function focusRow(gridEl: HTMLDivElement) { + focusElement(getRowToScroll(gridEl), true); +} + +function focusCell(gridEl: HTMLDivElement, shouldScroll = true) { + focusElement(getCellToScroll(gridEl), shouldScroll); +} diff --git a/src/GroupCell.tsx b/src/GroupCell.tsx index 95db0fab34..41a2bbf5d3 100644 --- a/src/GroupCell.tsx +++ b/src/GroupCell.tsx @@ -40,20 +40,19 @@ function GroupCell({ return (
{ - // prevents clicking on the cell from stealing focus from focusSink - event.preventDefault(); - }} onClick={isLevelMatching ? toggleGroup : undefined} onFocus={onFocus} > diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index e5c59fd895..7c2c453c6b 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -2,7 +2,7 @@ import { memo, useMemo } from 'react'; import { css } from 'ecij'; import { RowSelectionContext, type RowSelectionContextValue } from './hooks'; -import { classnames, getRowStyle } from './utils'; +import { classnames } from './utils'; import type { BaseRenderRowProps, GroupRow } from './types'; import { SELECT_COLUMN_KEY } from './Columns'; import GroupCell from './GroupCell'; @@ -44,6 +44,7 @@ function GroupedRow({ 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; @@ -64,15 +65,16 @@ function GroupedRow({ aria-setsize={row.setSize} aria-posinset={row.posInSet + 1} // aria-posinset is 1-based aria-expanded={row.isExpanded} + tabIndex={isPositionOnRow ? 0 : -1} className={classnames( rowClassname, groupRowClassname, `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, - selectedCellIdx === -1 && rowSelectedClassname, + isPositionOnRow && rowSelectedClassname, className )} onMouseDown={handleSelectGroup} - style={getRowStyle(gridRowStart)} + style={{ gridRowStart }} {...props} > {viewportColumns.map((column) => ( diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index 8da9ed3e90..40333f76ac 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -45,21 +45,15 @@ const resizeHandleClassname = `rdg-resize-handle ${resizeHandle}`; const cellDraggableClassname = 'rdg-cell-draggable'; -const cellDragging = css` +const cellDraggingOrOver = css` @layer rdg.HeaderCell { background-color: var(--rdg-header-draggable-background-color); } `; -const cellDraggingClassname = `rdg-cell-dragging ${cellDragging}`; +const cellDraggingClassname = `rdg-cell-dragging ${cellDraggingOrOver}`; -const cellOver = css` - @layer rdg.HeaderCell { - background-color: var(--rdg-header-draggable-background-color); - } -`; - -const cellOverClassname = `rdg-cell-drag-over ${cellOver}`; +const cellOverClassname = `rdg-cell-drag-over ${cellDraggingOrOver}`; const dragImageClassname = css` @layer rdg.HeaderCell { @@ -336,7 +330,7 @@ function ResizeHandle({ const isRtl = direction === 'rtl'; function onPointerDown(event: React.PointerEvent) { - if (event.pointerType === 'mouse' && event.buttons !== 1) { + if (event.pointerType === 'mouse' && event.button !== 0) { return; } diff --git a/src/HeaderRow.tsx b/src/HeaderRow.tsx index dea30c9fe9..3c9fe6217a 100644 --- a/src/HeaderRow.tsx +++ b/src/HeaderRow.tsx @@ -62,6 +62,7 @@ function HeaderRow({ direction }: HeaderRowProps) { const [draggedColumnKey, setDraggedColumnKey] = useState(); + const isPositionOnRow = selectedCellIdx === -1; const cells = []; for (let index = 0; index < columns.length; index++) { @@ -98,9 +99,7 @@ function HeaderRow({ aria-rowindex={rowIdx} // aria-rowindex is 1 based className={classnames( headerRowClassname, - { - [rowSelectedClassname]: selectedCellIdx === -1 - }, + isPositionOnRow && rowSelectedClassname, headerRowClass )} > diff --git a/src/Row.tsx b/src/Row.tsx index e758c107b6..d38ea2bc5d 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -1,7 +1,7 @@ import { memo, useMemo } from 'react'; import { RowSelectionContext, useLatestFunc, type RowSelectionContextValue } from './hooks'; -import { classnames, getColSpan, getRowStyle } from './utils'; +import { classnames, getColSpan } from './utils'; import type { CalculatedColumn, RenderRowProps } from './types'; import { useDefaultRenderers } from './DataGridDefaultRenderersContext'; import { rowClassname, rowSelectedClassname } from './style/row'; @@ -18,6 +18,7 @@ function Row({ row, viewportColumns, selectedCellEditor, + isTreeGrid, onCellMouseDown, onCellClick, onCellDoubleClick, @@ -34,12 +35,12 @@ function Row({ onRowChange(column, rowIdx, newRow); }); + const isPositionOnRow = selectedCellIdx === -1; + className = classnames( rowClassname, `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, - { - [rowSelectedClassname]: selectedCellIdx === -1 - }, + isPositionOnRow && rowSelectedClassname, rowClass?.(row, rowIdx), className ); @@ -87,9 +88,10 @@ function Row({
= Pick< CellRendererProps, 'rowIdx' | 'column' | 'colSpan' | 'isCellSelected' | 'selectCell' @@ -33,7 +25,6 @@ function SummaryCell({ const { summaryCellClass } = column; const className = getCellClassname( column, - summaryCellClassname, typeof summaryCellClass === 'function' ? summaryCellClass(row) : summaryCellClass ); diff --git a/src/SummaryRow.tsx b/src/SummaryRow.tsx index 3e66c2c41b..f400fdf315 100644 --- a/src/SummaryRow.tsx +++ b/src/SummaryRow.tsx @@ -1,9 +1,8 @@ import { memo } from 'react'; import { css } from 'ecij'; -import { classnames, getColSpan, getRowStyle } from './utils'; +import { classnames, getColSpan } from './utils'; import type { RenderRowProps } from './types'; -import { cell, cellFrozen } from './style/cell'; import { bottomSummaryRowClassname, rowClassname, @@ -14,7 +13,7 @@ import SummaryCell from './SummaryCell'; type SharedRenderRowProps = Pick< RenderRowProps, - 'viewportColumns' | 'rowIdx' | 'gridRowStart' | 'selectCell' + 'viewportColumns' | 'rowIdx' | 'gridRowStart' | 'selectCell' | 'isTreeGrid' >; interface SummaryRowProps extends SharedRenderRowProps { @@ -29,21 +28,8 @@ interface SummaryRowProps extends SharedRenderRowProps { const summaryRow = css` @layer rdg.SummaryRow { - > .${cell} { - position: sticky; - } - } -`; - -const topSummaryRow = css` - @layer rdg.SummaryRow { - > .${cell} { - z-index: 2; - } - - > .${cellFrozen} { - z-index: 3; - } + position: sticky; + z-index: 2; } `; @@ -60,9 +46,13 @@ function SummaryRow({ selectedCellIdx, isTop, selectCell, + isTreeGrid, 'aria-rowindex': ariaRowIndex }: SummaryRowProps) { + const isPositionOnRow = selectedCellIdx === -1; + const cells = []; + for (let index = 0; index < viewportColumns.length; index++) { const column = viewportColumns[index]; const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'SUMMARY', row }); @@ -89,20 +79,18 @@ function SummaryRow({
{cells} diff --git a/src/TreeDataGrid.tsx b/src/TreeDataGrid.tsx index 5194d770a8..9f6845cad5 100644 --- a/src/TreeDataGrid.tsx +++ b/src/TreeDataGrid.tsx @@ -381,6 +381,7 @@ export function TreeDataGrid({ lastFrozenColumnIndex, draggedOverCellIdx, selectedCellEditor, + isTreeGrid, ...rowProps }: RenderRowProps ) { @@ -418,7 +419,8 @@ export function TreeDataGrid({ onRowChange, lastFrozenColumnIndex, draggedOverCellIdx, - selectedCellEditor + selectedCellEditor, + isTreeGrid }); } diff --git a/src/style/cell.ts b/src/style/cell.ts index 207d468f56..f3d4f3ce1e 100644 --- a/src/style/cell.ts +++ b/src/style/cell.ts @@ -13,7 +13,6 @@ export const cell = css` padding-inline: 8px; border-inline-end: var(--rdg-border-width) solid var(--rdg-border-color); border-block-end: var(--rdg-border-width) solid var(--rdg-border-color); - grid-row-start: var(--rdg-grid-row-start); align-content: center; background-color: inherit; diff --git a/src/style/core.ts b/src/style/core.ts index a2bbf2022b..0f1be75f2f 100644 --- a/src/style/core.ts +++ b/src/style/core.ts @@ -62,8 +62,7 @@ const root = css` /* needed on Firefox to fix scrollbars */ &::before { content: ''; - grid-column: 1/-1; - grid-row: 1/-1; + grid-area: -2 / -2 / -1 / -1; } > :nth-last-child(1 of .${topSummaryRowClassname}) { @@ -93,19 +92,3 @@ const viewportDragging = css` `; export const viewportDraggingClassname = `rdg-viewport-dragging ${viewportDragging}`; - -export const focusSinkClassname = css` - @layer rdg.FocusSink { - grid-column: 1/-1; - pointer-events: none; - /* Should have a higher value than 1 to show up above regular frozen cells */ - z-index: 1; - } -`; - -export const focusSinkHeaderAndSummaryClassname = css` - @layer rdg.FocusSink { - /* Should have a higher value than 3 to show up above header and summary rows */ - z-index: 3; - } -`; diff --git a/src/style/layers.css b/src/style/layers.css index d7059d54aa..0630460d1f 100644 --- a/src/style/layers.css +++ b/src/style/layers.css @@ -1,6 +1,5 @@ @layer rdg { @layer Defaults, - FocusSink, CheckboxInput, CheckboxIcon, CheckboxLabel, diff --git a/src/style/row.ts b/src/style/row.ts index 7c7a2687b4..7eaf1a43b0 100644 --- a/src/style/row.ts +++ b/src/style/row.ts @@ -1,14 +1,42 @@ import { css } from 'ecij'; +import { cellFrozen } from './cell'; + export const row = css` @layer rdg.Row { - display: contents; + display: grid; + grid-column: 1/-1; + grid-template: subgrid / subgrid; background-color: var(--rdg-background-color); &:hover { background-color: var(--rdg-row-hover-background-color); } + &:focus { + outline: none; + } + + &[tabindex='0'] { + /* we render the outline in a pseudo element as otherwise cells render above it */ + &::after { + content: ''; + grid-column: 1 / -1; + z-index: 1; + pointer-events: none; + border: var(--rdg-selection-width) solid var(--rdg-selection-color); + } + + & > .${cellFrozen}:first-child::before { + content: ''; + display: inline-block; + position: absolute; + inset-block: 0; + inset-inline-start: 0; + border-inline-start: var(--rdg-selection-width) solid var(--rdg-selection-color); + } + } + &[aria-selected='true'] { background-color: var(--rdg-row-selected-background-color); @@ -21,28 +49,8 @@ export const row = css` export const rowClassname = `rdg-row ${row}`; -export const rowSelected = css` - @layer rdg.FocusSink { - outline: 2px solid var(--rdg-selection-color); - outline-offset: -2px; - } -`; - export const rowSelectedClassname = 'rdg-row-selected'; -export const rowSelectedWithFrozenCell = css` - @layer rdg.FocusSink { - &::before { - content: ''; - display: inline-block; - block-size: 100%; - position: sticky; - inset-inline-start: 0; - border-inline-start: 2px solid var(--rdg-selection-color); - } - } -`; - export const topSummaryRowClassname = 'rdg-top-summary-row'; export const bottomSummaryRowClassname = 'rdg-bottom-summary-row'; diff --git a/src/types.ts b/src/types.ts index f635ed2d0e..53d4f4b019 100644 --- a/src/types.ts +++ b/src/types.ts @@ -254,6 +254,7 @@ export interface RenderRowProps extends BaseRenderR selectedCellEditor: ReactElement> | undefined; onRowChange: (column: CalculatedColumn, rowIdx: number, newRow: TRow) => void; rowClass: Maybe<(row: TRow, rowIdx: number) => Maybe>; + isTreeGrid: boolean; } export interface RowsChangeData { diff --git a/src/utils/styleUtils.ts b/src/utils/styleUtils.ts index 3de29e06e0..6b97ac54c2 100644 --- a/src/utils/styleUtils.ts +++ b/src/utils/styleUtils.ts @@ -1,12 +1,6 @@ -import type { CSSProperties } from 'react'; - import type { CalculatedColumn, CalculatedColumnOrColumnGroup, Maybe } from '../types'; import { cellClassname, cellFrozenClassname } from '../style/cell'; -export function getRowStyle(rowIdx: number): CSSProperties { - return { '--rdg-grid-row-start': rowIdx }; -} - export function getHeaderCellStyle( column: CalculatedColumnOrColumnGroup, rowIdx: number, diff --git a/test/browser/TreeDataGrid.test.tsx b/test/browser/TreeDataGrid.test.tsx index 575e6f1f47..b141c6dff6 100644 --- a/test/browser/TreeDataGrid.test.tsx +++ b/test/browser/TreeDataGrid.test.tsx @@ -3,18 +3,16 @@ import { page, userEvent } from 'vitest/browser'; import type { Column } from '../../src'; import { renderTextEditor, SelectColumn, TreeDataGrid } from '../../src'; -import { focusSinkClassname } from '../../src/style/core'; -import { rowSelected } from '../../src/style/row'; +import { rowSelectedClassname } from '../../src/style/row'; import { getCellsAtRowIndex, getRowWithCell, testCount, testRowCount } from './utils'; const treeGrid = page.getTreeGrid(); const headerRow = treeGrid.getHeaderRow(); -const headerCells = treeGrid.getHeaderCell(); +const headerCells = headerRow.getHeaderCell(); +const headerCheckbox = headerRow.getSelectAllCheckbox(); const rows = treeGrid.getRow(); const selectedCell = treeGrid.getSelectedCell(); -const rowSelectedClassname = 'rdg-row-selected'; - interface Row { id: number; country: string; @@ -263,7 +261,6 @@ test('should set aria-attributes', async () => { test('should select rows in a group', async () => { await setup(['year', 'country']); - const headerCheckbox = page.getSelectAllCheckbox(); await expect.element(headerCheckbox).not.toBeChecked(); // expand group @@ -315,42 +312,61 @@ test('should select rows in a group', async () => { test('cell navigation in a treegrid', async () => { await setup(['country', 'year']); await testRowCount(4); - const focusSink = page.getBySelector(`.${focusSinkClassname}`); + + const topSummaryRow = rows.nth(0); + const row1 = rows.nth(1); + const row3 = rows.nth(3); // expand group - const groupCell1 = page.getCell({ name: 'USA' }); + const groupCell1 = row1.getCell({ name: 'USA' }); await expect.element(document.body).toHaveFocus(); - await expect.element(focusSink).toHaveAttribute('tabIndex', '-1'); + await expect.element(row1).toHaveAttribute('tabIndex', '-1'); + await expect.element(row1).not.toHaveClass(rowSelectedClassname); + await userEvent.click(groupCell1); - await expect.element(focusSink).toHaveFocus(); - await expect.element(focusSink).toHaveAttribute('tabIndex', '0'); - await expect.element(focusSink).toHaveStyle('grid-row-start:3'); - await expect.element(focusSink).toHaveClass(rowSelected); + await expect.element(row1).toHaveFocus(); + await expect.element(row1).toHaveAttribute('tabIndex', '0'); + await expect.element(row1).toHaveClass(rowSelectedClassname); + await userEvent.keyboard('{arrowup}'); - await expect.element(focusSink).toHaveFocus(); - await expect.element(focusSink).toHaveStyle('grid-row-start:2'); - await expect.element(focusSink).toHaveClass(rowSelected); + await expect.element(topSummaryRow).toHaveFocus(); + await expect.element(topSummaryRow).toHaveAttribute('tabIndex', '0'); + await expect.element(topSummaryRow).toHaveClass(rowSelectedClassname); + + // header row does not get selected await userEvent.keyboard('{arrowup}'); - await expect.element(focusSink).toHaveFocus(); - await expect.element(focusSink).toHaveStyle('grid-row-start:1'); - await expect.element(focusSink).toHaveClass(rowSelected); - await expect.element(focusSink).toHaveFocus(); - await expect.element(focusSink).toHaveStyle('grid-row-start:1'); - await expect.element(focusSink).toHaveClass(rowSelected); + await expect.element(headerCheckbox).toHaveFocus(); + await expect.element(headerCheckbox).toHaveAttribute('tabIndex', '0'); + await expect.element(headerRow).not.toHaveClass(rowSelectedClassname); + + // header row cannot get selected + await userEvent.keyboard('{arrowleft}'); + await expect.element(headerCheckbox).toHaveFocus(); + await expect.element(headerCheckbox).toHaveAttribute('tabIndex', '0'); + await expect.element(headerRow).not.toHaveClass(rowSelectedClassname); + await userEvent.keyboard('{arrowdown}'); - await expect.element(focusSink).toHaveFocus(); - await expect.element(focusSink).toHaveStyle('grid-row-start:2'); - await expect.element(focusSink).toHaveClass(rowSelected); + 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); + + // can select summary row + await userEvent.keyboard('{arrowleft}'); + await expect.element(topSummaryRow).toHaveFocus(); + await expect.element(topSummaryRow).toHaveAttribute('tabIndex', '0'); + await expect.element(topSummaryRow).toHaveClass(rowSelectedClassname); + const groupCell2 = page.getCell({ name: '2021' }); await userEvent.click(groupCell2); - await expect.element(focusSink).toHaveFocus(); - await expect.element(focusSink).toHaveAttribute('tabIndex', '0'); + await expect.element(row3).toHaveFocus(); + await expect.element(row3).toHaveAttribute('tabIndex', '0'); // select cell const cells = getCellsAtRowIndex(5); await userEvent.click(cells.nth(1)); await expect.element(cells.nth(1)).toHaveAttribute('aria-selected', 'true'); - await expect.element(focusSink).toHaveAttribute('tabIndex', '-1'); + await expect.element(cells.nth(1)).toHaveFocus(); + await expect.element(cells.nth(1)).toHaveAttribute('tabIndex', '0'); // select the previous cell await userEvent.keyboard('{arrowleft}'); @@ -361,7 +377,7 @@ test('cell navigation in a treegrid', async () => { 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(focusSink).toHaveFocus(); + await expect.element(rows.nth(4)).toHaveFocus(); // if the row is selected then arrowright should select the first cell on the same row await userEvent.keyboard('{arrowright}'); @@ -388,10 +404,12 @@ test('cell navigation in a treegrid', async () => { await expect.element(rows.nth(5)).toHaveClass(rowSelectedClassname); await userEvent.keyboard('{home}'); - await expect.element(headerRow).toHaveClass(rowSelectedClassname); + await expect.element(headerCheckbox).toHaveFocus(); + await expect.element(headerCheckbox).toHaveAttribute('tabIndex', '0'); + await expect.element(headerRow).not.toHaveClass(rowSelectedClassname); // collpase parent group - await userEvent.keyboard('{arrowdown}{arrowdown}{arrowleft}'); + await userEvent.keyboard('{arrowdown}{arrowdown}{arrowleft}{arrowleft}'); await expect.element(page.getCell({ name: '2021' })).not.toBeInTheDocument(); await testRowCount(4); }); diff --git a/test/browser/column/summaryCellClass.test.ts b/test/browser/column/summaryCellClass.test.ts index 6586e48350..35734cf12e 100644 --- a/test/browser/column/summaryCellClass.test.ts +++ b/test/browser/column/summaryCellClass.test.ts @@ -1,8 +1,7 @@ import { page } from 'vitest/browser'; import type { Column } from '../../../src'; -import { cellClassname as cellClass } from '../../../src/style/cell'; -import { summaryCellClassname } from '../../../src/SummaryCell'; +import { cellClassname } from '../../../src/style/cell'; import { setup } from '../utils'; const cells = page.getCell(); @@ -11,7 +10,6 @@ interface SummaryRow { id: number; } -const cellClassname = `${cellClass} ${summaryCellClassname}`; const topSummaryRows: readonly SummaryRow[] = [{ id: 0 }, { id: 1 }]; const bottomSummaryRows: readonly SummaryRow[] = [{ id: 2 }, { id: 3 }]; diff --git a/test/browser/utils.tsx b/test/browser/utils.tsx index af5f8d709e..38f0026a93 100644 --- a/test/browser/utils.tsx +++ b/test/browser/utils.tsx @@ -50,7 +50,7 @@ export async function validateCellPosition(columnIdx: number, rowIdx: number) { } export async function scrollGrid(options: ScrollToOptions) { - page.getGrid().element().scrollTo(options); + page.getGrid().element().scroll(options); // let the browser fire the 'scroll' event await new Promise(requestAnimationFrame); } diff --git a/test/browser/virtualization.test.ts b/test/browser/virtualization.test.ts index 2c86542547..70529a94c4 100644 --- a/test/browser/virtualization.test.ts +++ b/test/browser/virtualization.test.ts @@ -209,7 +209,7 @@ test('zero columns', async () => { await expect.element(headerCells).toHaveLength(0); await expect.element(cells).toHaveLength(0); - await expect.element(rows).toHaveLength(0); + await expect.element(rows).toHaveLength(34); }); test('zero rows', async () => {