Skip to content

Commit

Permalink
[DataGrid] Fix accessibility issues in panels and toolbar buttons (#8862
Browse files Browse the repository at this point in the history
)

Co-authored-by: Andreas Richter <andreas.richter2@qbeyond.de>
  • Loading branch information
romgrk and Andreas Richter authored May 25, 2023
1 parent 8c25860 commit 3d2939d
Show file tree
Hide file tree
Showing 16 changed files with 92 additions and 33 deletions.
4 changes: 2 additions & 2 deletions docs/pages/x/api/data-grid/grid-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ import { GridApi } from '@mui/x-data-grid';
| <span class="prop-name">setRowSelectionModel</span> | <span class="prop-type">(rowIds: GridRowId[]) =&gt; void</span> | Updates the selected rows to be those passed to the `rowIds` argument.<br />Any row already selected will be unselected. |
| <span class="prop-name">setSortModel</span> | <span class="prop-type">(model: GridSortModel) =&gt; void</span> | Updates the sort model and triggers the sorting of rows. |
| <span class="prop-name">showColumnMenu</span> | <span class="prop-type">(field: string) =&gt; void</span> | Display the column menu under the `field` column. |
| <span class="prop-name">showFilterPanel</span> | <span class="prop-type">(targetColumnField?: string) =&gt; void</span> | Shows the filter panel. If `targetColumnField` is given, a filter for this field is also added. |
| <span class="prop-name">showFilterPanel</span> | <span class="prop-type">(targetColumnField?: string, panelId?: string, labelId?: string) =&gt; void</span> | Shows the filter panel. If `targetColumnField` is given, a filter for this field is also added. |
| <span class="prop-name">showHeaderFilterMenu</span> | <span class="prop-type">(field: GridColDef['field']) =&gt; void</span> | Opens the header filter menu for the given field. |
| <span class="prop-name">showPreferences</span> | <span class="prop-type">(newValue: GridPreferencePanelsValue) =&gt; void</span> | Displays the preferences panel. The `newValue` argument controls the content of the panel. |
| <span class="prop-name">showPreferences</span> | <span class="prop-type">(newValue: GridPreferencePanelsValue, panelId?: string, labelId?: string) =&gt; void</span> | Displays the preferences panel. The `newValue` argument controls the content of the panel. |
| <span class="prop-name">sortColumn</span> | <span class="prop-type">(column: GridColDef, direction?: GridSortDirection, allowMultipleSorting?: boolean) =&gt; void</span> | Sorts a column. |
| <span class="prop-name">startCellEditMode</span> | <span class="prop-type">(params: GridStartCellEditModeParams) =&gt; void</span> | Puts the cell corresponding to the given row id and field into edit mode. |
| <span class="prop-name">startHeaderFilterEditMode</span> | <span class="prop-type">(field: GridColDef['field']) =&gt; void</span> | Puts the cell corresponding to the given row id and field into edit mode. |
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/x/api/data-grid/grid-filter-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
{
"name": "showFilterPanel",
"description": "Shows the filter panel. If <code>targetColumnField</code> is given, a filter for this field is also added.",
"type": "(targetColumnField?: string) => void"
"type": "(targetColumnField?: string, panelId?: string, labelId?: string) => void"
},
{
"name": "upsertFilterItem",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ const FULL_INITIAL_STATE: GridInitialState = {
preferencePanel: {
open: true,
openedPanelValue: GridPreferencePanelsValue.filters,
panelId: undefined,
labelId: undefined,
},
sorting: {
sortModel: [{ field: 'id', sort: 'desc' }],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ function GridActionsCell(props: GridActionsCellProps) {
ref={buttonRef}
id={buttonId}
aria-label={apiRef.current.getLocaleText('actionsCellMore')}
aria-controls={menuId}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
aria-haspopup="menu"
aria-expanded={open}
aria-controls={open ? menuId : undefined}
role="menuitem"
size="small"
onClick={showMenu}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export const ColumnHeaderMenuIcon = React.memo((props: ColumnHeaderMenuIconProps
aria-label={apiRef.current.getLocaleText('columnMenuLabel')}
size="small"
onClick={handleMenuIconClick}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
aria-controls={columnMenuId}
aria-haspopup="menu"
aria-expanded={open}
aria-controls={open ? columnMenuId : undefined}
id={columnMenuButtonId}
{...rootProps.slotProps?.baseIconButton}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { unstable_composeClasses as composeClasses } from '@mui/utils';
import { unstable_composeClasses as composeClasses, unstable_useId as useId } from '@mui/utils';
import Badge from '@mui/material/Badge';
import { useGridSelector } from '../../hooks';
import { gridPreferencePanelStateSelector } from '../../hooks/features/preferencesPanel/gridPreferencePanelSelector';
import { GridPreferencePanelsValue } from '../../hooks/features/preferencesPanel/gridPreferencePanelsValue';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
Expand Down Expand Up @@ -37,6 +38,9 @@ function GridColumnHeaderFilterIconButton(props: ColumnHeaderFilterIconButtonPro
const rootProps = useGridRootProps();
const ownerState = { ...props, classes: rootProps.classes };
const classes = useUtilityClasses(ownerState);
const preferencePanel = useGridSelector(apiRef, gridPreferencePanelStateSelector);
const labelId = useId();
const panelId = useId();

const toggleFilter = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
Expand All @@ -48,27 +52,33 @@ function GridColumnHeaderFilterIconButton(props: ColumnHeaderFilterIconButtonPro
if (open && openedPanelValue === GridPreferencePanelsValue.filters) {
apiRef.current.hideFilterPanel();
} else {
apiRef.current.showFilterPanel();
apiRef.current.showFilterPanel(undefined, panelId, labelId);
}

if (onClick) {
onClick(apiRef.current.getColumnHeaderParams(field), event);
}
},
[apiRef, field, onClick],
[apiRef, field, onClick, panelId, labelId],
);

if (!counter) {
return null;
}

const open = preferencePanel.open && preferencePanel.labelId === labelId;

const iconButton = (
<rootProps.slots.baseIconButton
id={labelId}
onClick={toggleFilter}
color="default"
aria-label={apiRef.current.getLocaleText('columnHeaderFiltersLabel')}
size="small"
tabIndex={-1}
aria-haspopup="menu"
aria-expanded={open}
aria-controls={open ? panelId : undefined}
{...rootProps.slotProps?.baseIconButton}
>
<rootProps.slots.columnFilteredIcon className={classes.icon} fontSize="small" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const GridPreferencesPanel = React.forwardRef<
ref={ref}
as={rootProps.slots.basePopper}
open={columns.length > 0 && preferencePanelState.open}
id={preferencePanelState.panelId}
aria-labelledby={preferencePanelState.labelId}
{...rootProps.slotProps?.panel}
{...props}
{...rootProps.slotProps?.basePopper}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { ButtonProps } from '@mui/material/Button';
import { unstable_useId as useId } from '@mui/material/utils';
import { useGridSelector } from '../../hooks/utils/useGridSelector';
import { gridPreferencePanelStateSelector } from '../../hooks/features/preferencesPanel/gridPreferencePanelSelector';
import { GridPreferencePanelsValue } from '../../hooks/features/preferencesPanel/gridPreferencePanelsValue';
Expand All @@ -9,15 +10,25 @@ import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
export const GridToolbarColumnsButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
function GridToolbarColumnsButton(props, ref) {
const { onClick, ...other } = props;
const columnButtonId = useId();
const columnPanelId = useId();

const apiRef = useGridApiContext();
const rootProps = useGridRootProps();
const { open, openedPanelValue } = useGridSelector(apiRef, gridPreferencePanelStateSelector);
const preferencePanel = useGridSelector(apiRef, gridPreferencePanelStateSelector);

const showColumns = (event: React.MouseEvent<HTMLButtonElement>) => {
if (open && openedPanelValue === GridPreferencePanelsValue.columns) {
if (
preferencePanel.open &&
preferencePanel.openedPanelValue === GridPreferencePanelsValue.columns
) {
apiRef.current.hidePreferences();
} else {
apiRef.current.showPreferences(GridPreferencePanelsValue.columns);
apiRef.current.showPreferences(
GridPreferencePanelsValue.columns,
columnPanelId,
columnButtonId,
);
}

onClick?.(event);
Expand All @@ -28,11 +39,17 @@ export const GridToolbarColumnsButton = React.forwardRef<HTMLButtonElement, Butt
return null;
}

const isOpen = preferencePanel.open && preferencePanel.panelId === columnPanelId;

return (
<rootProps.slots.baseButton
ref={ref}
id={columnButtonId}
size="small"
aria-label={apiRef.current.getLocaleText('toolbarColumnsLabel')}
aria-haspopup="menu"
aria-expanded={isOpen}
aria-controls={isOpen ? columnPanelId : undefined}
startIcon={<rootProps.slots.columnSelectorIcon />}
{...other}
onClick={showColumns}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ export const GridToolbarDensitySelector = React.forwardRef<HTMLButtonElement, Bu
size="small"
startIcon={startIcon}
aria-label={apiRef.current.getLocaleText('toolbarDensityLabel')}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="menu"
aria-controls={densityMenuId}
aria-expanded={open}
aria-controls={open ? densityMenuId : undefined}
id={densityButtonId}
{...other}
onClick={handleDensitySelectorOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export const GridToolbarExportContainer = React.forwardRef<HTMLButtonElement, Bu

const apiRef = useGridApiContext();
const rootProps = useGridRootProps();
const buttonId = useId();
const menuId = useId();
const exportButtonId = useId();
const exportMenuId = useId();

const [open, setOpen] = React.useState(false);
const buttonRef = React.useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -58,11 +58,11 @@ export const GridToolbarExportContainer = React.forwardRef<HTMLButtonElement, Bu
ref={handleRef}
size="small"
startIcon={<rootProps.slots.exportIcon />}
aria-expanded={open ? 'true' : undefined}
aria-expanded={open}
aria-label={apiRef.current.getLocaleText('toolbarExportLabel')}
aria-haspopup="menu"
aria-labelledby={menuId}
id={buttonId}
aria-controls={open ? exportMenuId : undefined}
id={exportButtonId}
{...other}
onClick={handleMenuOpen}
{...rootProps.slotProps?.baseButton}
Expand All @@ -76,9 +76,9 @@ export const GridToolbarExportContainer = React.forwardRef<HTMLButtonElement, Bu
position="bottom-start"
>
<MenuList
id={menuId}
id={exportMenuId}
className={gridClasses.menuList}
aria-labelledby={buttonId}
aria-labelledby={exportButtonId}
onKeyDown={handleListKeyDown}
autoFocusItem={open}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { styled } from '@mui/material/styles';
import {
unstable_composeClasses as composeClasses,
unstable_capitalize as capitalize,
unstable_useId as useId,
} from '@mui/utils';
import Badge from '@mui/material/Badge';
import { ButtonProps } from '@mui/material/Button';
Expand Down Expand Up @@ -35,7 +36,7 @@ const useUtilityClasses = (ownerState: OwnerState) => {
const GridToolbarFilterListRoot = styled('ul', {
name: 'MuiDataGrid',
slot: 'ToolbarFilterList',
overridesResolver: (props, styles) => styles.toolbarFilterList,
overridesResolver: (_props, styles) => styles.toolbarFilterList,
})<{ ownerState: OwnerState }>(({ theme }) => ({
margin: theme.spacing(1, 1, 0.5),
padding: theme.spacing(0, 1),
Expand All @@ -60,6 +61,8 @@ const GridToolbarFilterButton = React.forwardRef<HTMLButtonElement, GridToolbarF
const lookup = useGridSelector(apiRef, gridColumnLookupSelector);
const preferencePanel = useGridSelector(apiRef, gridPreferencePanelStateSelector);
const classes = useUtilityClasses(rootProps);
const filterButtonId = useId();
const filterPanelId = useId();

const tooltipContentNode = React.useMemo(() => {
if (preferencePanel.open) {
Expand Down Expand Up @@ -108,9 +111,13 @@ const GridToolbarFilterButton = React.forwardRef<HTMLButtonElement, GridToolbarF
const toggleFilter = (event: React.MouseEvent<HTMLButtonElement>) => {
const { open, openedPanelValue } = preferencePanel;
if (open && openedPanelValue === GridPreferencePanelsValue.filters) {
apiRef.current.hideFilterPanel();
apiRef.current.hidePreferences();
} else {
apiRef.current.showFilterPanel();
apiRef.current.showPreferences(
GridPreferencePanelsValue.filters,
filterPanelId,
filterButtonId,
);
}
buttonProps.onClick?.(event);
};
Expand All @@ -120,6 +127,7 @@ const GridToolbarFilterButton = React.forwardRef<HTMLButtonElement, GridToolbarF
return null;
}

const isOpen = preferencePanel.open && preferencePanel.panelId === filterPanelId;
return (
<rootProps.slots.baseTooltip
title={tooltipContentNode}
Expand All @@ -129,8 +137,12 @@ const GridToolbarFilterButton = React.forwardRef<HTMLButtonElement, GridToolbarF
>
<rootProps.slots.baseButton
ref={ref}
id={filterButtonId}
size="small"
aria-label={apiRef.current.getLocaleText('toolbarFiltersLabel')}
aria-controls={isOpen ? filterPanelId : undefined}
aria-expanded={isOpen}
aria-haspopup
startIcon={
<Badge badgeContent={activeFilters.length} color="primary">
<rootProps.slots.openFilterButtonIcon />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const useGridFilter = (
);

const showFilterPanel = React.useCallback<GridFilterApi['showFilterPanel']>(
(targetColumnField) => {
(targetColumnField, panelId, labelId) => {
logger.debug('Displaying filter panel');
if (targetColumnField) {
const filterModel = gridFilterModelSelector(apiRef);
Expand Down Expand Up @@ -226,7 +226,7 @@ export const useGridFilter = (
items: newFilterItems,
});
}
apiRef.current.showPreferences(GridPreferencePanelsValue.filters);
apiRef.current.showPreferences(GridPreferencePanelsValue.filters, panelId, labelId);
},
[apiRef, logger, props.disableMultipleColumnsFiltering],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { GridPreferencePanelsValue } from './gridPreferencePanelsValue';

export interface GridPreferencePanelState {
open: boolean;
panelId?: string;
labelId?: string;
/**
* Tab currently opened.
* @default GridPreferencePanelsValue.filter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ export const useGridPreferencesPanel = (
}, [hidePreferences]);

const showPreferences = React.useCallback<GridPreferencesPanelApi['showPreferences']>(
(newValue) => {
(newValue, panelId, labelId) => {
logger.debug('Opening Preferences Panel');
doNotHidePanel();
apiRef.current.setState((state) => ({
...state,
preferencePanel: { ...state.preferencePanel, open: true, openedPanelValue: newValue },
preferencePanel: {
...state.preferencePanel,
open: true,
openedPanelValue: newValue,
panelId,
labelId,
},
}));
apiRef.current.publishEvent('preferencePanelOpen', {
openedPanelValue: newValue,
Expand Down
Loading

0 comments on commit 3d2939d

Please sign in to comment.