Skip to content

Commit

Permalink
[DataGrid] Fix keyboard navigation for actions cell with disabled but…
Browse files Browse the repository at this point in the history
…tons (#10947)
  • Loading branch information
michelengelen authored Nov 8, 2023
1 parent efa968b commit a711a0e
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 16 deletions.
33 changes: 21 additions & 12 deletions packages/grid/x-data-grid/src/components/cell/GridActionsCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ function GridActionsCell(props: GridActionsCellProps) {
focus() {
// If ignoreCallToFocus is true, then one of the buttons was clicked and the focus is already set
if (!ignoreCallToFocus.current) {
setFocusedButtonIndex(0);
// find the first focusable button and pass the index to the state
const focusableButtonIndex = options.findIndex((o) => !o.props.disabled);
setFocusedButtonIndex(focusableButtonIndex);
}
},
}),
[],
[options],
);

React.useEffect(() => {
Expand Down Expand Up @@ -141,19 +143,26 @@ function GridActionsCell(props: GridActionsCellProps) {
return;
}

const getNewIndex = (index: number, direction: 'left' | 'right'): number => {
if (index < 0 || index > options.length) {
return index;
}

// for rtl mode we need to reverse the direction
const rtlMod = theme.direction === 'rtl' ? -1 : 1;
const indexMod = (direction === 'left' ? -1 : 1) * rtlMod;

// if the button that should receive focus is disabled go one more step
return options[index + indexMod]?.props.disabled
? getNewIndex(index + indexMod, direction)
: index + indexMod;
};

let newIndex: number = focusedButtonIndex;
if (event.key === 'ArrowRight') {
if (theme.direction === 'rtl') {
newIndex -= 1;
} else {
newIndex += 1;
}
newIndex = getNewIndex(focusedButtonIndex, 'right');
} else if (event.key === 'ArrowLeft') {
if (theme.direction === 'rtl') {
newIndex += 1;
} else {
newIndex -= 1;
}
newIndex = getNewIndex(focusedButtonIndex, 'left');
}

if (newIndex < 0 || newIndex >= numberOfButtons) {
Expand Down
11 changes: 8 additions & 3 deletions packages/grid/x-data-grid/src/components/cell/GridCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
GridRowId,
GridCellMode,
GridEditCellProps,
GridActionsColDef,
} from '../../models';
import {
GridRenderEditCellParams,
Expand Down Expand Up @@ -585,9 +586,13 @@ const GridCellV7 = React.forwardRef<HTMLDivElement, GridCellV7Props>((props, ref

const { cellMode, hasFocus, isEditable, value, formattedValue } = cellParamsWithAPI;

const managesOwnFocus = column.type === 'actions';
const canManageOwnFocus =
column.type === 'actions' &&
(column as GridActionsColDef)
.getActions?.(apiRef.current.getRowParams(rowId))
.some((action) => !action.props.disabled);
const tabIndex =
(cellMode === 'view' || !isEditable) && !managesOwnFocus ? cellParamsWithAPI.tabIndex : -1;
(cellMode === 'view' || !isEditable) && !canManageOwnFocus ? cellParamsWithAPI.tabIndex : -1;

const { classes: rootClasses, getCellClassName } = rootProps;

Expand Down Expand Up @@ -772,7 +777,7 @@ const GridCellV7 = React.forwardRef<HTMLDivElement, GridCellV7Props>((props, ref
);
}

if (React.isValidElement(children) && managesOwnFocus) {
if (React.isValidElement(children) && canManageOwnFocus) {
children = React.cloneElement<any>(children, { focusElementRef });
}

Expand Down
110 changes: 109 additions & 1 deletion packages/grid/x-data-grid/src/tests/keyboard.DataGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
getColumnValues,
getRow,
} from 'test/utils/helperFn';
import { DataGrid, DataGridProps, GridColDef } from '@mui/x-data-grid';
import { DataGrid, DataGridProps, GridActionsCellItem, GridColDef } from '@mui/x-data-grid';
import { useBasicDemoData, getBasicGridData } from '@mui/x-data-grid-generator';
import RestoreIcon from '@mui/icons-material/Restore';

const isJSDOM = /jsdom/.test(window.navigator.userAgent);

Expand Down Expand Up @@ -716,6 +717,113 @@ describe('<DataGrid /> - Keyboard', () => {
expect(virtualScroller.scrollLeft).to.equal(0);
});

it('should focus actions cell with one disabled item', () => {
const columns = [
{
field: 'actions',
type: 'actions',
getActions: () => [
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_1'} disabled />,
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_2'} />,
],
},
{ field: 'id', width: 400 },
{ field: 'name' },
];
const rows = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];

render(
<div style={{ width: 300, height: 300 }}>
<DataGrid rows={rows} columns={columns} />
</div>,
);

const cell = getCell(0, 1);
userEvent.mousePress(cell);

fireEvent.keyDown(cell, { key: 'ArrowLeft' });
expect(getActiveCell()).to.equal(`0-0`);

// expect the only focusable button to be the active element
expect(document.activeElement?.id).to.equal('action_2');
});

it('should focus actions cell with all items disabled', () => {
const columns = [
{
field: 'actions',
type: 'actions',
getActions: () => [
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_1'} disabled />,
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_2'} disabled />,
],
},
{ field: 'id', width: 400 },
{ field: 'name' },
];
const rows = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];

render(
<div style={{ width: 300, height: 300 }}>
<DataGrid rows={rows} columns={columns} />
</div>,
);

const cell = getCell(0, 1);
userEvent.mousePress(cell);

fireEvent.keyDown(cell, { key: 'ArrowLeft' });
expect(getActiveCell()).to.equal(`0-0`);
});

it('should be able to navigate the actions', () => {
const columns = [
{
field: 'actions',
type: 'actions',
getActions: () => [
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_1'} disabled />,
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_2'} />,
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_3'} disabled />,
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_4'} disabled />,
<GridActionsCellItem label="Test" icon={<RestoreIcon />} id={'action_5'} />,
],
},
{ field: 'id', width: 400 },
{ field: 'name' },
];
const rows = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];

render(
<div style={{ width: 300, height: 300 }}>
<DataGrid rows={rows} columns={columns} />
</div>,
);

const cell = getCell(0, 1);
userEvent.mousePress(cell);

fireEvent.keyDown(cell, { key: 'ArrowLeft' });
expect(getActiveCell()).to.equal(`0-0`);

// expect the only focusable button to be the active element
expect(document.activeElement?.id).to.equal('action_2');

fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' });

// expect the only focusable button to be the active element
expect(document.activeElement?.id).to.equal('action_5');
});

it('should not throw when moving into an empty grid', async () => {
const columns = [{ field: 'id', width: 400 }, { field: 'name' }];
const rows = [] as any[];
Expand Down

0 comments on commit a711a0e

Please sign in to comment.