Skip to content

[charts] Focus the clicked item for keyboard navigation - #23247

Merged
JCQuintas merged 32 commits into
mui:masterfrom
JCQuintas:feat/charts-click-to-focus
Aug 7, 2026
Merged

JCQuintas merged 32 commits into
mui:masterfrom
JCQuintas:feat/charts-click-to-focus

Conversation

@JCQuintas

@JCQuintas JCQuintas commented Jul 29, 2026 •

Copy link
Copy Markdown
Member

Closes #23195

Clicking a chart item now makes it the active item for keyboard navigation and screen-reader narration, across every keyboard-navigable series.

Keyboard navigation is a great feature, but today the only way in is Tab — and then hoping you landed on the right proxy div — followed by repeated arrow presses to reach a distant point. A click gets you there directly.

Behavior

Situation Result
Click an item while the chart is not focused Item is stored, chart takes focus, screen reader announces it, the focus indicator stays hidden
Then any arrow key The indicator appears, moving from the clicked item
Click an item while the indicator is already visible The indicator moves to the clicked item and stays visible
Click that hits no item Resolved from the axis under the pointer (see below)
focusItemOnClick (new prop, false by default) The indicator shows immediately on every click
disableKeyboardNavigation No-op

The two modes are the point: a mouse user never sees a focus ring appear out of nowhere, while a keyboard user keeps the ring they were already using.

Clicks that hit no item

Only focusing the chart felt unresponsive, so a click that hits no item falls back to the axis under the pointer: the dataIndex comes from the x axis (then the y axis), and the item is taken from the series being navigated, or from the first series when none is focused yet. Clicking the empty space above a bar focuses that bar's column.

Charts with no cartesian axis, and clicks off the axis, keep the previous behavior of only focusing the chart while preserving the item.

Scatter charts configured with a hit area resolve the closest point first, so a near-miss click focuses the point rather than an axis index.

Implementation

keyboardNavigation.isFocused conflated the chart owns the DOM focus with the focus indicator is rendered. It is split into isFocused and a new isFocusVisible; the indicator, highlight and tooltip selectors require both, while selectorChartsFocusedOrToFocusedItem stays ungated so the accessibility description keeps working — that is what makes a click announce without showing a ring.

instance.focusItem() / focusChart() are new. Visibility comes from a focus-visible intent ref: pointerdown on the chart sets it to focusItemOnClick || <already visible>, and any document keydown sets it true (which covers tabbing in from outside).

Click wiring is split in two. useInteractionItemProps gained a merged onClick, covering pie, line marks, scatter markers, funnel sections, sankey nodes and links, and map shapes — merging inside the hook because the spread order relative to onClick was inconsistent across those components. The paths that must compute the index themselves call focusItem directly: bars and heatmap cells through their container hit test, line and area paths through the axis index, radar areas and marks through the click angle or the mark index, and scatter through the closest point.

On radar, which of the two paths runs depends on onMarkClick: marks are pointer transparent without it, so the click falls through to the area and the index comes from the click angle. With it, the mark takes the click and its own index is used. Both paths are needed — dropping either leaves one configuration unable to focus.

The highlight and tooltip only switch to keyboard mode when the focus is visible, so clicking an item no longer detaches the tooltip from the pointer.

Notes for review

  • The radial charts are deliberately left out. Giving them click-to-focus requires registering useChartItemClick, which also exposes a public onItemClick they never had. That is an API addition rather than an accessibility fix, so it moved to [charts-premium] Add onItemClick to the radial charts #23253.
  • restoreFocus no longer unconditionally flips lastUpdate to 'keyboard' — only when the focus is visible. This is the behavioral change most worth a careful look.
  • Two pre-existing bugs surfaced and are fixed here. The accessibility proxy root now keeps tabIndex={-1} once an announcer child owns the tab stop: the browser was blurring it mid hand-off, which read as the focus leaving the chart. And arrow keys at the edge of a non-cycling series now reveal the focus even though the focused item does not move.
  • The axis fallback resolves against the target series' axis, not the first one, so a chart with a secondary axis focuses the right point. Covered by a unit test on getItemAtAxisPosition.
  • Drive-by docs fix. The accessibility page documented the charts theme defaults under MuiChartDataProvider, which matches no component — the snippet silently did nothing. The name is MuiChartsDataProvider. Caught by adding a test for focusItemOnClick set through the theme; the existing disableKeyboardNavigation snippet had the same typo.
  • Scatter markers keep firing onItemClick mid zoom. Moving the callback into the interaction props initially put it behind the isInteracting guard, which silently suppressed it during a zoom gesture on the Pro/Premium async renderer. Only the pointer handlers are dropped there now, matching the previous behavior; covered by unit tests on getMarkerInteractionProps.
  • Known gap: the WebGL range bar plot has no click path at all, so range bar click-to-focus only works with the SVG plot.
  • Not verified: screen-reader double announcement on click needs a manual VoiceOver/NVDA pass. focusItem focuses one announcer div and the message effect then flips to the other.

isFocusVisible is part of the state shipped through @mui/x-charts/internals. isFocused keeps its DOM-focus meaning; the indicator meaning moved to the new field.

Testing

18 behavior tests in useChartKeyboardNavigation.clickToFocus.test.tsx covering both modes on bar charts, the closest point on scatter, line marks and paths, pie arcs, radar marks, the axis fallback, and focused-series preservation. Plus selector unit tests and 6 unit tests for the axis resolver. Every existing checkClickEvent.test.tsx asserts onItemClick still fires exactly once, guarding the merged-onClick rewrite.

…ible

`isFocused` conflated "the chart subtree owns the DOM focus" with "the focus
indicator is rendered". Split the second meaning into `isFocusVisible` so an
upcoming pointer-driven focus can set the item without revealing it.

The focus indicator, highlight and tooltip selectors now require both flags.
`selectorChartsFocusedOrToFocusedItem` stays ungated so the accessibility
description keeps working.

No behavior change: the focusin handler sets both flags.
Adds `chartsAccessibilityProxyRef` to `useChartElementRef` and a
`focusAccessibilityProxy` helper, so the keyboard navigation plugin can move the
DOM focus into the chart without querying the layer container, whose subtree also
holds the zoom slider and brush focusable elements.
…ance

`focusItem` makes any item the one keyboard navigation resumes from and moves the
DOM focus into the chart. A focus-visible intent ref, fed by pointerdown, keydown
and focusout on the layer container, decides whether the focus indicator shows:
pointer driven focus stays hidden until the user presses a key, unless
`focusItemOnClick` is set.

The highlight and tooltip only switch to keyboard mode when the focus is visible,
so clicking an item no longer detaches the tooltip from the pointer.

Navigation keys now always reveal the focus, even at the edges of a series that
does not cycle, where the focused item does not move.
Threads the new keyboard navigation param through the container, radar and heatmap
prop hooks, and regenerates the propTypes and API documentation.
Clicking an item now makes it the item keyboard navigation resumes from, whether or
not an `onItemClick` callback is set.

`useInteractionItemProps` gained a merged `onClick` covering pie, line marks,
scatter markers, radar marks, funnel sections, sankey nodes and links, and map
shapes. The paths that have to compute the index themselves call `focusItem`
directly: bars and heatmap cells through their container hit test, line and area
paths through the axis index, radar areas through the click angle, and scatter
through the closest point, so clicking next to a point focuses it.

Radial bar and radial line charts register `useChartItemClick`, which also gives
them the `onItemClick` callback they were missing.

A click that resolves no item still moves the DOM focus into the chart, keeping the
previously focused item.

Known gap: the WebGL range bar plot has no click path, so range bar click to focus
only works with the SVG plot.
Covers the two modes on bar charts, the closest point resolution on scatter, line
marks and paths, pie arcs and radar marks, and asserts the highlight stays pointer
driven and `onItemClick` still fires exactly once.

