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

[docs] Add a11y section to Tabs #20965

Merged
merged 4 commits into from
May 12, 2020
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
102 changes: 102 additions & 0 deletions docs/src/pages/components/tabs/AccessibleTabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';

function TabPanel(props) {
Copy link
Member

@oliviertassinari oliviertassinari May 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it use the lab API instead? I will help to gain more feedback.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean more towards this now, yes. For a11y attributes on the stable API we can refer to the previous demos.

const { children, value, index, ...other } = props;

return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box p={3}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}

TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};

function DemoTabs(props) {
const { labelId, onChange, selectionFollowsFocus, value } = props;

return (
<AppBar position="static">
<Tabs
aria-labelledby={labelId}
onChange={onChange}
selectionFollowsFocus={selectionFollowsFocus}
value={value}
>
<Tab label="Item One" aria-controls="simple-tabpanel-0" id="simple-tab-0" />
<Tab label="Item Two" aria-controls="simple-tabpanel-1" id="simple-tab-1" />
<Tab label="Item Three" aria-controls="simple-tabpanel-2" id="simple-tab-2" />
</Tabs>
</AppBar>
);
}

DemoTabs.propTypes = {
labelId: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
selectionFollowsFocus: PropTypes.bool,
value: PropTypes.number.isRequired,
};

const useStyles = makeStyles({
root: {
flexGrow: 1,
},
});

export default function AccessibleTabs() {
const classes = useStyles();

const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};

return (
<div className={classes.root}>
<Typography id="demo-a11y-tabs-automatic-label">
Tabs where selection follows focus
</Typography>
<DemoTabs
labelId="demo-a11y-tabs-automatic-label"
selectionFollowsFocus
onChange={handleChange}
value={value}
/>
<Typography id="demo-a11y-tabs-manual-label">
Tabs where each tab needs to be selected manually
</Typography>
<DemoTabs labelId="demo-a11y-tabs-manual-label" onChange={handleChange} value={value} />
<TabPanel value={value} index={0}>
Item One
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
</div>
);
}
100 changes: 100 additions & 0 deletions docs/src/pages/components/tabs/AccessibleTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';

interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}

function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;

return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box p={3}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}

interface DemoTabsProps {
labelId: string;
onChange: (event: unknown, value: number) => void;
selectionFollowsFocus?: boolean;
value: number;
}
function DemoTabs(props: DemoTabsProps) {
const { labelId, onChange, selectionFollowsFocus, value } = props;

return (
<AppBar position="static">
<Tabs
aria-labelledby={labelId}
onChange={onChange}
selectionFollowsFocus={selectionFollowsFocus}
value={value}
>
<Tab label="Item One" aria-controls="simple-tabpanel-0" id="simple-tab-0" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Tab label="Item Two" aria-controls="simple-tabpanel-1" id="simple-tab-1" />
<Tab label="Item Three" aria-controls="simple-tabpanel-2" id="simple-tab-2" />
</Tabs>
</AppBar>
);
}

const useStyles = makeStyles({
root: {
flexGrow: 1,
},
});

export default function AccessibleTabs() {
const classes = useStyles();

const [value, setValue] = React.useState(0);
const handleChange = (event: unknown, newValue: number) => {
setValue(newValue);
};

return (
<div className={classes.root}>
<Typography id="demo-a11y-tabs-automatic-label">
Tabs where selection follows focus
</Typography>
<DemoTabs
labelId="demo-a11y-tabs-automatic-label"
selectionFollowsFocus
onChange={handleChange}
value={value}
/>
<Typography id="demo-a11y-tabs-manual-label">
Tabs where each tab needs to be selected manually
</Typography>
<DemoTabs labelId="demo-a11y-tabs-manual-label" onChange={handleChange} value={value} />
<TabPanel value={value} index={0}>
Item One
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
</div>
);
}
27 changes: 26 additions & 1 deletion docs/src/pages/components/tabs/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,32 @@ Tab labels may be either all icons or all text.

{{"demo": "pages/components/tabs/IconLabelTabs.js", "bg": true}}

## Experimental Tabs API
## Accessibility

(WAI-ARIA: https://www.w3.org/TR/wai-aria-practices/#tabpanel)

The following steps are needed in order to provide necessary information for assistive technologies:

1. Label `Tabs` via `aria-label` or `aria-labelledby`.
2. `Tab`s need to be connected to their
corresponding `[role="tabpanel"]` by setting the correct `id`, `aria-controls` and `aria-labelledby`.

An example for the current implementation can be found in the demos on this page. We've also published [an experimental API](#experimental-api) in `@material-ui/lab` that does not require
extra work.

### Keyboard navigation

The components implement keyboard navigation using the "manual activation" behavior. If you want to switch to the
"selection automatically follows focus" behavior you have pass `selectionFollowsFocus` to the `Tabs` component. The WAI-ARIA authoring practices have a detailed guide on [how to decide when to make selection automatically follow focus](https://www.w3.org/TR/wai-aria-practices/#kbd_selection_follows_focus).

#### `selectionFollowsFocus` Demo

The following two demos only differ in their keyboard navigation behavior.
Focus a tab and navigate with arrow keys to notice the difference.

{{"demo": "pages/components/tabs/AccessibleTabs.js", "bg": true}}

## Experimental API

`@material-ui/lab` offers utility components that inject props to implement accessible tabs
following [WAI-ARIA authoring practices](https://www.w3.org/TR/wai-aria-practices/#tabpanel).
Expand Down