Skip to content

Commit

Permalink
[docs] Add a11y section to Tabs (#20965)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored May 12, 2020
1 parent 5dfb13c commit 3bc1e2d
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 1 deletion.
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) {
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" />
<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

0 comments on commit 3bc1e2d

Please sign in to comment.