Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataGrid] Fix keyboard navigation for actions cell with disabled buttons #10947

Merged
merged 1 commit into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading