[charts-pro] Add keyboard navigation to sankey#20777
[charts-pro] Add keyboard navigation to sankey#20777alexfauquette merged 9 commits intomui:masterfrom
Conversation
|
Deploy preview: https://deploy-preview-20777--material-ui-x.netlify.app/ Updated pages: Bundle size report
|
There was a problem hiding this comment.
Pull request overview
This PR adds keyboard navigation functionality to the Sankey chart component, enabling users to navigate between nodes and links using arrow keys. The implementation includes visual focus indicators and exports the necessary hooks for consumers.
Key Changes
- Implements keyboard navigation handler for Sankey charts with arrow key support for navigating nodes (up/down within layers, left/right to connected links) and links (up/down among same-source links, left/right to source/target nodes)
- Adds FocusedSankeyNode and FocusedSankeyLink components to visually indicate the currently focused element with a stroke outline
- Exports useFocusedItem hook from x-charts and x-charts-pro packages for programmatic access to the focused item
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/x-charts-pro/src/SankeyChart/seriesConfig/keyboardFocusHandler.ts | New keyboard navigation logic for Sankey charts, handling arrow key navigation between nodes and links |
| packages/x-charts-pro/src/SankeyChart/FocusedSankeyNode.tsx | Component rendering focus indicator rectangle for focused nodes |
| packages/x-charts-pro/src/SankeyChart/FocusedSankeyLink.tsx | Component rendering focus indicator path for focused links |
| packages/x-charts-pro/src/SankeyChart/SankeyChart.tsx | Integrates FocusedSankeyNode and FocusedSankeyLink components, adds enableKeyboardNavigation prop |
| packages/x-charts-pro/src/SankeyChart/SankeyChart.plugins.ts | Registers keyboard navigation plugin for Sankey charts |
| packages/x-charts-pro/src/SankeyChart/seriesConfig/index.ts | Adds keyboardFocusHandler to Sankey series configuration |
| packages/x-charts/src/hooks/useFocusedItem.ts | New hook to access the currently focused item from keyboard navigation state |
| packages/x-charts/src/hooks/index.ts | Exports useFocusedItem hook |
| packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/index.ts | Exports keyboard focus handler types |
| packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/keyboardFocusHandler.types.ts | Updates type signatures to support series-specific type parameters |
| packages/x-charts/src/internals/plugins/models/chart.ts | Adds TSeriesType generic parameter to ChartState type |
| packages/x-charts/src/internals/plugins/corePlugins/corePlugins.ts | Updates ChartCorePluginSignatures to be generic over series type |
| scripts/x-charts.exports.json | Adds useFocusedItem to exports |
| scripts/x-charts-pro.exports.json | Adds FocusedSankeyLink, FocusedSankeyNode, and useFocusedItem to exports |
| scripts/x-charts-premium.exports.json | Adds useFocusedItem to exports |
| docs/* | Adds API documentation pages and updates sankey documentation with new components |
packages/x-charts-pro/src/SankeyChart/seriesConfig/keyboardFocusHandler.ts
Outdated
Show resolved
Hide resolved
packages/x-charts-pro/src/SankeyChart/seriesConfig/keyboardFocusHandler.ts
Outdated
Show resolved
Hide resolved
packages/x-charts-pro/src/SankeyChart/seriesConfig/keyboardFocusHandler.ts
Outdated
Show resolved
Hide resolved
| import type { KeyboardFocusHandler, FocusedItemUpdater } from '@mui/x-charts/internals'; | ||
|
|
||
| // ==================================================================================================================== | ||
| // | ||
| // Info: This files use node.layer and not node.depth to navigate between nodes on the same level. | ||
| // | ||
| // - depth is the graph depth, starting at 0 on the left, increasing by 1 for each link to the right. | ||
| // - layer is the visual level of the node. It takes into consideration the node alignment (left/right/justify/center) | ||
| // | ||
| // ==================================================================================================================== | ||
|
|
||
| const getFirstNode: FocusedItemUpdater<'sankey', 'sankey'> = (_, state) => { | ||
| // If no node is defined, find the first node with layer = 0 | ||
| const seriesId = state.series.defaultizedSeries.sankey?.seriesOrder[0]; | ||
| if (!seriesId || !state.series.defaultizedSeries.sankey) { | ||
| return null; | ||
| } | ||
| const series = state.series.defaultizedSeries.sankey.series[seriesId]; | ||
|
|
||
| if (series.data.nodes.length > 0) { | ||
| const index = series.data.nodes.findIndex((node) => node.layer === 0); | ||
| return { seriesId, type: 'sankey', subType: 'node', nodeId: series.data.nodes[index].id }; | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| const getNodeToNode = | ||
| (step: -1 | 1): FocusedItemUpdater<'sankey', 'sankey'> => | ||
| (currentItem, state) => { | ||
| if (currentItem && currentItem.subType === 'node') { | ||
| const data = state.series.defaultizedSeries.sankey?.series[currentItem.seriesId].data; | ||
| const currentNodeIndex = data?.nodes.findIndex((node) => node.id === currentItem.nodeId); | ||
|
|
||
| if (currentNodeIndex === undefined || currentNodeIndex < 0 || !data) { | ||
| return getFirstNode(null, state); | ||
| } | ||
|
|
||
| const currentNode = data.nodes[currentNodeIndex]; | ||
| for (let i = 1; i <= data.nodes.length; i += 1) { | ||
| const index = (data.nodes.length + currentNodeIndex + step * i) % data.nodes.length; | ||
| if (data.nodes[index].layer === currentNode.layer) { | ||
| return { | ||
| seriesId: currentItem.seriesId, | ||
| type: 'sankey', | ||
| subType: 'node', | ||
| nodeId: data.nodes[index].id, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| // If we fail, we fallback on the fist node | ||
| return getFirstNode(null, state); | ||
| }; | ||
|
|
||
| const getNodeToLink = | ||
| (step: 'source' | 'target'): FocusedItemUpdater<'sankey', 'sankey'> => | ||
| (currentItem, state) => { | ||
| if (currentItem && currentItem.subType === 'node') { | ||
| const data = state.series.defaultizedSeries.sankey?.series[currentItem.seriesId].data; | ||
| const currentNodeIndex = data?.nodes.findIndex((node) => node.id === currentItem.nodeId); | ||
|
|
||
| if (currentNodeIndex === undefined || currentNodeIndex < 0 || !data) { | ||
| return getFirstNode(null, state); | ||
| } | ||
|
|
||
| const currentNode = data.nodes[currentNodeIndex]; | ||
|
|
||
| const links = step === 'source' ? currentNode.sourceLinks : currentNode.targetLinks; | ||
| if (links.length === 0) { | ||
| // No link in that direction, we stay where we are. | ||
| return currentItem; | ||
| } | ||
| return { | ||
| seriesId: currentItem.seriesId, | ||
| type: 'sankey', | ||
| subType: 'link', | ||
| sourceId: links[0].source.id, | ||
| targetId: links[0].target.id, | ||
| }; | ||
| } | ||
| // If we fail, we fallback on the fist node | ||
| return getFirstNode(null, state); | ||
| }; | ||
|
|
||
| const getLinkToNode = | ||
| (step: 'source' | 'target'): FocusedItemUpdater<'sankey', 'sankey'> => | ||
| (currentItem, state) => { | ||
| if (currentItem && currentItem.subType === 'link') { | ||
| const nodeId = step === 'source' ? currentItem.sourceId : currentItem.targetId; | ||
| return { seriesId: currentItem.seriesId, type: 'sankey', subType: 'node', nodeId }; | ||
| } | ||
| return getFirstNode(null, state); | ||
| }; | ||
|
|
||
| const getLinkToLink = | ||
| (step: -1 | 1): FocusedItemUpdater<'sankey', 'sankey'> => | ||
| (currentItem, state) => { | ||
| if (currentItem && currentItem.subType === 'link') { | ||
| const data = state.series.defaultizedSeries.sankey?.series[currentItem.seriesId].data; | ||
| const currentLinkIndex = data?.links.findIndex( | ||
| (link) => | ||
| link.source.id === currentItem.sourceId && link.target.id === currentItem.targetId, | ||
| ); | ||
|
|
||
| if (currentLinkIndex === undefined || currentLinkIndex < 0 || !data) { | ||
| return getFirstNode(null, state); | ||
| } | ||
|
|
||
| for (let i = 1; i <= data.links.length; i += 1) { | ||
| const index = (data.links.length + currentLinkIndex + step * i) % data.links.length; | ||
| if (data.links[index].source.id === currentItem.sourceId) { | ||
| return { | ||
| seriesId: currentItem.seriesId, | ||
| type: 'sankey', | ||
| subType: 'link', | ||
| sourceId: data.links[index].source.id, | ||
| targetId: data.links[index].target.id, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return getFirstNode(null, state); | ||
| }; | ||
|
|
||
| const keyboardFocusHandler: KeyboardFocusHandler<'sankey', 'sankey'> = | ||
| (event) => (currentItem, state) => { | ||
| const isLink = currentItem?.subType === 'link'; | ||
|
|
||
| switch (event.key) { | ||
| case 'ArrowDown': | ||
| return isLink ? getLinkToLink(1)(currentItem, state) : getNodeToNode(1)(currentItem, state); | ||
| case 'ArrowUp': | ||
| return isLink | ||
| ? getLinkToLink(-1)(currentItem, state) | ||
| : getNodeToNode(-1)(currentItem, state); | ||
| case 'ArrowRight': | ||
| return isLink | ||
| ? getLinkToNode('target')(currentItem, state) | ||
| : getNodeToLink('source')(currentItem, state); | ||
| case 'ArrowLeft': | ||
| return isLink | ||
| ? getLinkToNode('source')(currentItem, state) | ||
| : getNodeToLink('target')(currentItem, state); | ||
|
|
||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| export default keyboardFocusHandler; |
There was a problem hiding this comment.
The keyboard navigation logic lacks test coverage. The repository has comprehensive testing for similar components (e.g., FunnelChart, Heatmap) and hooks (e.g., useFunnelSeries, useSankeySeries). Consider adding tests to verify:
- Navigation between nodes in the same layer (up/down arrows)
- Navigation between links with the same source (up/down arrows)
- Navigation from nodes to source/target links (left/right arrows)
- Navigation from links to source/target nodes (left/right arrows)
- Edge cases like empty nodes, no links in a direction, and wrapping behavior
There was a problem hiding this comment.
@copilot add a new commit to apply changes based on this feedback
packages/x-charts-pro/src/SankeyChart/seriesConfig/keyboardFocusHandler.ts
Show resolved
Hide resolved
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
packages/x-charts-pro/src/SankeyChart/seriesConfig/keyboardFocusHandler.ts
Outdated
Show resolved
Hide resolved
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
JCQuintas
left a comment
There was a problem hiding this comment.
Overall I agree with the behaviour.
If we get issues with this, we could try other options
- A more "stateful one", where if moving backwards then up/down should iterate over the target's links.
- One that takes into account the "link color" keyword? If
linkOptions.color: 'target'then iterate over the targets links.
| } | ||
| }; | ||
|
|
||
| export default keyboardFocusHandler; |
There was a problem hiding this comment.
We tend to export functions instead
There was a problem hiding this comment.
I don't understand you point.
For now all the similar files are doing
const keyboardFocusHandler: KeyboardFocusHandler<'pie', 'pie'> = (event) => ...
export default keyboardFocusHandler| TSeriesType extends ChartSeriesType = ChartSeriesType, | ||
| > = MergeSignaturesProperty<[...ChartCorePluginSignatures<TSeriesType>, ...TSignatures], 'state'> & |
There was a problem hiding this comment.
Snhould these type changes be in a separate pr?
There was a problem hiding this comment.
I could extract it if you prefer. Just that it does not make lot of sens to add this generic in a dedicated PR since it would not be used
There was a problem hiding this comment.
No need, only if you expect issues out of it 😅
|
The algorithm seems to work well in most cases, but here's one where it fails, probably because we're checking that the source and target are the same, but in this case there's an intermediate node. Here I'm pressing Arrow Down / Arrow Up on every node and link: Screen.Recording.2026-01-14.at.10.31.12.movIt's this demo |
bernardobelchior
left a comment
There was a problem hiding this comment.
Sorry, I forgot to submit my comments 😅
| return null; | ||
| } | ||
|
|
||
| const link = layout?.links.find( |
There was a problem hiding this comment.
| const link = layout?.links.find( | |
| const link = layout.links.find( |
| if (!focusedItem || focusedItem.type !== 'sankey' || focusedItem.subType !== 'node' || !layout) { | ||
| return null; | ||
| } | ||
| const node = layout?.nodes.find(({ id }) => id === focusedItem.nodeId); |
There was a problem hiding this comment.
| const node = layout?.nodes.find(({ id }) => id === focusedItem.nodeId); | |
| const node = layout.nodes.find(({ id }) => id === focusedItem.nodeId); |
| width={x1 - x0} | ||
| height={y1 - y0} | ||
| fill="none" | ||
| stroke={(theme.vars ?? theme).palette.text.primary} |
There was a problem hiding this comment.
Should we always use (theme.vars ?? theme)? I think we might have some places where we don't do this
| title: React Sankey chart | ||
| productId: x-charts | ||
| components: SankeyChart, SankeyPlot, SankeyTooltip, SankeyTooltipContent, SankeyDataProvider | ||
| components: SankeyChart, SankeyPlot, SankeyTooltip, SankeyTooltipContent, SankeyDataProvider, FocusedSankeyNode, FocusedSankeyLink |
There was a problem hiding this comment.
Should we explain in the docs that these components should be added to the chart when using composition?
Yes
To the link A-B IMO
Which one do you mean? |
This MR contains the following updates: | Package | Type | Update | Change | OpenSSF | |---|---|---|---|---| | [@mui/x-charts](https://mui.com/x/react-charts/) ([source](https://github.com/mui/mui-x/tree/HEAD/packages/x-charts)) | dependencies | minor | [`8.24.0` → `8.25.0`](https://renovatebot.com/diffs/npm/@mui%2fx-charts/8.24.0/8.25.0) | [](https://securityscorecards.dev/viewer/?uri=github.com/mui/mui-x) | | [@mui/x-tree-view](https://mui.com/x/react-tree-view/) ([source](https://github.com/mui/mui-x/tree/HEAD/packages/x-tree-view)) | dependencies | minor | [`8.24.0` → `8.25.0`](https://renovatebot.com/diffs/npm/@mui%2fx-tree-view/8.24.0/8.25.0) | [](https://securityscorecards.dev/viewer/?uri=github.com/mui/mui-x) | | [prettier](https://prettier.io) ([source](https://github.com/prettier/prettier)) | devDependencies | minor | [`3.7.4` → `3.8.0`](https://renovatebot.com/diffs/npm/prettier/3.7.4/3.8.0) | [](https://securityscorecards.dev/viewer/?uri=github.com/prettier/prettier) | --- ### Release Notes <details> <summary>mui/mui-x (@​mui/x-charts)</summary> ### [`v8.25.0`](https://github.com/mui/mui-x/blob/HEAD/CHANGELOG.md#8250) [Compare Source](mui/mui-x@v8.24.0...v8.25.0) <!-- generated comparing v8.24.0..master --> *Jan 14, 2026* We'd like to extend a big thank you to the 8 contributors who made this release possible. Here are some highlights ✨: - 📊 The Chart legend now has an option that enables [click to toggle visibility](https://mui.com/x/react-charts/legend/#toggle-visibility) of series.  - 🐞 Bugfixes - 📚 Documentation improvements The following team members contributed to this release: [@​alexfauquette](https://github.com/alexfauquette), [@​arminmeh](https://github.com/arminmeh), [@​bernardobelchior](https://github.com/bernardobelchior), [@​cherniavskii](https://github.com/cherniavskii), [@​JCQuintas](https://github.com/JCQuintas), [@​mapache-salvaje](https://github.com/mapache-salvaje), [@​rita-codes](https://github.com/rita-codes), [@​Janpot](https://github.com/Janpot) ##### Data Grid ##### `@mui/[email protected]` Internal changes. ##### `@mui/[email protected]` [](https://mui.com/r/x-pro-svg-link "Pro plan") Same changes as in `@mui/[email protected]`. ##### `@mui/[email protected]` [](https://mui.com/r/x-premium-svg-link "Premium plan") Same changes as in `@mui/[email protected]`. ##### Date and Time Pickers ##### `@mui/[email protected]` Internal changes. ##### `@mui/[email protected]` [](https://mui.com/r/x-pro-svg-link "Pro plan") Same changes as in `@mui/[email protected]`. ##### Charts ##### `@mui/[email protected]` - \[charts] Add Legend actions ([#​20404](mui/mui-x#20404)) [@​JCQuintas](https://github.com/JCQuintas) - \[charts] Add `initialHiddenItems` prop to set initial state ([#​20894](mui/mui-x#20894)) [@​JCQuintas](https://github.com/JCQuintas) - \[charts] Control the item tooltip ([#​20617](mui/mui-x#20617)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts] Export plugins from premium ([#​20866](mui/mui-x#20866)) [@​JCQuintas](https://github.com/JCQuintas) - \[charts] Fix node anchor on iOS ([#​20848](mui/mui-x#20848)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts] Fix test inconsistency in charts ([#​20907](mui/mui-x#20907)) [@​JCQuintas](https://github.com/JCQuintas) - \[charts] Revert `touch-action: pan-y` removal when zoom is disabled ([#​20852](mui/mui-x#20852)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[charts] Use React event handler to detect pointer type ([#​20849](mui/mui-x#20849)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts] Enable keyboard navigation in radar chart ([#​20765](mui/mui-x#20765)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts] Fix tooltip position for stacked line series ([#​20901](mui/mui-x#20901)) [@​alexfauquette](https://github.com/alexfauquette) ##### `@mui/[email protected]` [](https://mui.com/r/x-pro-svg-link "Pro plan") Same changes as in `@mui/[email protected]`, plus: - \[charts-pro] Add keyboard navigation to funnel ([#​20766](mui/mui-x#20766)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts-pro] Add keyboard navigation to heatmap ([#​20786](mui/mui-x#20786)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts-pro] Add keyboard navigation to sankey ([#​20777](mui/mui-x#20777)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts-pro] Prefer global pointer interaction tracker in Heatmap ([#​20697](mui/mui-x#20697)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[charts-pro] Support composition for Sankey ([#​20604](mui/mui-x#20604)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts-pro] Fix crash when two same-direction axes have a zoom preview ([#​20916](mui/mui-x#20916)) [@​bernardobelchior](https://github.com/bernardobelchior) ##### `@mui/[email protected]` [](https://mui.com/r/x-premium-svg-link "Premium plan") Same changes as in `@mui/[email protected]`, plus: - \[charts-premium] Add `ChartContainerPremium` ([#​20910](mui/mui-x#20910)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[charts-premium] Fix `ChartDataProviderPremium` tests ([#​20868](mui/mui-x#20868)) [@​bernardobelchior](https://github.com/bernardobelchior) ##### Tree View ##### `@mui/[email protected]` Internal changes. ##### `@mui/[email protected]` [](https://mui.com/r/x-pro-svg-link "Pro plan") Same changes as in `@mui/[email protected]`. ##### Codemod ##### `@mui/[email protected]` Internal changes. ##### Docs - \[docs] Fix Waterfall Chart documentation badge from Pro to Premium ([#​20888](mui/mui-x#20888)) [@​Copilot](https://github.com/Copilot) - \[docs] Fix broken links on Data Grid Editing sub-pages ([#​20911](mui/mui-x#20911)) [@​arminmeh](https://github.com/arminmeh) - \[docs] Increase chart axis size in docs to fit in Ubuntu Firefox ([#​20844](mui/mui-x#20844)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[docs] Simplify heatmap zoom demo ([#​20851](mui/mui-x#20851)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[docs] Revise the Charts Composition doc ([#​20032](mui/mui-x#20032)) [@​mapache-salvaje](https://github.com/mapache-salvaje) - \[docs] Revise the Charts Localization doc ([#​20800](mui/mui-x#20800)) [@​mapache-salvaje](https://github.com/mapache-salvaje) - \[docs] Revise the Charts Stacking doc ([#​20830](mui/mui-x#20830)) [@​mapache-salvaje](https://github.com/mapache-salvaje) - \[docs] Fix broken links ([#​20914](mui/mui-x#20914)) [@​Janpot](https://github.com/Janpot) ##### Core - \[code-infra] Fix `material-ui/disallow-react-api-in-server-components` ([#​20909](mui/mui-x#20909)) [@​JCQuintas](https://github.com/JCQuintas) - \[code-infra] Prepare for v9 ([#​20860](mui/mui-x#20860)) [@​JCQuintas](https://github.com/JCQuintas) - \[code-infra] Update codeowners ([#​20886](mui/mui-x#20886)) [@​JCQuintas](https://github.com/JCQuintas) - \[internal] Remove local Claude settings from the repo ([#​20853](mui/mui-x#20853)) [@​cherniavskii](https://github.com/cherniavskii) </details> <details> <summary>prettier/prettier (prettier)</summary> ### [`v3.8.0`](https://github.com/prettier/prettier/blob/HEAD/CHANGELOG.md#380) [Compare Source](prettier/prettier@3.7.4...3.8.0) [diff](prettier/prettier@3.7.4...3.8.0) 🔗 [Release Notes](https://prettier.io/blog/2026/01/14/3.8.0) </details> --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi44NC4yIiwidXBkYXRlZEluVmVyIjoiNDIuODQuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=--> See merge request swiss-armed-forces/cyber-command/cea/loom!253 Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
chore(deps): update frontend dependencies (minor) (minor) This MR contains the following updates: | Package | Type | Update | Change | OpenSSF | |---|---|---|---|---| | [@mui/x-charts](https://mui.com/x/react-charts/) ([source](https://github.com/mui/mui-x/tree/HEAD/packages/x-charts)) | dependencies | minor | [`8.24.0` → `8.25.0`](https://renovatebot.com/diffs/npm/@mui%2fx-charts/8.24.0/8.25.0) | [](https://securityscorecards.dev/viewer/?uri=github.com/mui/mui-x) | | [@mui/x-tree-view](https://mui.com/x/react-tree-view/) ([source](https://github.com/mui/mui-x/tree/HEAD/packages/x-tree-view)) | dependencies | minor | [`8.24.0` → `8.25.0`](https://renovatebot.com/diffs/npm/@mui%2fx-tree-view/8.24.0/8.25.0) | [](https://securityscorecards.dev/viewer/?uri=github.com/mui/mui-x) | | [prettier](https://prettier.io) ([source](https://github.com/prettier/prettier)) | devDependencies | minor | [`3.7.4` → `3.8.0`](https://renovatebot.com/diffs/npm/prettier/3.7.4/3.8.0) | [](https://securityscorecards.dev/viewer/?uri=github.com/prettier/prettier) | --- ### Release Notes <details> <summary>mui/mui-x (@​mui/x-charts)</summary> ### [`v8.25.0`](https://github.com/mui/mui-x/blob/HEAD/CHANGELOG.md#8250) [Compare Source](mui/mui-x@v8.24.0...v8.25.0) <!-- generated comparing v8.24.0..master --> *Jan 14, 2026* We'd like to extend a big thank you to the 8 contributors who made this release possible. Here are some highlights ✨: - 📊 The Chart legend now has an option that enables [click to toggle visibility](https://mui.com/x/react-charts/legend/#toggle-visibility) of series.  - 🐞 Bugfixes - 📚 Documentation improvements The following team members contributed to this release: [@​alexfauquette](https://github.com/alexfauquette), [@​arminmeh](https://github.com/arminmeh), [@​bernardobelchior](https://github.com/bernardobelchior), [@​cherniavskii](https://github.com/cherniavskii), [@​JCQuintas](https://github.com/JCQuintas), [@​mapache-salvaje](https://github.com/mapache-salvaje), [@​rita-codes](https://github.com/rita-codes), [@​Janpot](https://github.com/Janpot) ##### Data Grid ##### `@mui/[email protected]` Internal changes. ##### `@mui/[email protected]` [](https://mui.com/r/x-pro-svg-link "Pro plan") Same changes as in `@mui/[email protected]`. ##### `@mui/[email protected]` [](https://mui.com/r/x-premium-svg-link "Premium plan") Same changes as in `@mui/[email protected]`. ##### Date and Time Pickers ##### `@mui/[email protected]` Internal changes. ##### `@mui/[email protected]` [](https://mui.com/r/x-pro-svg-link "Pro plan") Same changes as in `@mui/[email protected]`. ##### Charts ##### `@mui/[email protected]` - \[charts] Add Legend actions ([#​20404](mui/mui-x#20404)) [@​JCQuintas](https://github.com/JCQuintas) - \[charts] Add `initialHiddenItems` prop to set initial state ([#​20894](mui/mui-x#20894)) [@​JCQuintas](https://github.com/JCQuintas) - \[charts] Control the item tooltip ([#​20617](mui/mui-x#20617)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts] Export plugins from premium ([#​20866](mui/mui-x#20866)) [@​JCQuintas](https://github.com/JCQuintas) - \[charts] Fix node anchor on iOS ([#​20848](mui/mui-x#20848)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts] Fix test inconsistency in charts ([#​20907](mui/mui-x#20907)) [@​JCQuintas](https://github.com/JCQuintas) - \[charts] Revert `touch-action: pan-y` removal when zoom is disabled ([#​20852](mui/mui-x#20852)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[charts] Use React event handler to detect pointer type ([#​20849](mui/mui-x#20849)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts] Enable keyboard navigation in radar chart ([#​20765](mui/mui-x#20765)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts] Fix tooltip position for stacked line series ([#​20901](mui/mui-x#20901)) [@​alexfauquette](https://github.com/alexfauquette) ##### `@mui/[email protected]` [](https://mui.com/r/x-pro-svg-link "Pro plan") Same changes as in `@mui/[email protected]`, plus: - \[charts-pro] Add keyboard navigation to funnel ([#​20766](mui/mui-x#20766)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts-pro] Add keyboard navigation to heatmap ([#​20786](mui/mui-x#20786)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts-pro] Add keyboard navigation to sankey ([#​20777](mui/mui-x#20777)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts-pro] Prefer global pointer interaction tracker in Heatmap ([#​20697](mui/mui-x#20697)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[charts-pro] Support composition for Sankey ([#​20604](mui/mui-x#20604)) [@​alexfauquette](https://github.com/alexfauquette) - \[charts-pro] Fix crash when two same-direction axes have a zoom preview ([#​20916](mui/mui-x#20916)) [@​bernardobelchior](https://github.com/bernardobelchior) ##### `@mui/[email protected]` [](https://mui.com/r/x-premium-svg-link "Premium plan") Same changes as in `@mui/[email protected]`, plus: - \[charts-premium] Add `ChartContainerPremium` ([#​20910](mui/mui-x#20910)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[charts-premium] Fix `ChartDataProviderPremium` tests ([#​20868](mui/mui-x#20868)) [@​bernardobelchior](https://github.com/bernardobelchior) ##### Tree View ##### `@mui/[email protected]` Internal changes. ##### `@mui/[email protected]` [](https://mui.com/r/x-pro-svg-link "Pro plan") Same changes as in `@mui/[email protected]`. ##### Codemod ##### `@mui/[email protected]` Internal changes. ##### Docs - \[docs] Fix Waterfall Chart documentation badge from Pro to Premium ([#​20888](mui/mui-x#20888)) [@​Copilot](https://github.com/Copilot) - \[docs] Fix broken links on Data Grid Editing sub-pages ([#​20911](mui/mui-x#20911)) [@​arminmeh](https://github.com/arminmeh) - \[docs] Increase chart axis size in docs to fit in Ubuntu Firefox ([#​20844](mui/mui-x#20844)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[docs] Simplify heatmap zoom demo ([#​20851](mui/mui-x#20851)) [@​bernardobelchior](https://github.com/bernardobelchior) - \[docs] Revise the Charts Composition doc ([#​20032](mui/mui-x#20032)) [@​mapache-salvaje](https://github.com/mapache-salvaje) - \[docs] Revise the Charts Localization doc ([#​20800](mui/mui-x#20800)) [@​mapache-salvaje](https://github.com/mapache-salvaje) - \[docs] Revise the Charts Stacking doc ([#​20830](mui/mui-x#20830)) [@​mapache-salvaje](https://github.com/mapache-salvaje) - \[docs] Fix broken links ([#​20914](mui/mui-x#20914)) [@​Janpot](https://github.com/Janpot) ##### Core - \[code-infra] Fix `material-ui/disallow-react-api-in-server-components` ([#​20909](mui/mui-x#20909)) [@​JCQuintas](https://github.com/JCQuintas) - \[code-infra] Prepare for v9 ([#​20860](mui/mui-x#20860)) [@​JCQuintas](https://github.com/JCQuintas) - \[code-infra] Update codeowners ([#​20886](mui/mui-x#20886)) [@​JCQuintas](https://github.com/JCQuintas) - \[internal] Remove local Claude settings from the repo ([#​20853](mui/mui-x#20853)) [@​cherniavskii](https://github.com/cherniavskii) </details> <details> <summary>prettier/prettier (prettier)</summary> ### [`v3.8.0`](https://github.com/prettier/prettier/blob/HEAD/CHANGELOG.md#380) [Compare Source](prettier/prettier@3.7.4...3.8.0) [diff](prettier/prettier@3.7.4...3.8.0) 🔗 [Release Notes](https://prettier.io/blog/2026/01/14/3.8.0) </details> --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi44NC4yIiwidXBkYXRlZEluVmVyIjoiNDIuODQuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=--> See merge request swiss-armed-forces/cyber-command/cea/loom!253 Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com> Co-authored-by: open-source Pipeline <group_90701827_bot_ed04ae348bc5f40af9966fb8b6867e99@noreply.gitlab.com>

The sankey now has a keyboard navigation.
Behavior
for node:
for links:
Decisions up to discussion
The link up/down are limited to the same source node. It might look broken when layers are simple.
Here for example if you focus a yellow link you will only be able to move to the other yellow link. But user could expect to also navigate to the other links of the same level.
But if we consider more complex use cases like this one, it not that clear to know which link should be included or not in the loop and in which order we navigate across them.
So basically the proposed behavior is a bit restrictive but fully predictable