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

Added keyboard accessibility in DataGrid component #53

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
### Fixed

### Changed
- Added keyboard accessibility to the header in `DataGrid` component
- Added focus to the input label in `TextField` and `SelectMultiple` components
- Fixed the width of the `Tile` action menu and corrected the focus of nested-level `Accordion` component

### Breaking changes

Expand Down
1 change: 0 additions & 1 deletion src/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ const StyledAccordion = styled(MuiAccordion)<AccordionPropsAll>((props) => {
...(hasNested ? { padding: '8px 0px 8px 8px' } : { padding: '8px 8px 8px 8px' }),
},
'&.MuiAccordion-root': {
overflow: 'hidden',
'&:focus': {
boxShadow: `0 0 0 2px ${theme.palette.primary.main}`,
zIndex: 1,
Expand Down
38 changes: 29 additions & 9 deletions src/DataGrid/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ const StyledDataGrid = styled(MuiDataGrid)<DataGridProps>((props) => {
marginTop: '0!important',
},
},
...(props.hideFooter) && {
'& .MuiDataGrid-virtualScroller': {
borderBottom: `1px ${theme.palette.border.secondary} solid`,
},
},
...(props.totalCount <= 0) && {
'& .MuiDataGrid-footerContainer': {
borderTop: 'none',
Expand Down Expand Up @@ -430,6 +435,7 @@ const DataGrid = ({ components, componentsProps, ...props }: DataGridProps) => {
const rowCheckbox: HTMLElement | undefined = findTargetElement(target, 'PrivateSwitchBase-input', false);
// find column header title element that contains column title and sorting icon as we are supporting click for whole space of title and icon in header title
const columnHeaderTitle: HTMLElement | undefined = findTargetElement(target, 'MuiDataGrid-columnHeaderTitle', false);
const columnHeader: HTMLElement | undefined = findTargetElement(target, 'MuiDataGrid-columnHeader', false);
// this is for us to navigate to the first row of the table body
if (target && (event.key === 'Tab' || event.key === 'ArrowDown')) {
// to find the first row in order to focus
Expand All @@ -454,26 +460,37 @@ const DataGrid = ({ components, componentsProps, ...props }: DataGridProps) => {
if (rowCheckbox) {
rowCheckbox.focus();
}
if (!rowCheckbox && columnHeader) {
columnHeader.focus();
}
}
};

// this function handles when user use keydown on datagrid it will change focus from select all checkbox to column header row
const handleOnBaseCheckFocus = (event: React.FocusEvent) => {
/**
* Handles the focus event on the header of the DataGrid.
* @param event - The keyboard event triggered when the header is focused.
* This function prevents the default focus behavior, finds the column header row,
* and sets it to be focusable by adding a `tabindex` attribute. It then focuses
* on the column header row and adds a keydown event listener for keyboard navigation.
* Additionally, it removes the focus from the row when the header is focused.
*/
const handleOnHeaderFocus = (event: KeyboardEvent) => {
event.preventDefault();
// need to get coloumn header row to so that we can focus on it.
const parentElem = findTargetElement(event.target, 'MuiDataGrid-columnHeaders', true);
if (parentElem && !parentElem.hasAttribute('tabindex')) {
const parentElem = findTargetElement(event.target, 'MuiDataGrid-root', true);
const columnHeaderRow = parentElem?.querySelector('.MuiDataGrid-columnHeaders') as HTMLDivElement;
if (columnHeaderRow && !columnHeaderRow.hasAttribute('tabindex')) {
// add tabindex so that we can are able to focus on it.
parentElem.setAttribute('tabindex', '0');
columnHeaderRow.setAttribute('tabindex', '0');
// need some wait to set attribute to take effect
window.setTimeout(() => {
parentElem.focus();
columnHeaderRow.focus();
}, 0);
// add keydown event to coloumn header row for some keyboard navigation
parentElem.addEventListener('keydown', (e) => {
columnHeaderRow.addEventListener('keydown', (e) => {
return handleOnColumnHeaderRowKeyDown(e);
});
parentElem.addEventListener('focus', () => {
columnHeaderRow.addEventListener('focus', () => {
setFocusRow(''); // remove focus on the row when we focus on the header
}, { once: true });
}
Expand Down Expand Up @@ -639,10 +656,13 @@ const DataGrid = ({ components, componentsProps, ...props }: DataGridProps) => {
baseCheckbox: {
'data-testid': DataGridTestIds.DATAGRID_CHECKBOX,
onClick: onCheckboxClick,
onFocus: handleOnBaseCheckFocus,
onKeyDown: handleOnCheckboxKeydown,
},
},
header: {
tabIndex: 0,
onFocus: handleOnHeaderFocus,
},
row: {
tabIndex: 0,
onKeyDown: handleOnRowKeyDown,
Expand Down
2 changes: 1 addition & 1 deletion src/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const getMuiTablePaginationThemeOverrides = (): Components<Omit<Theme, 'c
display: 'none', // hides the form action label space from Autocomplete
},
'> .autocomplete-container': {
margin: '0 12px 0 4px',
margin: '0 4px',
},
},
'div[data-testid=tablePaginationActionsPageDiv]': {
Expand Down
2 changes: 0 additions & 2 deletions src/Select/SelectMultiple.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import React from 'react';
import { StoryFn, Meta } from '@storybook/react';
import OutlinedInput from '@mui/material/OutlinedInput/OutlinedInput';
import { userEvent, within } from '@storybook/testing-library';

import Select, { SelectChangeEvent } from './Select';
Expand Down Expand Up @@ -128,7 +127,6 @@ const Template: StoryFn<typeof Select> = (args) => {
{...args}
value={values}
onChange={handleChange}
input={<OutlinedInput />}
renderValue={(selected) => {
if ((selected as string[]).length === 0) {
return <em>{args.placeholder}</em>;
Expand Down
39 changes: 31 additions & 8 deletions src/TextField/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@mui/material';
import { unstable_useId as useId } from '@mui/utils';

import { styled } from '@mui/material/styles';
import Typography from '../Typography';
import InputLabelAndAction, { InputLabelAndActionProps, ActionProps } from '../prerequisite_components/InputLabelAndAction/InputLabelAndAction';
import { ThemeDirectionType } from '../theme';
Expand Down Expand Up @@ -264,6 +265,14 @@ export const getMuiTextFieldThemeOverrides = (): Components<Omit<Theme, 'compone
};
};

const TextFieldContainer = styled('div')((theme) => {
return {
'.MuiAutocomplete--label--focused': {
color: theme.theme.palette.primary.main,
},
};
});

const getEndAdornment = (props: TextFieldProps, isComboBox: boolean) => {
// This is workaround until proper Search component has already been implemented
// This hides the endAdornment when startAdornment is present and it's a simple Textfield (NOT affecting Autocomplete / Multiselect)
Expand All @@ -282,7 +291,7 @@ const getEndAdornment = (props: TextFieldProps, isComboBox: boolean) => {
);
};

const getInputLabelAndActionProps = (props: TextFieldProps): InputLabelAndActionProps => {
const getInputLabelAndActionProps = (props: TextFieldProps, isFocus: boolean): InputLabelAndActionProps => {
const inputLabelId = props.label && props.id ? `${props.id}-label` : undefined;
const inputLabelProps: InputLabelAndActionProps = {
color: props.color,
Expand All @@ -297,6 +306,7 @@ const getInputLabelAndActionProps = (props: TextFieldProps): InputLabelAndAction
actionProps: props.actionProps,
hiddenLabel: props.hiddenLabel,
fullWidth: props.fullWidth,
isFocus,
};
return inputLabelProps;
};
Expand Down Expand Up @@ -348,7 +358,7 @@ const renderNonEditInput = (props: TextFieldProps, muiTextFieldProps: OutlinedTe
return <Typography variant="body2">{muiTextFieldProps.value ? muiTextFieldProps.value : null}</Typography>;
};

const renderInput = (props: TextFieldProps) => {
const renderInput = (props: TextFieldProps, setIsFocus: React.Dispatch<React.SetStateAction<boolean>>) => {
const muiTextFieldProps = getMuiTextFieldProps(props);
const helperTextId = props.helperText && props.id ? `${props.id}-helper-text` : undefined;
if (props.nonEdit) {
Expand All @@ -359,21 +369,34 @@ const renderInput = (props: TextFieldProps) => {
</>
);
}
return <MuiTextField {...muiTextFieldProps} />;
return (
<MuiTextField
{...muiTextFieldProps}
onFocus={() => {
setIsFocus(true);
}}
onBlur={() => {
setIsFocus(false);
}}
/>
);
};

const TextField = React.forwardRef(({ ...props }: TextFieldProps, forwardRef: React.ForwardedRef<unknown>) => {
const muiInputLabelProps = getInputLabelAndActionProps(props);
const [isFocus, setIsFocus] = React.useState(false);
const muiInputLabelProps = getInputLabelAndActionProps(props, isFocus);
if (!props.id) {
const id = useId();
props.id = id;
}
const muiFormControlProps = getMuiFormControlProps(props, forwardRef);
return (
<MuiFormControl {...muiFormControlProps}>
<InputLabelAndAction {...muiInputLabelProps} />
{renderInput(props)}
</MuiFormControl>
<TextFieldContainer>
<MuiFormControl {...muiFormControlProps}>
<InputLabelAndAction {...muiInputLabelProps} />
{renderInput(props, setIsFocus)}
</MuiFormControl>
</TextFieldContainer>
);
}) as React.FC<TextFieldProps>;

Expand Down
2 changes: 1 addition & 1 deletion src/composite_components/Tile/TileActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const TileActionMenu: React.FC<ITileActionMenuProps> = (props: ITileActionMenuPr
</IconButton>
</Tooltip>
<Menu
PaperProps={{ sx: { width: '240px', padding: '0px' } }}
PaperProps={{ sx: { padding: '0px' } }}
id="basic-menu"
data-testid={TileActionTestIds.TILE_ACTION_MENU}
anchorEl={anchorEl}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,17 +214,16 @@ const renderInputLabelAndAction = (props: InputLabelAndActionProps) => {
>
{ limitedActionProps && limitedActionProps.map((actionProp, index) => {
return (
<Tooltip title={actionProp.tooltip} placement="bottom">
// eslint-why index is not the sole key definition, it is prefixed by other identifiers
// eslint-disable-next-line react/no-array-index-key
<Tooltip title={actionProp.tooltip} placement="bottom" key={`${actionProp.label}-${index}`}>
<StyledSpan>
<MuiInputActionLink
disabled={actionProp.disabled || props.disabled}
href={actionProp.href}
onClick={actionProp.handleClick}
underline="none"
sx={{ display: 'inline' }}
// eslint-why index is not the sole key definition, it is prefixed by other identifiers
// eslint-disable-next-line react/no-array-index-key
key={`${actionProp.label}-${index}`}
>
{actionProp.label}
</MuiInputActionLink>
Expand Down