Merge pull request #11342 from ericvoshall/catalog-a11y-fixes
Accessibility updates for the Catalog page and example App
This commit is contained in:
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Accessibility updates:
|
||||
|
||||
- Added screen reader elements to describe default table `Action` buttons
|
||||
@@ -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>
|
||||
|
||||
+32
-25
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user