Skip to content

Commit d38b21b

Browse files
Fix cell stealing focus on click (#3790)
* Add a failing test * Fix test * Fix group selection * Simplify * Update test/browser/column/renderCell.test.tsx Co-authored-by: Nicolas Stepien <[email protected]> * Update test/browser/column/renderCell.test.tsx --------- Co-authored-by: Nicolas Stepien <[email protected]>
1 parent 3026fce commit d38b21b

File tree

7 files changed

+59
-22
lines changed

7 files changed

+59
-22
lines changed

src/Cell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ function Cell<R, SR>({
4747
);
4848
const isEditable = isCellEditableUtil(column, row);
4949

50-
function selectCellWrapper(openEditor?: boolean) {
51-
selectCell({ rowIdx, idx: column.idx }, openEditor);
50+
function selectCellWrapper(enableEditor?: boolean) {
51+
selectCell({ rowIdx, idx: column.idx }, { enableEditor });
5252
}
5353

5454
function handleMouseEvent(

src/DataGrid.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import type {
5757
Position,
5858
Renderers,
5959
RowsChangeData,
60+
SelectCellOptions,
6061
SelectHeaderRowEvent,
6162
SelectRowEvent,
6263
SortColumn
@@ -109,7 +110,7 @@ export type DefaultColumnOptions<R, SR> = Pick<
109110
export interface DataGridHandle {
110111
element: HTMLDivElement | null;
111112
scrollToCell: (position: PartialPosition) => void;
112-
selectCell: (position: Position, enableEditor?: Maybe<boolean>) => void;
113+
selectCell: (position: Position, options: SelectCellOptions) => void;
113114
}
114115

115116
type SharedDivProps = Pick<
@@ -502,7 +503,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
502503
/**
503504
* callbacks
504505
*/
505-
const focusCellOrCellContent = useCallback(
506+
const focusCell = useCallback(
506507
(shouldScroll = true) => {
507508
const cell = getCellToScroll(gridRef.current!);
508509
if (cell === null) return;
@@ -511,10 +512,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
511512
scrollIntoView(cell);
512513
}
513514

514-
// Focus cell content when available instead of the cell itself
515-
const elementToFocus =
516-
cell.querySelector<Element & HTMLOrSVGElement>('[tabindex="0"]') ?? cell;
517-
elementToFocus.focus({ preventScroll: true });
515+
cell.focus({ preventScroll: true });
518516
},
519517
[gridRef]
520518
);
@@ -528,11 +526,11 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
528526
focusSinkRef.current.focus({ preventScroll: true });
529527
scrollIntoView(focusSinkRef.current);
530528
} else {
531-
focusCellOrCellContent();
529+
focusCell();
532530
}
533531
setShouldFocusCell(false);
534532
}
535-
}, [shouldFocusCell, focusCellOrCellContent, selectedPosition.idx]);
533+
}, [shouldFocusCell, focusCell, selectedPosition.idx]);
536534