The accessibility proxy root keeps a `tabIndex` of -1 once an announcer child owns
the tab stop. Without it the browser blurred the root during the internal hand-off,
which read as the focus leaving the chart and revealed a focus set by a click.

Keyboard modality is tracked on the document so tabbing back in from an outside
element shows the focus again.
Adds a section to the accessibility page explaining the two focus modes, and a
toggle to the keyboard navigation demo. Also lists the map, radial bar and radial
line charts among the ones supporting keyboard navigation.
Clicking the empty space of a cartesian chart only gave the chart focus, which felt
unresponsive. It now resolves the axis under the pointer and focuses the item it
points at, taken from the series being navigated, or from the first series when
none is focused yet.

Clicks outside of any axis, and charts without a cartesian axis, keep the previous
behavior of only focusing the chart.

A pointer interaction that already resolved an item skips the fallback, so the
closest point of a scatter chart is not overwritten by an axis index.
@JCQuintas JCQuintas self-assigned this Jul 29, 2026
@JCQuintas JCQuintas added type: new feature Expand the scope of the product to solve a new problem. scope: charts Changes related to the charts. labels Jul 29, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 🔺+3.52KB(+0.89%) 🔺+1.12KB(+0.93%)
@mui/x-charts-pro 🔺+3.55KB(+0.67%) 🔺+1.14KB(+0.71%)
@mui/x-charts-premium 🔺+3.57KB(+0.55%) 🔺+1.09KB(+0.55%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 0B(0.00%) 0B(0.00%)
@mui/x-scheduler-premium 0B(0.00%) 0B(0.00%)
@mui/x-chat 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes

Performance

Total duration: 1,859.86 ms 🔺+401.32 ms(+27.5%) | Renders: 63 (+0) | Paint: 2,514.44 ms 🔺+589.70 ms(+30.6%)

Test Duration Renders
Heatmap: 100x100 grid 356.58 ms 🔺+120.08 ms(+50.8%) 2 (+0)
SparkLineChart grid of 100 instances 169.02 ms 🔺+78.94 ms(+87.6%) 1 (+0)
LineChart stacked area with multiple series 409.43 ms 🔺+72.19 ms(+21.4%) 2 (+0)
CandlestickChart with big data amount (webgl renderer) 165.60 ms 🔺+51.58 ms(+45.2%) 5 (+0)
BarChart stacked with multiple series 105.24 ms 🔺+43.48 ms(+70.4%) 2 (+0)

…and 19 more (+2 within noise) — details

Metric alarms

Test Metric Change
Heatmap: 100x100 grid bench:paint 🔺 +153.91 ms
SparkLineChart grid of 100 instances bench:paint 🔺 +128.57 ms
LineChart stacked area with multiple series bench:paint 🔺 +86.35 ms
CandlestickChart with big data amount (webgl renderer) bench:paint 🔺 +59.12 ms
BarChart stacked with multiple series bench:paint 🔺 +62.88 ms

…and 16 more metric alarms — details


Check out the code infra dashboard for more information about this PR.

…t name

The accessibility page documented the charts theme defaults under
`MuiChartDataProvider`, which matches no component, so the snippet silently did
nothing. The name is `MuiChartsDataProvider`.

Adds a test covering `focusItemOnClick` set through the theme, which is what
caught it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds “click-to-focus” support for Charts keyboard navigation: clicking a chart item becomes the starting point for arrow-key navigation and triggers immediate screen-reader narration, while keeping the focus indicator hidden unless it was already visible or focusItemOnClick is enabled.

Changes:

  • Split keyboard-navigation “chart has DOM focus” vs “focus indicator is visible” via isFocused + new isFocusVisible, and wire focus visibility intent across pointer vs keyboard interactions.
  • Introduce instance.focusItem() / instance.focusChart() and route click handlers across many series implementations to update the focused item consistently.
  • Add axis-based fallback resolution for background clicks (cartesian charts), and expand tests + docs to cover the new behavior and the focusItemOnClick prop.

Reviewed changes

Copilot reviewed 107 out of 107 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/x-charts/src/SparkLineChart/SparkLineChart.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts/src/ScatterChart/ScatterChart.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts/src/ScatterChart/Scatter.tsx Merges click handling into interaction props; adds keyboard-nav dependency.
packages/x-charts/src/ScatterChart/async/ScatterAsyncBatch.tsx Merges click handling into interaction props; adds keyboard-nav dependency.
packages/x-charts/src/RadarChart/useRadarChartProps.ts Threads focusItemOnClick through chart props.
packages/x-charts/src/RadarChart/RadarSeriesPlot/useInteractionAllItemProps.ts Ensures interaction props include keyboard focus support.
packages/x-charts/src/RadarChart/RadarSeriesPlot/RadarSeriesMarks.tsx Focuses clicked radar marks (but currently gates pointer events on onItemClick).
packages/x-charts/src/RadarChart/RadarSeriesPlot/RadarSeriesArea.tsx Focuses clicked radar area using click angle to resolve dataIndex.
packages/x-charts/src/RadarChart/RadarChart.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts/src/PieChart/PieChart.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts/src/PieChart/PieArc.tsx Uses merged interaction onClick so click-to-focus and consumer click both run once.
packages/x-charts/src/LineChart/useLineItemClickHandler.ts Resolves dataIndex from axis on path clicks and focuses the resolved item.
packages/x-charts/src/LineChart/MarkElement.tsx Ensures mark clicks focus the item via merged interaction onClick.
packages/x-charts/src/LineChart/LinePlot.tsx Wires line-path click to resolver handler and passes cursor as needed.
packages/x-charts/src/LineChart/LineElement.tsx Allows cursor to be overridden by caller.
packages/x-charts/src/LineChart/LineChart.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts/src/LineChart/CircleMarkElement.tsx Ensures circle mark clicks focus the item via merged interaction onClick.
packages/x-charts/src/LineChart/AreaPlot.tsx Wires area-path click to resolver handler and passes cursor as needed.
packages/x-charts/src/LineChart/AreaElement.tsx Allows cursor to be overridden by caller.
packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/utils/getItemAtAxisPosition.ts Adds axis-based item resolver for background clicks.
packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/utils/getItemAtAxisPosition.test.ts Adds unit tests for axis-based background click resolution.
packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.types.ts Adds focusItem/focusChart instance APIs and isFocusVisible state + focusItemOnClick param.
packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.ts Implements focus intent tracking, click-to-focus, and axis fallback; adjusts focus restore behavior.
packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.selectors.ts Gates focus-indicator/highlight/tooltip selectors on “focus visible”; adds selectorChartsIsFocusVisible.
packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.selectors.test.ts Adds selector coverage for visible vs hidden focus state.
packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.clickToFocus.test.tsx Adds click-to-focus integration tests across chart types and modes.
packages/x-charts/src/internals/plugins/featurePlugins/useChartItemClick/useChartItemClick.types.ts Declares keyboard navigation as an optional dependency for item-click plugin.
packages/x-charts/src/internals/plugins/featurePlugins/useChartItemClick/useChartItemClick.ts Focuses clicked item before calling onItemClick, when keyboard navigation is available.
packages/x-charts/src/internals/plugins/featurePlugins/useChartClosestPoint/useChartClosestPoint.types.ts Adds keyboard navigation to dependencies.
packages/x-charts/src/internals/plugins/featurePlugins/useChartClosestPoint/useChartClosestPoint.ts Focuses the closest scatter point on tap/click, then calls onItemClick.
packages/x-charts/src/internals/plugins/corePlugins/useChartElementRef/useChartElementRef.types.ts Adds chartsAccessibilityProxyRef to instance.
packages/x-charts/src/internals/plugins/corePlugins/useChartElementRef/useChartElementRef.ts Creates and exposes chartsAccessibilityProxyRef.
packages/x-charts/src/internals/components/ChartsAccessibilityProxy/index.ts Exports new focusAccessibilityProxy helper.
packages/x-charts/src/internals/components/ChartsAccessibilityProxy/focusAccessibilityProxy.ts Adds helper to focus the correct proxy element without scrolling.
packages/x-charts/src/internals/components/ChartsAccessibilityProxy/ChartsAccessibilityProxy.tsx Fixes tabIndex hand-off behavior and wires proxy ref into chart instance.
packages/x-charts/src/hooks/useInteractionItemProps.ts Adds merged onClick that focuses item then calls consumer handler; updates helper types.
packages/x-charts/src/ChartsContainer/useChartsContainerProps.ts Threads focusItemOnClick into container props.
packages/x-charts/src/ChartsContainer/ChartsContainer.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts/src/BarChart/useRegisterItemClickHandlers.ts Focuses bar item on click even without onItemClick; adds keyboard-nav dependency.
packages/x-charts/src/BarChart/BarChart.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/SankeyChart/SankeyNodeElement.tsx Uses merged interaction onClick to focus node then call consumer handler.
packages/x-charts-pro/src/SankeyChart/SankeyLinkElement.tsx Uses merged interaction onClick to focus link then call consumer handler.
packages/x-charts-pro/src/SankeyChart/SankeyDataProvider.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/SankeyChart/SankeyChart.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/RadarChartPro/RadarChartPro.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/PieChartPro/PieChartPro.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/Heatmap/useHeatmapProps.ts Threads focusItemOnClick through heatmap props.
packages/x-charts-pro/src/Heatmap/Heatmap.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/FunnelChart/FunnelSection.tsx Uses merged interaction onClick to focus section then call consumer handler.
packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-premium/src/ScatterChartPremium/ScatterChartPremium.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-premium/src/RadialLineChart/RadialLineChart.tsx Adds focusItemOnClick and introduces public onItemClick prop types.
packages/x-charts-premium/src/RadialLineChart/RadialLineChart.plugins.ts Adds useChartItemClick plugin to enable click-to-focus + onItemClick.
packages/x-charts-premium/src/RadialBarChart/RadialBarChart.tsx Adds focusItemOnClick and introduces public onItemClick prop types.
packages/x-charts-premium/src/RadialBarChart/RadialBarChart.plugins.ts Adds useChartItemClick plugin to enable click-to-focus + onItemClick.
packages/x-charts-premium/src/Map/MapShape.tsx Uses merged interaction onClick to focus shape then call consumer handler.
packages/x-charts-premium/src/HeatmapPremium/HeatmapPremium.tsx Adds focusItemOnClick PropTypes entry.
packages/x-charts-premium/src/BarChartPremium/RangeBar/RangeBarPlot.tsx Focuses clicked range-bar items (SVG renderer).
packages/x-charts-premium/src/BarChartPremium/BarChartPremium.tsx Adds focusItemOnClick PropTypes entry.
docs/translations/api-docs/charts/spark-line-chart/spark-line-chart.json Documents focusItemOnClick.
docs/translations/api-docs/charts/scatter-chart/scatter-chart.json Documents focusItemOnClick.
docs/translations/api-docs/charts/scatter-chart-pro/scatter-chart-pro.json Documents focusItemOnClick.
docs/translations/api-docs/charts/scatter-chart-premium/scatter-chart-premium.json Documents focusItemOnClick.
docs/translations/api-docs/charts/sankey-data-provider/sankey-data-provider.json Documents focusItemOnClick.
docs/translations/api-docs/charts/sankey-chart/sankey-chart.json Documents focusItemOnClick.
docs/translations/api-docs/charts/radial-line-chart/radial-line-chart.json Documents focusItemOnClick and new onItemClick.
docs/translations/api-docs/charts/radial-bar-chart/radial-bar-chart.json Documents focusItemOnClick and new onItemClick.
docs/translations/api-docs/charts/radar-chart/radar-chart.json Documents focusItemOnClick.
docs/translations/api-docs/charts/radar-chart-pro/radar-chart-pro.json Documents focusItemOnClick.
docs/translations/api-docs/charts/pie-chart/pie-chart.json Documents focusItemOnClick.
docs/translations/api-docs/charts/pie-chart-pro/pie-chart-pro.json Documents focusItemOnClick.
docs/translations/api-docs/charts/line-chart/line-chart.json Documents focusItemOnClick.
docs/translations/api-docs/charts/line-chart-pro/line-chart-pro.json Documents focusItemOnClick.
docs/translations/api-docs/charts/heatmap/heatmap.json Documents focusItemOnClick.
docs/translations/api-docs/charts/heatmap-premium/heatmap-premium.json Documents focusItemOnClick.
docs/translations/api-docs/charts/funnel-chart/funnel-chart.json Documents focusItemOnClick.
docs/translations/api-docs/charts/charts-container/charts-container.json Documents focusItemOnClick.
docs/translations/api-docs/charts/bar-chart/bar-chart.json Documents focusItemOnClick.
docs/translations/api-docs/charts/bar-chart-pro/bar-chart-pro.json Documents focusItemOnClick.
docs/translations/api-docs/charts/bar-chart-premium/bar-chart-premium.json Documents focusItemOnClick.
docs/pages/x/api/charts/spark-line-chart.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/scatter-chart.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/scatter-chart-pro.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/scatter-chart-premium.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/sankey-data-provider.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/sankey-chart.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/radial-line-chart.json Adds focusItemOnClick + onItemClick API page metadata.
docs/pages/x/api/charts/radial-bar-chart.json Adds focusItemOnClick + onItemClick API page metadata.
docs/pages/x/api/charts/radar-chart.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/radar-chart-pro.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/pie-chart.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/pie-chart-pro.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/line-chart.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/line-chart-pro.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/heatmap.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/heatmap-premium.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/funnel-chart.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/charts-container.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/bar-chart.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/bar-chart-pro.json Adds focusItemOnClick API page metadata.
docs/pages/x/api/charts/bar-chart-premium.json Adds focusItemOnClick API page metadata.
docs/data/charts/accessibility/KeyboardNavigation.tsx Updates the demo to toggle and pass focusItemOnClick.
docs/data/charts/accessibility/KeyboardNavigation.js Generated demo output updated to match TSX demo changes.
docs/data/charts/accessibility/accessibility.md Documents click-to-focus behavior and expands supported chart list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 70 to 77
pointerEvents={onItemClick ? undefined : 'none'}
onClick={(event) =>
onItemClick?.(event, { type: 'radar', seriesId: id, dataIndex: index })
}
onClick={(event) => {
const identifier = { type: 'radar', seriesId: id, dataIndex: index } as const;
instance.focusItem?.(identifier);
onItemClick?.(event, identifier);
}}
cursor={onItemClick ? 'pointer' : 'unset'}
{...other}

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.

Click-to-focus does work on radar without onMarkClick — I checked before changing anything. The marks are pointer-transparent, so the click lands on the series area underneath, which resolves the data index from the click angle via getRotationIndex. Added a test covering exactly that case (focuses the mark through the area when no click callback is set).

Making the marks always pointer-active would be a regression: they carry no interaction props, so they'd start intercepting pointer events from the area and break the hover highlight and tooltip on every mark.

That said, chasing this did surface a real bug next door, so thanks — RadarSeriesPlot (the composition component) spread the interaction props after its own onClick, so the handler this PR adds was replacing onAreaClick instead of running alongside it. Fixed in c78d2c8, with a regression test that fails without it.

It also showed my radar tests were too weak: they only asserted that some focus indicator appeared, which passes even when the click resolves nothing. They now assert which mark is focused.

RadarSeriesPlot, the composition component, spread the interaction props after its
own `onClick`, so the click handler added for the keyboard focus replaced
`onAreaClick` instead of running alongside it. It now merges both and focuses the
clicked item, like the split RadarSeriesArea and RadarSeriesMarks already did.

Also strengthens the radar click to focus tests, which only asserted that some
indicator appeared and so passed even when the click resolved nothing.
The fallback read the data index off the first x or y axis, so on a chart with
several axes a click could resolve against a scale the target series is not bound
to and focus the wrong point. The target series is now resolved first, and its own
`xAxisId` / `yAxisId` picks the axis, mirroring useLineItemClickHandler.

Also keeps the scatter marker click alive during a zoom interaction. Moving the
callback into the interaction props had put it behind the `isInteracting` guard,
which silently stopped `onItemClick` from firing mid zoom. Only the pointer
handlers are dropped there, as before.
`updateFocus` hardcoded `isFocused: true`, which assumes the accessibility proxy is
present to receive the focus. A composition that does not render it would get an
indicator that no blur can clear, since the focusout listener lives on the same
missing container. The flag is now read from the DOM, and the item is still stored
so it is restored on the next focus.

Also documents that the interaction props own the click, so consumers pass their
callback through the options instead of setting `onClick` next to the spread.
Scatter markers and range bars derived their cursor from the presence of a click
handler. Now that the handler is always attached to drive the keyboard focus, they
showed a pointer cursor on charts that are not clickable.

Both take the cursor from their plot instead, which derives it from `onItemClick`
like the other series do. `ScatterMarkerProps` gained a `cursor` prop so custom
markers can do the same, since `onClick` no longer tells them whether the chart is
interactive.
Custom slot components have to forward the click handler for click to focus to
work, and can no longer read `onClick` to tell whether the chart is interactive.
Documents both, and how to reach the same behavior from a plot built from scratch.
`focusChart` and `focusItem` carried the same enabled check, visibility resolution,
proxy focus and no-op comparison. Both now delegate to a single `applyFocus`, where
an undefined item means focusing the chart alone, and `focusItem` only adds the
identifier validation on top.
Registering `useChartItemClick` on the radial charts gave them click to focus, but
it also added a public `onItemClick` they never had. That is a separate API
addition, so it moves to its own pull request.
- The click fallback listens on the document, so it runs after the React handlers
  instead of before them. It was resolving an item on every click, only to be
  corrected by the series handler a moment later.
- `restoreFocus` was `applyFocus` without the focusing step, which is already a
  no-op once the focus is inside the chart. It now delegates.
- `focusChart` was only used by the fallback, so it is no longer part of the
  instance API.
- Inlines a single use helper and folds the two symmetric visible index lookups
  into one.
The clicked item is resolved by the series that owns the click, not read from the
highlight. Controlling the highlight stops the store from tracking the pointer, so
a focus derived from it would follow the consumer instead. Pins that down.
| <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 😆

The fallback focused the chart whenever a click hit nothing. The drawing area is a
rectangle, but the data rarely fills it, so clicking next to a pie, outside its
circle, took the focus for a click that was not meant for the chart.

Such clicks are now left alone. Cartesian charts are unaffected: a click in the
plot still resolves through the axis.
Every series already reports the item the pointer is over, for the tooltip and the
highlight, so the click needs no per series wiring of its own. That state was not
usable directly because `highlightedItem` and `tooltipItem` are controlled props:
once controlled, the store stops following the pointer.

Adds `interaction.hoveredItem`, written by the same hover paths but never
controlled, and focuses it on tap. This replaces the merged click handler in
`useInteractionItemProps` and the click wiring of the pie, line, scatter, radar,
funnel, sankey, map and range bar series, along with the cursor adjustments those
needed.

Touch has no hover phase, so the container hit test, the closest point and the
radar area also resolve the item on `pointerdown`: a stationary tap does not
produce a `pointermove`.

The highlight and tooltip selectors are untouched, and can be derived from the
hovered item separately.
It was registered inside the effect that binds the DOM listeners on the layer
container, which it has nothing to do with.

Also drops the radar wiring, which needs the pointer item to be resolved from the
click angle, so it moves to its own pull request.
The radial charts ship `getItemAtPosition`, so they report the item under the
pointer through the shared hit test and need no wiring of their own.
Clicking an item moved the highlight and the tooltip to the focused item, which
detaches the tooltip from the pointer. The pointer is still on the chart though,
so they have to keep following it.

They now hand over to the focused item only when the last interaction came from
the keyboard, tracked next to the focus visibility. The previous guard was the
focus visibility alone, which let a click through whenever the focus was already
visible or `focusItemOnClick` was set.
@JCQuintas
JCQuintas marked this pull request as ready for review August 3, 2026 17:56
Touch clears the pointer item on `pointerleave`, right after the tap, and then
emits its compatibility `mousedown`, which moves the focus to the body. Resolving
and focusing in the same step therefore either read a cleared item or lost the
focus a moment later.

The tap now only resolves the item, and the click that closes the sequence focuses
it. A mouse is unaffected: its `mousedown` comes before the tap.
@JCQuintas
JCQuintas requested a review from alexfauquette August 4, 2026 12:07
@JCQuintas
JCQuintas requested a review from silviuaavram August 5, 2026 16:12
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

…-to-focus

# Conflicts:
#	docs/data/charts/accessibility/accessibility.md
#	docs/pages/x/api/charts/bar-chart-premium.json
#	docs/pages/x/api/charts/bar-chart-pro.json
#	docs/pages/x/api/charts/bar-chart.json
#	docs/pages/x/api/charts/charts-container.json
#	docs/pages/x/api/charts/funnel-chart.json
#	docs/pages/x/api/charts/heatmap-premium.json
#	docs/pages/x/api/charts/heatmap.json
#	docs/pages/x/api/charts/pie-chart-pro.json
#	docs/pages/x/api/charts/pie-chart.json
#	docs/pages/x/api/charts/radar-chart-pro.json
#	docs/pages/x/api/charts/radar-chart.json
#	docs/pages/x/api/charts/radial-bar-chart.json
#	docs/pages/x/api/charts/radial-line-chart.json
#	docs/pages/x/api/charts/sankey-chart.json
#	docs/pages/x/api/charts/sankey-data-provider.json
#	packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.ts
#	packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.types.ts
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 5, 2026

@noraleonte noraleonte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me 👀

Leaving one small comment

const item = pendingFocusItemRef.current;
pendingFocusItemRef.current = null;

if (item !== null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI pointed this out, might be worth a check:

_ If the second click never reaches document (i.e. tap remembers the item, but handler has e.stopPropagation()), the item stays remembered with no expiry and fires on whatever the user clicks next._

Suggested change
if (item !== null) {
if (item !== null && chartsLayerContainerRef.current?.contains(event.target as Node))

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.

Def a problem, usually user interactions are complex due to the async nature 😢

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.

Good catch, that's a real one. Applied in 6077861.

I confirmed the failure mode before fixing it. The pending item is already cleared on every document click, so it can only ever fire once, but that one time can be a click anywhere on the page: stopPropagation keeps the originating click from reaching the listener, then the next click consumes the stale item. Since the ref is only reset on a pointerdown inside the chart, a click outside never resets it first.

Added a test that clicks a bar with a handler stopping the propagation, then clicks a button outside the chart, and asserts no item ends up focused. It fails without the guard (expected SVGRectElement to equal null).

A handler calling `stopPropagation` keeps the click from reaching the document
listener, so the item the tap resolved stayed pending and was applied to whatever
the user clicked next, including outside the chart.

The listener now only focuses when the click happened inside the chart.
The merge took the master version of the generated files, and the regeneration that
followed silently failed on an incompatible Node version, so `focusItemOnClick` was
missing from the chart API pages.
@JCQuintas
JCQuintas merged commit c9fb27f into mui:master Aug 7, 2026
22 checks passed
@JCQuintas
JCQuintas deleted the feat/charts-click-to-focus branch August 7, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: charts Changes related to the charts. type: new feature Expand the scope of the product to solve a new problem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants