Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c768cf6
[charts] Split the keyboard focus state into isFocused and isFocusVis…
JCQuintas Jul 29, 2026
526c673
[charts] Expose the accessibility proxy element on the chart instance
JCQuintas Jul 29, 2026
df7aa1b
[charts] Add focusItem and focusChart to the keyboard navigation inst…
JCQuintas Jul 29, 2026
93e7d23
[charts] Add the focusItemOnClick prop
JCQuintas Jul 29, 2026
1434037
[charts] Focus the clicked item on every keyboard navigable series
JCQuintas Jul 29, 2026
e8012cd
[charts] Test the click to focus behavior
JCQuintas Jul 29, 2026
1bedb13
[docs] Document click to focus and focusItemOnClick
JCQuintas Jul 29, 2026
76e3fac
[charts] Resolve a click that hit no item from the axis
JCQuintas Jul 29, 2026
fe087cb
[charts] Test the theme default props and fix the documented componen…
JCQuintas Jul 29, 2026
c78d2c8
[charts] Focus the clicked item in RadarSeriesPlot
JCQuintas Jul 29, 2026
c149a2d
[charts] Resolve the axis fallback against the target series axis
JCQuintas Jul 29, 2026
8049d3c
[charts] Do not claim a focus the chart could not take
JCQuintas Jul 30, 2026
91a8a59
[charts] Keep the pointer cursor tied to onItemClick
JCQuintas Jul 30, 2026
67b7f53
[docs] Document click to focus for custom components
JCQuintas Jul 30, 2026
690ec24
[charts] Merge the focus entry points into one path
JCQuintas Jul 30, 2026
13878a2
[charts] Leave the radial charts out of click to focus
JCQuintas Jul 30, 2026
b548ae3
[charts] Simplify the keyboard focus plugin
JCQuintas Jul 30, 2026
2425de9
[charts] Cover the click resolution against a controlled highlight
JCQuintas Jul 30, 2026
89515c5
[charts] Ignore a click that resolves to no item
JCQuintas Jul 30, 2026
06271b6
[charts] Resolve the clicked item from the item under the pointer
JCQuintas Aug 3, 2026
6c19282
wip-radar-split
JCQuintas Aug 3, 2026
f2c68fa
[charts] Register the tap listener in its own effect
JCQuintas Aug 3, 2026
b84cfe4
[charts-premium] Cover click to focus on the radial charts
JCQuintas Aug 3, 2026
70de9c0
[charts] Keep the tooltip on the pointer when clicking an item
JCQuintas Aug 3, 2026
450449d
Merge branch 'master' into feat/charts-click-to-focus
JCQuintas Aug 3, 2026
9cb041f
[charts] Focus the tapped item on touch
JCQuintas Aug 3, 2026
24e5800
Merge remote-tracking branch 'origin/feat/charts-click-to-focus' into…
JCQuintas Aug 3, 2026
dabcdd2
Merge remote-tracking branch 'upstream/master' into feat/charts-click…
JCQuintas Aug 5, 2026
6077861
[charts] Drop a pending item when the click is stopped
JCQuintas Aug 6, 2026
a3bc928
Merge branch 'master' into feat/charts-click-to-focus
JCQuintas Aug 7, 2026
925e599
[docs] Regenerate the API documentation after the merge
JCQuintas Aug 7, 2026
c5f4883
Merge remote-tracking branch 'origin/feat/charts-click-to-focus' into…
JCQuintas Aug 7, 2026
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
34 changes: 31 additions & 3 deletions docs/data/charts/accessibility/KeyboardNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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 FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import { ScatterChart } from '@mui/x-charts/ScatterChart';
import { BarChart } from '@mui/x-charts/BarChart';
import { LineChart } from '@mui/x-charts/LineChart';
Expand Down Expand Up @@ -33,6 +35,7 @@ const series = [

export default function KeyboardNavigation() {
const [chartType, setChartType] = React.useState('line');
const [focusItemOnClick, setFocusItemOnClick] = React.useState(false);
const chartRef = React.useRef(null);

const handleChange = (event) => setChartType(event.target.value);
Expand Down Expand Up @@ -67,16 +70,38 @@ export default function KeyboardNavigation() {
>
Focus chart
</Button>
<FormControlLabel
control={
<Switch
checked={focusItemOnClick}
onChange={(event) => setFocusItemOnClick(event.target.checked)}
/>
}
label="focusItemOnClick"
/>
</Stack>
<Chart key={chartType} chartRef={chartRef} type={chartType} />
<Chart
key={chartType}
chartRef={chartRef}
type={chartType}
focusItemOnClick={focusItemOnClick}
/>
</Stack>
);
}

function Chart({ chartRef, type }) {
function Chart({ chartRef, type, focusItemOnClick }) {
switch (type) {
case 'scatter':
return <ScatterChart ref={chartRef} height={300} series={scatterSeries} />;
return (
<ScatterChart
ref={chartRef}
height={300}
series={scatterSeries}
focusItemOnClick={focusItemOnClick}
/>
);

case 'line':
return (
<LineChart
Expand All @@ -85,6 +110,7 @@ function Chart({ chartRef, type }) {
xAxis={[{ data: data.map((p) => p.x1).toSorted((a, b) => a - b) }]}
series={series}
slotProps={{ tooltip: { trigger: 'item' } }}
focusItemOnClick={focusItemOnClick}
/>
);

Expand All @@ -98,6 +124,7 @@ function Chart({ chartRef, type }) {
]}
slotProps={{ tooltip: { trigger: 'item' } }}
series={series}
focusItemOnClick={focusItemOnClick}
/>
);

Expand All @@ -118,6 +145,7 @@ function Chart({ chartRef, type }) {
]}
height={300}
hideLegend={false}
focusItemOnClick={focusItemOnClick}
/>
);

Expand Down
33 changes: 31 additions & 2 deletions docs/data/charts/accessibility/KeyboardNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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 FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import { ScatterChart } from '@mui/x-charts/ScatterChart';
import { BarChart } from '@mui/x-charts/BarChart';
import { LineChart } from '@mui/x-charts/LineChart';
Expand Down Expand Up @@ -34,6 +36,7 @@ type ChartType = 'scatter' | 'line' | 'bar' | 'pie';

export default function KeyboardNavigation() {
const [chartType, setChartType] = React.useState<ChartType>('line');
const [focusItemOnClick, setFocusItemOnClick] = React.useState(false);
const chartRef = React.useRef<HTMLDivElement>(null);

const handleChange = (event: SelectChangeEvent) =>
Expand Down Expand Up @@ -71,22 +74,45 @@ export default function KeyboardNavigation() {
>
Focus chart
</Button>
<FormControlLabel
control={
<Switch
checked={focusItemOnClick}
onChange={(event) => setFocusItemOnClick(event.target.checked)}
/>
}
label="focusItemOnClick"
/>
</Stack>
<Chart key={chartType} chartRef={chartRef} type={chartType} />
<Chart
key={chartType}
chartRef={chartRef}
type={chartType}
focusItemOnClick={focusItemOnClick}
/>
</Stack>
);
}

function Chart<T extends ChartType = ChartType>({
chartRef,
type,
focusItemOnClick,
}: {
chartRef: React.RefObject<HTMLDivElement | null>;
type: T;
focusItemOnClick: boolean;
}) {
switch (type) {
case 'scatter':
return <ScatterChart ref={chartRef} height={300} series={scatterSeries} />;
return (
<ScatterChart
ref={chartRef}
height={300}
series={scatterSeries}
focusItemOnClick={focusItemOnClick}
/>
);
case 'line':
return (
<LineChart
Expand All @@ -95,6 +121,7 @@ function Chart<T extends ChartType = ChartType>({
xAxis={[{ data: data.map((p) => p.x1).toSorted((a, b) => a - b) }]}
series={series}
slotProps={{ tooltip: { trigger: 'item' } }}
focusItemOnClick={focusItemOnClick}
/>
);
case 'bar':
Expand All @@ -107,6 +134,7 @@ function Chart<T extends ChartType = ChartType>({
]}
slotProps={{ tooltip: { trigger: 'item' } }}
series={series}
focusItemOnClick={focusItemOnClick}
/>
);
case 'pie':
Expand All @@ -126,6 +154,7 @@ function Chart<T extends ChartType = ChartType>({
]}
height={300}
hideLegend={false}
focusItemOnClick={focusItemOnClick}
/>
);

Expand Down
72 changes: 70 additions & 2 deletions docs/data/charts/accessibility/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ You can also disable it globally using [theme default props](/material-ui/custom

```js
components: {
MuiChartDataProvider: {
MuiChartsDataProvider: {
defaultProps: {
disableKeyboardNavigation: true
},
Expand All @@ -66,7 +66,7 @@ components: {

{{"demo": "KeyboardNavigation.js"}}

This feature is currently supported by the following charts: line, bar, pie, scatter, sparkline, funnel, radar, heatmap, sankey, and range bar.
This feature is currently supported by the following charts: line, bar, pie, scatter, sparkline, funnel, radar, heatmap, sankey, map, radial bar, radial line, and range bar.

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.
Expand All @@ -76,6 +76,38 @@ When focused, the chart highlights a value item that can be modified with arrow
| <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 |

### Start from a clicked item

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

All series currently tells the chart which item the pointer is over, which is how the tooltip and the highlight work. So click-to-focus could in theory ignore clicks entirely and just focus whatever is currently hovered, which would be simpler than this pr.

That would drop the changes to useInteractionItemProps, so most of the diff, along with the merged click handler and the cursor adjustments it needed.

The problem is that highlightedItem and tooltipItem are controlled props. Once an app controls the highlight, setHighlight returns early and the store stops following the pointer at all (useChartHighlight.ts#L105), so the stored item is whatever the app decided rather than what was clicked. On those charts you'd click one bar and focus another, silently, and only for people using a documented API. The tooltip item has the same catch.

So there's no single reliable "item under the pointer" to read from, and each series has to report its own click instead.

@alexfauquette Question is, should highlight or tooltip control the focus? Or should the focus be independent? 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Question is, should highlight or tooltip control the focus?

IMO, it's different. The highlight could be sync between different chart for other components. Which make no sens for focus.

Or should the focus be independent? 🤔

Should we introduce an intermediate repesentation?

Disclaimer: I worked on it a long time ago so I might forget stuff

An initial reason why tooltip and highlight are handled differently because of edge cases. Mostly the fact that you can highlight a line series but not display a tooltip for it.

Currently, The tooltip and highlight work nearly the same way (When you see a setTooltipItem(), the setHighlight is not that far ;) )

And the swich between focused/highlighted/controlled item is done by selector

export const selectorChartsHighlightedItem = createSelectorMemoized(
  selectHighlight,
  selectorChartsKeyboardItem,
  function selectorChartsHighlightedItem(highlight, keyboardItem) {
    return highlight.isControlled || highlight.lastUpdate === 'pointer'
      ? highlight.item
      : keyboardItem;
  },
);

Technically a solution could be:

  • introduce an hovered item state purely handled by the library and stored in the interaction
  • derive from this one the highlight/tooltip item from pointer. If highlight or tooltip is controlled, the selector just ignore it and return highlight.item and tooltip.item

That was my 2 cents on this topic

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@alexfauquette nice input, that managed to decrease the touched files from this PR by quite a few 😆


Clicking an item makes it the item keyboard navigation resumes from, so users can jump straight to a distant data point instead of tabbing into the chart and pressing an arrow key repeatedly.
Screen readers announce the clicked item right away.

A click that lands on no item is resolved from the axis under the pointer, so clicking the empty space above a bar still focuses that bar's column.
The item is taken from the series being navigated, or from the first series when none is focused yet.
Clicks that resolve to no item at all are ignored, so clicking next to a pie leaves the focus where it was.

How the focus indicator behaves depends on how the user got there:

- If the chart was not being navigated with the keyboard, the click gives the chart focus but the indicator stays hidden. It appears on the next key press, on the item next to the one clicked.
- If the focus indicator is already visible, the click moves it to the clicked item and keeps it visible.

Set `focusItemOnClick` to show the indicator immediately on every click:

```jsx
<BarChart focusItemOnClick series={series} />
```

It can also be set globally using [theme default props](/material-ui/customization/theme-components/#theme-default-props):

```js
components: {
MuiChartsDataProvider: {
defaultProps: {
focusItemOnClick: true
},
},
}
```

### Activating the focused item

Charts can trigger their click callbacks from the keyboard, so mouse-only interactions such as drill-down or filtering stay available to keyboard users ([WCAG 2.1 SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html)).
Expand Down Expand Up @@ -106,6 +138,9 @@ Both become the default in v10.

When a chart exposes several item callbacks, activation fires the one a pointer would reach first on the focused data point: line charts try `onMarkClick`, then `onLineClick`, then `onAreaClick`; radar charts try `onMarkClick`, then `onAreaClick`; and Sankey charts call `onNodeClick` or `onLinkClick` depending on the focused element. Only one of them fires.

Activation acts on the visible focus indicator.
A click sets the item keyboard navigation resumes from without revealing it, so <kbd class="key">Enter</kbd> and <kbd class="key">Space</kbd> do nothing until a key press makes the focus visible.

## Screen reader compatibility

Charts use a proxy strategy to support screen reader when user navigate with keyboard navigation.
Expand All @@ -132,3 +167,36 @@ import { FocusedHeatmapCell } from '@mui/x-charts-pro/Heatmap';
import { FocusedFunnelSection } from '@mui/x-charts-pro/FunnelChart';
import { FocusedSankeyLink, FocusedSankeyNode } from '@mui/x-charts-pro/SankeyChart';
```

### Custom components

Click-to-focus reads the item the pointer is over, which every series already reports for the tooltip and the highlight.
A custom component rendered through a slot inherits that for free, as long as it forwards the interaction props to the rendered element:

```jsx
function CustomMarker({
x,
y,
color,
seriesId,
dataIndex,
isHighlighted,
isFaded,
...other
}) {
// `other` carries the pointer handlers that report the item.
return (
<path d={shape} transform={`translate(${x}, ${y})`} fill={color} {...other} />
);
}
```

When building a plot from scratch rather than through a slot, `useInteractionItemProps` returns those handlers for an item:

```jsx
import { useInteractionItemProps } from '@mui/x-charts/internals';

function CustomPlotItem({ seriesId, dataIndex }) {
return <rect {...useInteractionItemProps({ type: 'bar', seriesId, dataIndex })} />;
}
```
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/bar-chart-premium.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"grid": {
"type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" }
},
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/bar-chart-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"grid": {
"type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" }
},
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/bar-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"grid": {
"type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" }
},
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/charts-container.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"height": { "type": { "name": "number" } },
"hiddenItems": {
"type": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/funnel-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"gap": { "type": { "name": "number" }, "default": "0" },
"height": { "type": { "name": "number" } },
"hiddenItems": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/heatmap-premium.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"height": { "type": { "name": "number" } },
"hideLegend": { "type": { "name": "bool" } },
"highlightedItem": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/heatmap.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"height": { "type": { "name": "number" } },
"hideLegend": { "type": { "name": "bool" } },
"highlightedItem": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/line-chart-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"description": "{ enablePositionBasedPointerInteraction?: bool, keyboardActivation?: bool }"
}
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"grid": {
"type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" }
},
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/line-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"description": "{ enablePositionBasedPointerInteraction?: bool, keyboardActivation?: bool }"
}
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"grid": {
"type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" }
},
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/pie-chart-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"height": { "type": { "name": "number" } },
"hiddenItems": {
"type": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/pie-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"height": { "type": { "name": "number" } },
"hiddenItems": {
"type": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/radar-chart-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"height": { "type": { "name": "number" } },
"hiddenItems": {
"type": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/radar-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"height": { "type": { "name": "number" } },
"hiddenItems": {
"type": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/radial-bar-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"grid": { "type": { "name": "shape", "description": "{ radius?: bool, rotation?: bool }" } },
"height": { "type": { "name": "number" } },
"hiddenItems": {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/radial-line-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"experimentalFeatures": {
"type": { "name": "shape", "description": "{ keyboardActivation?: bool }" }
},
"focusItemOnClick": { "type": { "name": "bool" }, "default": "false" },
"grid": { "type": { "name": "shape", "description": "{ radius?: bool, rotation?: bool }" } },
"height": { "type": { "name": "number" } },
"hiddenItems": {
Expand Down
Loading
Loading