-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[charts] Introduce keyboard navigation #19155
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
Changes from all commits
d6dd42e
3cb42a7
83acf89
b54144d
db812d7
a0848e1
e30dee1
780c2a0
41d8af8
34c5e57
50fb051
8173991
0683667
24590ec
ae974da
8639c34
df1096e
302d411
cf1dcce
ec366fe
a62690a
4117d87
8e7536a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import * as React from 'react'; | ||
import Stack from '@mui/material/Stack'; | ||
import Button from '@mui/material/Button'; | ||
import Select from '@mui/material/Select'; | ||
import InputLabel from '@mui/material/InputLabel'; | ||
import FormControl from '@mui/material/FormControl'; | ||
import MenuItem from '@mui/material/MenuItem'; | ||
import { ScatterChart } from '@mui/x-charts/ScatterChart'; | ||
import { BarChart } from '@mui/x-charts/BarChart'; | ||
import { LineChart } from '@mui/x-charts/LineChart'; | ||
import { PieChart } from '@mui/x-charts/PieChart'; | ||
import { data } from './randomData'; | ||
|
||
const scatterSeries = [ | ||
{ | ||
label: 'Series A', | ||
data: data.map((v) => ({ x: v.x1, y: v.y1, id: v.id })), | ||
}, | ||
{ | ||
label: 'Series B', | ||
data: data.map((v) => ({ x: v.x1, y: v.y2, id: v.id })), | ||
}, | ||
]; | ||
|
||
const series = [ | ||
{ label: 'Series A', data: data.map((p) => p.y1) }, | ||
{ label: 'Series B', data: data.map((p) => p.y2) }, | ||
]; | ||
|
||
export default function KeyboardNavigation() { | ||
const [chartType, setChartType] = React.useState('line'); | ||
const svgRef = React.useRef(null); | ||
|
||
const handleChange = (event) => setChartType(event.target.value); | ||
|
||
return ( | ||
<Stack width="100%" sx={{ display: 'block' }}> | ||
<Stack | ||
width="100%" | ||
direction="row" | ||
gap={2} | ||
justifyContent="center" | ||
sx={{ mb: 1 }} | ||
> | ||
<FormControl sx={{ minWidth: 200 }}> | ||
<InputLabel id="chart-type-label">Chart Type</InputLabel> | ||
<Select | ||
labelId="chart-type-label" | ||
id="chart-type-select" | ||
value={chartType} | ||
label="Chart Type" | ||
onChange={handleChange} | ||
> | ||
<MenuItem value="scatter">Scatter</MenuItem> | ||
<MenuItem value="line">Line</MenuItem> | ||
<MenuItem value="bar">Bar</MenuItem> | ||
<MenuItem value="pie">Pie</MenuItem> | ||
</Select> | ||
</FormControl> | ||
<Button onClick={() => svgRef.current?.focus()} variant="contained"> | ||
Focus chart | ||
</Button> | ||
</Stack> | ||
<Chart key={chartType} svgRef={svgRef} type={chartType} /> | ||
</Stack> | ||
); | ||
} | ||
|
||
function Chart({ svgRef, type }) { | ||
switch (type) { | ||
case 'scatter': | ||
return ( | ||
<ScatterChart | ||
ref={svgRef} | ||
enableKeyboardNavigation | ||
height={300} | ||
series={scatterSeries} | ||
/> | ||
); | ||
|
||
case 'line': | ||
return ( | ||
<LineChart | ||
ref={svgRef} | ||
enableKeyboardNavigation | ||
height={300} | ||
xAxis={[{ data: data.map((p) => p.x1).toSorted((a, b) => a - b) }]} | ||
series={series} | ||
/> | ||
); | ||
|
||
case 'bar': | ||
return ( | ||
<BarChart | ||
ref={svgRef} | ||
enableKeyboardNavigation | ||
height={300} | ||
xAxis={[ | ||
{ data: data.map((p) => Math.round(p.x1)).toSorted((a, b) => a - b) }, | ||
]} | ||
series={series} | ||
/> | ||
); | ||
|
||
case 'pie': | ||
return ( | ||
<PieChart | ||
ref={svgRef} | ||
enableKeyboardNavigation | ||
series={[ | ||
{ | ||
arcLabel: 'value', | ||
data: [ | ||
{ id: 0, value: 10, label: 'series A' }, | ||
{ id: 1, value: 15, label: 'series B' }, | ||
{ id: 2, value: 20, label: 'series C' }, | ||
], | ||
}, | ||
]} | ||
height={300} | ||
hideLegend={false} | ||
/> | ||
); | ||
|
||
default: | ||
throw new Error(`Unknown chart type: ${type}`); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import * as React from 'react'; | ||
import Stack from '@mui/material/Stack'; | ||
import Button from '@mui/material/Button'; | ||
import Select, { SelectChangeEvent } from '@mui/material/Select'; | ||
import InputLabel from '@mui/material/InputLabel'; | ||
import FormControl from '@mui/material/FormControl'; | ||
import MenuItem from '@mui/material/MenuItem'; | ||
import { ScatterChart } from '@mui/x-charts/ScatterChart'; | ||
import { BarChart } from '@mui/x-charts/BarChart'; | ||
import { LineChart } from '@mui/x-charts/LineChart'; | ||
import { PieChart } from '@mui/x-charts/PieChart'; | ||
import { data } from './randomData'; | ||
|
||
const scatterSeries = [ | ||
{ | ||
label: 'Series A', | ||
data: data.map((v) => ({ x: v.x1, y: v.y1, id: v.id })), | ||
}, | ||
{ | ||
label: 'Series B', | ||
data: data.map((v) => ({ x: v.x1, y: v.y2, id: v.id })), | ||
}, | ||
]; | ||
const series = [ | ||
{ label: 'Series A', data: data.map((p) => p.y1) }, | ||
{ label: 'Series B', data: data.map((p) => p.y2) }, | ||
]; | ||
|
||
type ChartType = 'scatter' | 'line' | 'bar' | 'pie'; | ||
|
||
export default function KeyboardNavigation() { | ||
const [chartType, setChartType] = React.useState<ChartType>('line'); | ||
const svgRef = React.useRef<SVGSVGElement>(null); | ||
|
||
const handleChange = (event: SelectChangeEvent) => | ||
setChartType(event.target.value as ChartType); | ||
|
||
return ( | ||
<Stack width="100%" sx={{ display: 'block' }}> | ||
<Stack | ||
width="100%" | ||
direction="row" | ||
gap={2} | ||
justifyContent="center" | ||
sx={{ mb: 1 }} | ||
> | ||
<FormControl sx={{ minWidth: 200 }}> | ||
<InputLabel id="chart-type-label">Chart Type</InputLabel> | ||
<Select | ||
labelId="chart-type-label" | ||
id="chart-type-select" | ||
value={chartType} | ||
label="Chart Type" | ||
onChange={handleChange} | ||
> | ||
<MenuItem value="scatter">Scatter</MenuItem> | ||
<MenuItem value="line">Line</MenuItem> | ||
<MenuItem value="bar">Bar</MenuItem> | ||
<MenuItem value="pie">Pie</MenuItem> | ||
</Select> | ||
</FormControl> | ||
<Button onClick={() => svgRef.current?.focus()} variant="contained"> | ||
Focus chart | ||
</Button> | ||
</Stack> | ||
<Chart key={chartType} svgRef={svgRef} type={chartType} /> | ||
</Stack> | ||
); | ||
} | ||
|
||
function Chart<T extends ChartType = ChartType>({ | ||
svgRef, | ||
type, | ||
}: { | ||
svgRef: React.RefObject<SVGSVGElement | null>; | ||
type: T; | ||
}) { | ||
switch (type) { | ||
case 'scatter': | ||
return ( | ||
<ScatterChart | ||
ref={svgRef} | ||
enableKeyboardNavigation | ||
height={300} | ||
series={scatterSeries} | ||
/> | ||
); | ||
case 'line': | ||
return ( | ||
<LineChart | ||
ref={svgRef} | ||
enableKeyboardNavigation | ||
height={300} | ||
xAxis={[{ data: data.map((p) => p.x1).toSorted((a, b) => a - b) }]} | ||
series={series} | ||
/> | ||
); | ||
case 'bar': | ||
return ( | ||
<BarChart | ||
ref={svgRef} | ||
enableKeyboardNavigation | ||
height={300} | ||
xAxis={[ | ||
{ data: data.map((p) => Math.round(p.x1)).toSorted((a, b) => a - b) }, | ||
]} | ||
series={series} | ||
/> | ||
); | ||
case 'pie': | ||
return ( | ||
<PieChart | ||
ref={svgRef} | ||
enableKeyboardNavigation | ||
series={[ | ||
{ | ||
arcLabel: 'value', | ||
data: [ | ||
{ id: 0, value: 10, label: 'series A' }, | ||
{ id: 1, value: 15, label: 'series B' }, | ||
{ id: 2, value: 20, label: 'series C' }, | ||
], | ||
}, | ||
]} | ||
height={300} | ||
hideLegend={false} | ||
/> | ||
); | ||
|
||
default: | ||
throw new Error(`Unknown chart type: ${type}`); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
--- | ||
productId: x-charts | ||
title: Charts - Accessibility | ||
packageName: '@mui/x-charts' | ||
--- | ||
|
||
# Accessibility | ||
|
||
<p class="description">Learn how the Charts implement accessibility features and guidelines, including keyboard navigation that follows international standards.</p> | ||
|
||
:::info | ||
A common misconception about accessibility is to only consider blind people and the screen reader. | ||
But there are other disability to consider, like: | ||
|
||
- **Color blindness**, making it hard to distinguish different series, or low contrast elements. | ||
- **Motion disability**, making it hard to open the tooltip on a given item. | ||
- **Cognitive disability**, making it hard to focus your attention on some details. | ||
- **Vestibular dysfunction**, making you uncomfortable with animations. | ||
|
||
::: | ||
|
||
## Guidelines | ||
|
||
Common conformance guidelines for accessibility include: | ||
|
||
- Globally accepted standard: [WCAG](https://www.w3.org/WAI/standards-guidelines/wcag/) | ||
- US: | ||
- [ADA](https://www.ada.gov/) - US Department of Justice | ||
Check warning on line 28 in docs/data/charts/accessibility/accessibility.md
|
||
- [Section 508](https://www.section508.gov/) - US federal agencies | ||
Check warning on line 29 in docs/data/charts/accessibility/accessibility.md
|
||
- Europe: [EAA](https://employment-social-affairs.ec.europa.eu/policies-and-activities/social-protection-social-inclusion/persons-disabilities/union-equality-strategy-rights-persons-disabilities-2021-2030/european-accessibility-act_en) (European Accessibility Act) | ||
|
||
WCAG 2.1 has three levels of conformance: A, AA, and AAA. | ||
Level AA exceeds the basic criteria for accessibility and is a common target for most organizations, so this is what this library aims to support. | ||
|
||
The WAI-ARIA Authoring Practices includes examples on [Tooltip](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/). | ||
|
||
## Animation | ||
|
||
Some charts have animations when rendering or when data updates. | ||
For users with vestibular motion disorders those animations can be problematic. | ||
By default animations are toggled based on the [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) media feature. | ||
|
||
<!-- | ||
## Screen reader compatibility | ||
|
||
Date and Time Pickers use ARIA roles and robust focus management across the interactive elements to convey the necessary information to users, being optimized for use with assistive technologies. | ||
--> | ||
|
||
## Keyboard support | ||
|
||
:::warning | ||
This feature is under development. | ||
The way keyboard interaction is visualized will evolve. | ||
Check warning on line 53 in docs/data/charts/accessibility/accessibility.md
|
||
|
||
For example the element highlight, or tooltip will be impacted by the feature. | ||
Check warning on line 55 in docs/data/charts/accessibility/accessibility.md
|
||
alexfauquette marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Those modifications will not be considered as breaking changes and so be added during minor or patch versions. | ||
Check warning on line 56 in docs/data/charts/accessibility/accessibility.md
|
||
::: | ||
|
||
Set `enableKeyboardNavigation` to `true` to enable the keyboard navigation on your charts. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have a global enable/disable toggle for this? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a sentence about how to use theme provider to do so There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add a demo? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||
You can also enable it globally using [theme default props](/material-ui/customization/theme-components/#theme-default-props) | ||
|
||
```js | ||
components: { | ||
MuiChartDataProvider: { | ||
defaultProps: { | ||
enableKeyboardNavigation: true | ||
}, | ||
}, | ||
} | ||
``` | ||
|
||
{{"demo": "KeyboardNavigation.js"}} | ||
|
||
This feature is currently supported by line, bar, pie, scatter, and sparkline charts. | ||
|
||
This makes the SVG component focusable thanks to [`tabIndex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex). | ||
When focused, the chart highlights a value item that can be modified with arrow navigation. | ||
|
||
| Keys | Description | | ||
| --------------------------------------------------------------------: | :---------------------------- | | ||
| <kbd class="key">Arrow Left</kbd>, <kbd class="key">Arrow Right</kbd> | Moves focus inside the series | | ||
| <kbd class="key">Arrow Up</kbd>, <kbd class="key">Arrow Down</kbd> | Move focus between series | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.