Merge pull request #11342 from ericvoshall/catalog-a11y-fixes

Accessibility updates for the Catalog page and example App
This commit is contained in:
Patrik Oldsberg
2022-05-19 11:48:17 +02:00
committed by GitHub
15 changed files with 193 additions and 96 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog-react': patch
---
Accessibility updates:
- Wrapped the `EntityLifecyclePicker`, `EntityOwnerPicker`, `EntityTagPicker`, in `label` elements
- Changed group name `Typography` component to `span` (from default `h6`), added `aria-label` to the `List` component, and `role` of `menuitem` to the container of the `MenuItem` component
+30
View File
@@ -0,0 +1,30 @@
---
'@backstage/create-app': patch
---
Accessibility updates:
- Added `aria-label` to the sidebar Logo link. To enable this for an existing app, please make the following changes:
`packages/app/src/components/Root/Root.tsx`
```diff
const SidebarLogo = () => {
const classes = useSidebarLogoStyles();
const { isOpen } = useContext(SidebarContext);
return (
<div className={classes.root}>
<Link
component={NavLink}
to="/"
underline="none"
className={classes.link}
+ aria-label="Home"
>
{isOpen ? <LogoFull /> : <LogoIcon />}
</Link>
</div>
);
};
```
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog': patch
---
Accessibility updates:
- Added screen reader elements to describe default table `Action` buttons
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/core-components': patch
---
Accessibility updates:
- Added `aria-label` to the `Select` component
- Changed heading level used in the header of `Table` component
@@ -77,6 +77,7 @@ const SidebarLogo = () => {
to="/"
underline="none"
className={classes.link}
aria-label="Home"
>
{isOpen ? <LogoFull /> : <LogoIcon />}
</Link>
@@ -199,6 +199,7 @@ export function SelectComponent(props: SelectProps) {
<FormControl className={classes.formControl}>
<InputLabel className={classes.formLabel}>{label}</InputLabel>
<Select
aria-label={label}
value={value}
native={native}
disabled={disabled}
@@ -499,7 +499,7 @@ export function Table<T extends object = {}>(props: TableProps<T>) {
icons={tableIcons}
title={
<>
<Typography variant="h5" component="h3">
<Typography variant="h5" component="h2">
{title}
</Typography>
{subtitle && (
@@ -69,6 +69,7 @@ const SidebarLogo = () => {
to="/"
underline="none"
className={classes.link}
aria-label="Home"
>
{isOpen ? <LogoFull /> : <LogoIcon />}
</Link>
@@ -99,31 +99,38 @@ export const EntityLifecyclePicker = () => {
return (
<Box pb={1} pt={1}>
<Typography variant="button">Lifecycle</Typography>
<Autocomplete
aria-label="Lifecycle"
multiple
options={availableLifecycles}
value={selectedLifecycles}
onChange={(_: object, value: string[]) => setSelectedLifecycles(value)}
renderOption={(option, { selected }) => (
<FormControlLabel
control={
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
}
label={option}
/>
)}
size="small"
popupIcon={<ExpandMoreIcon data-testid="lifecycle-picker-expand" />}
renderInput={params => (
<TextField {...params} className={classes.input} variant="outlined" />
)}
/>
<Typography variant="button" component="label">
Lifecycle
<Autocomplete
multiple
options={availableLifecycles}
value={selectedLifecycles}
onChange={(_: object, value: string[]) =>
setSelectedLifecycles(value)
}
renderOption={(option, { selected }) => (
<FormControlLabel
control={
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
}
label={option}
/>
)}
size="small"
popupIcon={<ExpandMoreIcon data-testid="lifecycle-picker-expand" />}
renderInput={params => (
<TextField
{...params}
className={classes.input}
variant="outlined"
/>
)}
/>
</Typography>
</Box>
);
};
@@ -103,31 +103,36 @@ export const EntityOwnerPicker = () => {
return (
<Box pb={1} pt={1}>
<Typography variant="button">Owner</Typography>
<Autocomplete
multiple
aria-label="Owner"
options={availableOwners}
value={selectedOwners}
onChange={(_: object, value: string[]) => setSelectedOwners(value)}
renderOption={(option, { selected }) => (
<FormControlLabel
control={
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
}
label={option}
/>
)}
size="small"
popupIcon={<ExpandMoreIcon data-testid="owner-picker-expand" />}
renderInput={params => (
<TextField {...params} className={classes.input} variant="outlined" />
)}
/>
<Typography variant="button" component="label">
Owner
<Autocomplete
multiple
options={availableOwners}
value={selectedOwners}
onChange={(_: object, value: string[]) => setSelectedOwners(value)}
renderOption={(option, { selected }) => (
<FormControlLabel
control={
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
}
label={option}
/>
)}
size="small"
popupIcon={<ExpandMoreIcon data-testid="owner-picker-expand" />}
renderInput={params => (
<TextField
{...params}
className={classes.input}
variant="outlined"
/>
)}
/>
</Typography>
</Box>
);
};
@@ -95,31 +95,36 @@ export const EntityTagPicker = () => {
return (
<Box pb={1} pt={1}>
<Typography variant="button">Tags</Typography>
<Autocomplete
multiple
aria-label="Tags"
options={availableTags}
value={selectedTags}
onChange={(_: object, value: string[]) => setSelectedTags(value)}
renderOption={(option, { selected }) => (
<FormControlLabel
control={
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
}
label={option}
/>
)}
size="small"
popupIcon={<ExpandMoreIcon data-testid="tag-picker-expand" />}
renderInput={params => (
<TextField {...params} className={classes.input} variant="outlined" />
)}
/>
<Typography variant="button" component="label">
Tags
<Autocomplete
multiple
options={availableTags}
value={selectedTags}
onChange={(_: object, value: string[]) => setSelectedTags(value)}
renderOption={(option, { selected }) => (
<FormControlLabel
control={
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
}
label={option}
/>
)}
size="small"
popupIcon={<ExpandMoreIcon data-testid="tag-picker-expand" />}
renderInput={params => (
<TextField
{...params}
className={classes.input}
variant="outlined"
/>
)}
/>
</Typography>
</Box>
);
};
@@ -167,7 +167,7 @@ describe('<UserListPicker />', () => {
expect(
getAllByRole('menuitem').map(({ textContent }) => textContent),
).toEqual(['Owned', 'Starred', 'All']);
).toEqual(['Owned 1', 'Starred 1', 'All 4']);
});
it('includes counts alongside each filter', async () => {
@@ -183,10 +183,8 @@ describe('<UserListPicker />', () => {
// menuitem itself, so we pick off the next sibling.
await waitFor(() => {
expect(
getAllByRole('menuitem').map(
({ nextSibling }) => nextSibling?.textContent,
),
).toEqual(['1', '1', '4']);
getAllByRole('menuitem').map(({ textContent }) => textContent),
).toEqual(['Owned 1', 'Starred 1', 'All 4']);
});
});
@@ -206,10 +204,8 @@ describe('<UserListPicker />', () => {
await waitFor(() => {
expect(
getAllByRole('menuitem').map(
({ nextSibling }) => nextSibling?.textContent,
),
).toEqual(['1', '0', '2']);
getAllByRole('menuitem').map(({ textContent }) => textContent),
).toEqual(['Owned 1', 'Starred 0', 'All 2']);
});
});
@@ -238,13 +238,18 @@ export const UserListPicker = (props: UserListPickerProps) => {
<Card className={classes.root}>
{filterGroups.map(group => (
<Fragment key={group.name}>
<Typography variant="subtitle2" className={classes.title}>
<Typography
variant="subtitle2"
component="span"
className={classes.title}
>
{group.name}
</Typography>
<Card className={classes.groupWrapper}>
<List disablePadding dense role="menu">
<List disablePadding dense role="menu" aria-label={group.name}>
{group.items.map(item => (
<MenuItem
role="none presentation"
key={item.id}
button
divider
@@ -254,6 +259,7 @@ export const UserListPicker = (props: UserListPickerProps) => {
disabled={filterCounts[item.id] === 0}
data-testid={`user-picker-${item.id}`}
tabIndex={0}
ContainerProps={{ role: 'menuitem' }}
>
{item.icon && (
<ListItemIcon className={classes.listIcon}>
@@ -261,7 +267,7 @@ export const UserListPicker = (props: UserListPickerProps) => {
</ListItemIcon>
)}
<ListItemText>
<Typography variant="body1">{item.label}</Typography>
<Typography variant="body1">{item.label} </Typography>
</ListItemText>
<ListItemSecondaryAction>
{filterCounts[item.id] ?? '-'}
@@ -41,6 +41,7 @@ import {
import StarBorder from '@material-ui/icons/StarBorder';
import { withStyles } from '@material-ui/core/styles';
import Star from '@material-ui/icons/Star';
import { Typography } from '@material-ui/core';
/**
* Props for {@link CatalogTable}.
@@ -115,9 +116,16 @@ export const CatalogTable = (props: CatalogTableProps) => {
const defaultActions: TableProps<CatalogTableRow>['actions'] = [
({ entity }) => {
const url = entity.metadata.annotations?.[ANNOTATION_VIEW_URL];
const title = 'View';
return {
icon: () => <OpenInNew aria-label="View" fontSize="small" />,
tooltip: 'View',
icon: () => (
<>
<Typography variant="srOnly">{title}</Typography>
<OpenInNew fontSize="small" />
</>
),
tooltip: title,
disabled: !url,
onClick: () => {
if (!url) return;
@@ -127,9 +135,16 @@ export const CatalogTable = (props: CatalogTableProps) => {
},
({ entity }) => {
const url = entity.metadata.annotations?.[ANNOTATION_EDIT_URL];
const title = 'Edit';
return {
icon: () => <Edit aria-label="Edit" fontSize="small" />,
tooltip: 'Edit',
icon: () => (
<>
<Typography variant="srOnly">{title}</Typography>
<Edit fontSize="small" />
</>
),
tooltip: title,
disabled: !url,
onClick: () => {
if (!url) return;
@@ -139,10 +154,17 @@ export const CatalogTable = (props: CatalogTableProps) => {
},
({ entity }) => {
const isStarred = isStarredEntity(entity);
const title = isStarred ? 'Remove from favorites' : 'Add to favorites';
return {
cellStyle: { paddingLeft: '1em' },
icon: () => (isStarred ? <YellowStar /> : <StarBorder />),
tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites',
icon: () => (
<>
<Typography variant="srOnly">{title}</Typography>
{isStarred ? <YellowStar /> : <StarBorder />}
</>
),
tooltip: title,
onClick: () => toggleStarredEntity(entity),
};
},
@@ -88,8 +88,8 @@ describe('TemplateListPage', () => {
{ mountedRoutes: { '/': rootRouteRef } },
);
expect(getByRole('menuitem', { name: 'All' })).toBeInTheDocument();
expect(getByRole('menuitem', { name: 'Starred' })).toBeInTheDocument();
expect(getByRole('menuitem', { name: /All/ })).toBeInTheDocument();
expect(getByRole('menuitem', { name: /Starred/ })).toBeInTheDocument();
});
it('should render the category picker', async () => {