537535
useImperativeHandle(ref, () => ({
538536
element: gridRef.current,
@@ -654,7 +652,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
654652
function handleFocus(event: React.FocusEvent<HTMLDivElement>) {
655653
// select the first header cell if the focus event is triggered by the grid
656654
if (event.target === event.currentTarget) {
657-
selectHeaderCell({ idx: minColIdx, rowIdx: headerRowsCount });
655+
selectHeaderCell({ idx: minColIdx, rowIdx: headerRowsCount }, { shouldFocusCell: true });
658656
}
659657
}
660658

@@ -777,7 +775,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
777775

778776
function handleDragHandleClick() {
779777
// keep the focus on the cell but do not scroll
780-
focusCellOrCellContent(false);
778+
focusCell(false);
781779
}
782780

783781
function handleDragHandleDoubleClick(event: React.MouseEvent<HTMLDivElement>) {
@@ -838,20 +836,20 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
838836
);
839837
}
840838

841-
function selectCell(position: Position, enableEditor?: Maybe<boolean>): void {
839+
function selectCell(position: Position, options?: SelectCellOptions): void {
842840
if (!isCellWithinSelectionBounds(position)) return;
843841
commitEditorChanges();
844842

845843
const samePosition = isSamePosition(selectedPosition, position);
846844

847-
if (enableEditor && isCellEditable(position)) {
845+
if (options?.enableEditor && isCellEditable(position)) {
848846
const row = rows[position.rowIdx];
849847
setSelectedPosition({ ...position, mode: 'EDIT', row, originalRow: row });
850848
} else if (samePosition) {
851849
// Avoid re-renders if the selected cell state is the same
852850
scrollIntoView(getCellToScroll(gridRef.current!));
853851
} else {
854-
setShouldFocusCell(true);
852+
setShouldFocusCell(options?.shouldFocusCell === true);
855853
setSelectedPosition({ ...position, mode: 'SELECT' });
856854
}
857855

@@ -864,8 +862,8 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
864862
}
865863
}
866864

867-
function selectHeaderCell({ idx, rowIdx }: Position) {
868-
selectCell({ rowIdx: minRowIdx + rowIdx - 1, idx });
865+
function selectHeaderCell({ idx, rowIdx }: Position, options?: SelectCellOptions): void {
866+
selectCell({ rowIdx: minRowIdx + rowIdx - 1, idx }, options);
869867
}
870868

871869
function getNextPosition(key: string, ctrlKey: boolean, shiftKey: boolean): Position {
@@ -952,7 +950,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
952950
isCellWithinBounds: isCellWithinSelectionBounds
953951
});
954952

955-
selectCell(nextSelectedCellPosition);
953+
selectCell(nextSelectedCellPosition, { shouldFocusCell: true });
956954
}
957955

958956
function getDraggedOverCellIdx(currentRowIdx: number): number | undefined {

src/GroupRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function GroupedRow<R, SR>({
4949
const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? row.level + 1 : row.level;
5050

5151
function handleSelectGroup() {
52-
selectCell({ rowIdx, idx: -1 });
52+
selectCell({ rowIdx, idx: -1 }, { shouldFocusCell: true });
5353
}
5454

5555
const selectionValue = useMemo(

src/hooks/useRovingTabIndex.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ export function useRovingTabIndex(isSelected: boolean) {
1010
}
1111

1212
function onFocus(event: React.FocusEvent<HTMLDivElement>) {
13-
if (event.target !== event.currentTarget) {
13+
const elementToFocus = event.currentTarget.querySelector<Element & HTMLOrSVGElement>(
14+
'[tabindex="0"]'
15+
);
16+
17+
// Focus cell content when available instead of the cell itself
18+
if (elementToFocus !== null) {
19+
elementToFocus.focus({ preventScroll: true });
1420
setIsChildFocused(true);
1521
}
1622
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type {
4848
RenderSummaryCellProps,
4949
RowHeightArgs,
5050
RowsChangeData,
51+
SelectCellOptions,
5152
SelectHeaderRowEvent,
5253
SelectRowEvent,
5354
SortColumn,

src/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ interface BaseCellRendererProps<TRow, TSummaryRow = unknown>
167167
'onCellMouseDown' | 'onCellClick' | 'onCellDoubleClick' | 'onCellContextMenu'
168168
> {
169169
rowIdx: number;
170-
selectCell: (position: Position, enableEditor?: Maybe<boolean>) => void;
170+
selectCell: (position: Position, options?: SelectCellOptions) => void;
171171
}
172172

173173
export interface CellRendererProps<TRow, TSummaryRow>
@@ -203,7 +203,7 @@ interface SelectCellKeyDownArgs<TRow, TSummaryRow = unknown> {
203203
column: CalculatedColumn<TRow, TSummaryRow>;
204204
row: TRow;
205205
rowIdx: number;
206-
selectCell: (position: Position, enableEditor?: Maybe<boolean>) => void;
206+
selectCell: (position: Position, options?: SelectCellOptions) => void;
207207
}
208208

209209
export interface EditCellKeyDownArgs<TRow, TSummaryRow = unknown> {
@@ -334,6 +334,11 @@ export interface Renderers<TRow, TSummaryRow> {
334334
noRowsFallback?: Maybe<ReactNode>;
335335
}
336336

337+
export interface SelectCellOptions {
338+
enableEditor?: Maybe<boolean>;
339+
shouldFocusCell?: Maybe<boolean>;
340+
}
341+
337342
export interface ColumnWidth {
338343
readonly type: 'resized' | 'measured';
339344
readonly width: number;

test/browser/column/renderCell.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,33 @@ describe('Custom cell renderer', () => {
116116
});
117117
});
118118

119+
test('Focus child if it sets tabIndex', async () => {
120+
const column: Column<Row> = {
121+
key: 'test',
122+
name: 'test',
123+
renderCell(props) {
124+
return (
125+
<>
126+
<button type="button" tabIndex={props.tabIndex}>
127+
value: {props.row.id}
128+
</button>
129+
<span>External Text</span>
130+
</>
131+
);
132+
}
133+
};
134+
135+
page.render(<DataGrid columns={[column]} rows={[{ id: 1 }]} />);
136+
137+
const button = page.getByRole('button', { name: 'value: 1' });
138+
await userEvent.click(page.getByText('External Text'));
139+
expect(button).toHaveFocus();
140+
await userEvent.tab();
141+
expect(button).not.toHaveFocus();
142+
await userEvent.click(button);
143+
expect(button).toHaveFocus();
144+
});
145+
119146
test('Cell should not steal focus when the focus is outside the grid and cell is recreated', async () => {
120147
const columns: readonly Column<Row>[] = [{ key: 'id', name: 'ID' }];
121148

0 commit comments

Comments
 (0)