Skip to content

fix: resolve keyboard navigation and tooltip issues for Pie charts (#6921)#7140

Merged
PavelVanecek merged 3 commits into
recharts:mainfrom
olagokemills:fix/pie-keyboard-navigation-6921
Mar 22, 2026
Merged

fix: resolve keyboard navigation and tooltip issues for Pie charts (#6921)#7140
PavelVanecek merged 3 commits into
recharts:mainfrom
olagokemills:fix/pie-keyboard-navigation-6921

Conversation

@olagokemills

@olagokemills olagokemills commented Mar 15, 2026

Copy link
Copy Markdown
Contributor
  • Changed keyboardEventsMiddleware to dynamically use selectTooltipEventType instead of hardcoding 'axis'.
  • Updated data length guard to fallback to displayedData.length when tooltipTicks is empty.
  • Removed restrictive coordinate check in focusAction that prevented Pie charts from initializing focus.
  • Fixes keyboard navigation for Pie charts (and other charts using the 'item' tooltip type).
  • All existing keyboard navigation tests still pass.

Fixes #6921

Description

The keyboard navigation middleware was previously hardcoded to use the 'axis' tooltip event type and relied strictly on tooltipTicks and a resolved coordinate. This worked well for Cartesian charts but failed for PieChart and other polar/non-axis charts which use the 'item' tooltip event type and calculate coordinates dynamically.

This PR updates the middleware to:

  1. Fetch the correct tooltipEventType from the state instead of defaulting to 'axis'.
  2. Fallback to using displayedData.length for upper-bound checks when tooltipTicks are not present.
  3. Remove the premature coordinate guard during the focus phase, allowing the tooltip state to initialize properly even for charts that resolve their coordinates later.

Related Issue

Fixes #6921

Motivation and Context

Keyboard users were unable to navigate PieChart components or view tooltips when rendering with the accessibilityLayer enabled. This change ensures that the accessibility features work seamlessly across all chart types, not just Cartesian ones, improving the overall a11y support in Recharts.

How Has This Been Tested?

  • Verified locally by rendering a PieChart with accessibilityLayer, navigating via the Tab key to focus, and using Left/Right Arrow keys to navigate between sectors.
  • Ensured tooltips display correctly upon focusing a Pie sector.
  • Ran the existing comprehensive test suite (npm test) and verified that all existing keyboard navigation and interaction tests continue to pass without regression.

Screenshots (if appropriate):

(Feel free to add a quick screen recording here showing you tabbing through a Pie chart if you have one!)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • I have added a storybook story or VR test, or extended an existing story or VR test to show my changes

Summary by CodeRabbit

  • Bug Fixes

    • Keyboard navigation now respects the active tooltip event type for coordinate selection and focus movement.
    • Improved boundary handling and early exits to prevent out-of-range or empty-data navigation.
    • Tooltip behavior on Enter/arrow keys and initial focus aligned with tooltip-driven navigation logic.
  • Tests

    • Updated accessibility/keyboard tests to assert tooltip visibility and sector navigation (focus, arrow keys, clamping, blur).

…#6921)

- Changed keyboardEventsMiddleware to use selectTooltipEventType instead of hardcoding 'axis'
- Fixes keyboard navigation for Pie charts which use 'item' tooltip type
- All existing keyboard navigation tests still pass

Partially fixes recharts#6921 - keyboard navigation now uses correct event type for all chart types.
Full resolution requires additional work to initialize tooltip state on focus without defaultIndex.
@coderabbitai

coderabbitai Bot commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Replaces hard-coded 'axis' with a selector-derived tooltipEventType in keyboardEventsMiddleware; uses tooltipEventType and displayedData/tooltipTicks for Enter/arrow-key coordinate selection and bounds checks; adds zero-length-data guard; tests updated to assert keyboard-driven tooltip focus and arrow navigation for Pie/Funnel charts.

Changes

Cohort / File(s) Summary
Keyboard navigation middleware
src/state/keyboardEventsMiddleware.ts
Introduce and use selectTooltipEventType to derive tooltipEventType from state instead of literal 'axis'; compute displayedData/tooltipTicks-based dataLength, early-return on zero-length, and update Enter/Arrow navigation coordinate selection and next-index bounds logic.
Tooltip payload combiner
src/state/selectors/combiners/combineTooltipPayloadConfigurations.ts
Extend single-item fallback condition to also apply when tooltip is active via keyboard (tooltipState.keyboardInteraction.active) in addition to defaultIndex.
Tests — accessibility/keyboard
test/chart/AccessibilityLayer.spec.tsx
Replace PieChart "no tooltip" test with focused/arrow-key navigation tests; update FunnelChart tests to assert tooltip shows first segment on focus and navigates with arrow keys; add blur behavior test.
New selector export
src/state/selectors/selectTooltipEventType.ts
Add selectTooltipEventType selector used by middleware to determine tooltip event type from state.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant DOM as Chart DOM
  participant KB as KeyboardEventsMiddleware
  participant Sel as Selectors (selectTooltipEventType, others)
  participant Store as Redux Store
  participant Tooltip as Tooltip renderer

  User->>DOM: keydown (Enter / Arrow)
  DOM->>KB: keyboard event
  KB->>Sel: selectTooltipEventType(state)
  KB->>Store: read displayedData, tooltipTicks
  KB->>KB: compute dataLength, nextIndex, coordinate (using tooltipEventType)
  KB->>Store: dispatch activeCoordinate / activeIndex
  Store->>Tooltip: update tooltip payload/position
  Tooltip->>DOM: render tooltip
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • PavelVanecek
  • ckifer
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing keyboard navigation and tooltip issues for Pie charts, which directly corresponds to the PR's core objective.
Description check ✅ Passed The description covers all required template sections with sufficient detail, including motivation, context, testing approach, and issue reference, though test coverage checklist items remain unchecked.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can validate your CodeRabbit configuration file in your editor.

If your editor has YAML language server, you can enable auto-completion and validation by adding # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json at the top of your CodeRabbit configuration file.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/state/keyboardEventsMiddleware.ts (1)

71-99: Add regression tests for the new keyboard paths.

Please add focused tests for: (1) empty tooltipTicks with non-empty displayedData, and (2) focus initialization for item tooltip charts (Pie). This change is behavior-critical and currently relies on manual verification.

As per coding guidelines: src/**/*.{ts,tsx}: Aim for 100% unit test code coverage when writing new code.

Also applies to: 150-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/keyboardEventsMiddleware.ts` around lines 71 - 99, Add unit tests
for keyboardEventsMiddleware to cover the two missing keyboard paths: (1) when
tooltipTicks is empty but selectTooltipDisplayedData returns a non-empty array
ensure arrow key navigation uses displayedData length and prevents out-of-bounds
movement, and (2) focus initialization for item tooltip charts (e.g., Pie) where
pressing Enter sets keyboardInteraction.activeIndex/activeCoordinate via
selectCoordinateForDefaultIndex; exercise selectors selectTooltipDisplayedData,
selectTooltipEventType, selectCoordinateForDefaultIndex and the dispatched
action setKeyboardInteraction to assert correct payloads and that behavior
differs for 'hover' vs item/tooltips.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/state/keyboardEventsMiddleware.ts`:
- Around line 94-95: The computation of dataLength in
keyboardEventsMiddleware.ts wrongly uses the nullish coalescing operator
(tooltipTicks?.length ?? displayedData.length) so an empty tooltipTicks (length
0) prevents falling back to displayedData; change it to use a truthy fallback
such as (tooltipTicks?.length || displayedData.length) or an explicit
conditional (tooltipTicks && tooltipTicks.length > 0 ? tooltipTicks.length :
displayedData.length) so nextIndex navigation uses displayedData when
tooltipTicks is empty; update the line where dataLength is defined and keep
references to tooltipTicks, displayedData, and nextIndex.
- Around line 72-76: Remove the unused tooltipEventType parameter from the
selectCoordinateForDefaultIndex selector and all its call sites (currently
passing tooltipEventType in calls near keyboard handling and other usages);
update the selector's combiner function combineCoordinateForDefaultIndex to drop
the tooltipEventType argument and adjust its parameter list and internal
references accordingly, and then remove the extra argument from every
selectCoordinateForDefaultIndex(...) invocation so the selector only receives
the actual state-derived inputs it uses (chart dimensions, layout, axis ticks,
tooltip settings).

---

Nitpick comments:
In `@src/state/keyboardEventsMiddleware.ts`:
- Around line 71-99: Add unit tests for keyboardEventsMiddleware to cover the
two missing keyboard paths: (1) when tooltipTicks is empty but
selectTooltipDisplayedData returns a non-empty array ensure arrow key navigation
uses displayedData length and prevents out-of-bounds movement, and (2) focus
initialization for item tooltip charts (e.g., Pie) where pressing Enter sets
keyboardInteraction.activeIndex/activeCoordinate via
selectCoordinateForDefaultIndex; exercise selectors selectTooltipDisplayedData,
selectTooltipEventType, selectCoordinateForDefaultIndex and the dispatched
action setKeyboardInteraction to assert correct payloads and that behavior
differs for 'hover' vs item/tooltips.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e89b0c8-c0ab-4443-bd2f-7f7aa82e0fd0

📥 Commits

Reviewing files that changed from the base of the PR and between 681daea and ff66d6e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • src/state/keyboardEventsMiddleware.ts

Comment thread src/state/keyboardEventsMiddleware.ts
Comment thread src/state/keyboardEventsMiddleware.ts Outdated

@coderabbitai coderabbitai Bot 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.

♻️ Duplicate comments (1)
src/state/keyboardEventsMiddleware.ts (1)

131-132: ⚠️ Potential issue | 🟠 Major

dataLength fallback is still wrong when tooltipTicks is an empty array.

The nullish coalescing operator (??) only replaces null/undefined, not 0. When tooltipTicks is [], tooltipTicks?.length returns 0, and 0 ?? displayedData.length evaluates to 0—not displayedData.length. This prevents keyboard navigation even when displayedData has items.

Proposed fix
-        const dataLength = tooltipTicks?.length ?? displayedData.length;
+        const dataLength = tooltipTicks?.length || displayedData.length;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/keyboardEventsMiddleware.ts` around lines 131 - 132, The fallback
for dataLength incorrectly treats an empty tooltipTicks array as valid because
`??` doesn't replace 0; update the expression to fall back when tooltipTicks is
missing or empty by using an explicit check: replace `const dataLength =
tooltipTicks?.length ?? displayedData.length;` with a conditional that uses
`tooltipTicks?.length > 0 ? tooltipTicks.length : displayedData.length` so
navigation uses displayedData when tooltipTicks is an empty array; keep the
surrounding logic that checks `dataLength === 0 || nextIndex >= dataLength ||
nextIndex < 0` unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/state/keyboardEventsMiddleware.ts`:
- Around line 131-132: The fallback for dataLength incorrectly treats an empty
tooltipTicks array as valid because `??` doesn't replace 0; update the
expression to fall back when tooltipTicks is missing or empty by using an
explicit check: replace `const dataLength = tooltipTicks?.length ??
displayedData.length;` with a conditional that uses `tooltipTicks?.length > 0 ?
tooltipTicks.length : displayedData.length` so navigation uses displayedData
when tooltipTicks is an empty array; keep the surrounding logic that checks
`dataLength === 0 || nextIndex >= dataLength || nextIndex < 0` unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3896102c-648d-4184-ad79-3686133bffc7

📥 Commits

Reviewing files that changed from the base of the PR and between ff66d6e and 837bb8f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • src/state/keyboardEventsMiddleware.ts

@marilia-gb marilia-gb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thank you!!

@PavelVanecek

Copy link
Copy Markdown
Collaborator

We're going to need a test for this new code - to prevent regressions.

Also please double check the robot comment

@codecov

codecov Bot commented Mar 17, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 22.85kB (0.45%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
recharts/bundle-cjs 1.3MB 6.0kB (0.46%) ⬆️
recharts/bundle-es6 1.13MB 5.53kB (0.49%) ⬆️
recharts/bundle-umd 546.79kB 993 bytes (0.18%) ⬆️
recharts/bundle-treeshaking-polar 448.04kB 4.96kB (1.12%) ⬆️
recharts/bundle-treeshaking-treemap* 349.65kB 476 bytes (0.14%) ⬆️
recharts/bundle-treeshaking-cartesian 643.64kB 4.88kB (0.76%) ⬆️

ℹ️ *Bundle size includes cached data from a previous commit

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-treeshaking-polar

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 4.96kB 448.04kB 1.12%
view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
state/selectors/axisSelectors.js 3.3kB 69.25kB 5.0%
state/selectors/tooltipSelectors.js 309 bytes 18.22kB 1.73%
state/keyboardEventsMiddleware.js 482 bytes 7.86kB 6.53% ⚠️
state/selectors/polarSelectors.js 52 bytes 4.94kB 1.06%
state/selectors/dataSelectors.js 1.82kB 4.23kB 75.35% ⚠️
state/selectors/combiners/combineTooltipPayloadConfigurations.js 45 bytes 2.36kB 1.95%
view changes for bundle: recharts/bundle-treeshaking-cartesian

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 4.88kB 643.64kB 0.76%
view changes for bundle: recharts/bundle-umd

Assets Changed:

Asset Name Size Change Total Size Change (%)
Recharts.js 993 bytes 546.79kB 0.18%
view changes for bundle: recharts/bundle-es6

Assets Changed:

Asset Name Size Change Total Size Change (%)
state/selectors/axisSelectors.js 3.16kB 59.23kB 5.64% ⚠️
state/selectors/tooltipSelectors.js 300 bytes 14.72kB 2.08%
state/keyboardEventsMiddleware.js 424 bytes 7.24kB 6.22% ⚠️
state/selectors/polarSelectors.js 74 bytes 3.94kB 1.92%
state/selectors/dataSelectors.js 1.52kB 3.32kB 84.9% ⚠️
state/selectors/combiners/combineTooltipPayloadConfigurations.js 45 bytes 2.15kB 2.14%
view changes for bundle: recharts/bundle-treeshaking-treemap

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 476 bytes 349.65kB 0.14%

@codecov

codecov Bot commented Mar 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.61%. Comparing base (681daea) to head (6f54728).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
src/state/keyboardEventsMiddleware.ts 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7140      +/-   ##
==========================================
+ Coverage   89.59%   89.61%   +0.02%     
==========================================
  Files         536      536              
  Lines       40401    40483      +82     
  Branches     5498     5520      +22     
==========================================
+ Hits        36197    36279      +82     
  Misses       4196     4196              
  Partials        8        8              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/chart/AccessibilityLayer.spec.tsx (1)

648-735: Align new keyboard tests with the fake-timer testing pattern.

These updated tests should use fake timers and pending-timer advancement to match the project’s Redux/requestAnimationFrame test contract.

As per coding guidelines, "Use vi.useFakeTimers() in all tests due to Redux autoBatchEnhancer dependency on timers and requestAnimationFrame" and "Call vi.runOnlyPendingTimers() to advance timers after renders when not using createSelectorTestCase helper, and avoid vi.runAllTimers() to prevent infinite loops".

Also applies to: 750-833

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/chart/AccessibilityLayer.spec.tsx` around lines 648 - 735, The new
keyboard tests (e.g., the "When chart receives focus..." / "Arrow keys navigate
between sectors" / "Arrow keys stop at boundaries" / "Tooltip closes when
PieChart loses focus" tests) must use fake timers and advance only pending
timers: at the start of each test (or in the test setup) call
vi.useFakeTimers(), then after render and after any act(...) that triggers async
timers call vi.runOnlyPendingTimers() to advance
requestAnimationFrame/Redux-related timers (do not use vi.runAllTimers());
update each test that uses getMainSurface(), svg.focus(), arrowRight(),
arrowLeft(), expectTooltipPayload(), and expectTooltipNotVisible() to include
these vi calls so the Tooltip updates deterministically.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/chart/AccessibilityLayer.spec.tsx`:
- Around line 817-820: The test stops one step short of the right-edge clamping:
after calling arrowRight(svg) and asserting tooltip shows 'Page F : 189' and no
additional mouse movements, add a second arrowRight(svg) call and repeat the
assertions to verify no further movement/clamping at the rightmost point; update
the block containing arrowRight(svg), expectTooltipPayload(container, '', ['Page
F : 189']), and expect(mockMouseMovements.mock.instances).toHaveLength(0) to
include the extra arrowRight call and identical expectations to ensure E->F
transition and then no-op at F are both validated.

---

Nitpick comments:
In `@test/chart/AccessibilityLayer.spec.tsx`:
- Around line 648-735: The new keyboard tests (e.g., the "When chart receives
focus..." / "Arrow keys navigate between sectors" / "Arrow keys stop at
boundaries" / "Tooltip closes when PieChart loses focus" tests) must use fake
timers and advance only pending timers: at the start of each test (or in the
test setup) call vi.useFakeTimers(), then after render and after any act(...)
that triggers async timers call vi.runOnlyPendingTimers() to advance
requestAnimationFrame/Redux-related timers (do not use vi.runAllTimers());
update each test that uses getMainSurface(), svg.focus(), arrowRight(),
arrowLeft(), expectTooltipPayload(), and expectTooltipNotVisible() to include
these vi calls so the Tooltip updates deterministically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d0d8e4d-3c96-4720-ad0d-8d47e0edb75b

📥 Commits

Reviewing files that changed from the base of the PR and between 837bb8f and 6f54728.

📒 Files selected for processing (3)
  • src/state/keyboardEventsMiddleware.ts
  • src/state/selectors/combiners/combineTooltipPayloadConfigurations.ts
  • test/chart/AccessibilityLayer.spec.tsx

Comment on lines 817 to 820
// Ignore right arrow when you're already at the right
arrowRight(svg);
expect(tooltip).toHaveTextContent('');
expectTooltipPayload(container, '', ['Page F : 189']);
expect(mockMouseMovements.mock.instances).toHaveLength(0);

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.

⚠️ Potential issue | 🟡 Minor

Boundary assertion is incomplete at the right edge.

This step validates E -> F, but it doesn’t verify clamping once already at F. Add one more ArrowRight assertion to prevent regressions.

✅ Suggested test patch
       // Ignore right arrow when you're already at the right
       arrowRight(svg);
       expectTooltipPayload(container, '', ['Page F : 189']);
+      arrowRight(svg);
+      expectTooltipPayload(container, '', ['Page F : 189']);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Ignore right arrow when you're already at the right
arrowRight(svg);
expect(tooltip).toHaveTextContent('');
expectTooltipPayload(container, '', ['Page F : 189']);
expect(mockMouseMovements.mock.instances).toHaveLength(0);
// Ignore right arrow when you're already at the right
arrowRight(svg);
expectTooltipPayload(container, '', ['Page F : 189']);
arrowRight(svg);
expectTooltipPayload(container, '', ['Page F : 189']);
expect(mockMouseMovements.mock.instances).toHaveLength(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/chart/AccessibilityLayer.spec.tsx` around lines 817 - 820, The test
stops one step short of the right-edge clamping: after calling arrowRight(svg)
and asserting tooltip shows 'Page F : 189' and no additional mouse movements,
add a second arrowRight(svg) call and repeat the assertions to verify no further
movement/clamping at the rightmost point; update the block containing
arrowRight(svg), expectTooltipPayload(container, '', ['Page F : 189']), and
expect(mockMouseMovements.mock.instances).toHaveLength(0) to include the extra
arrowRight call and identical expectations to ensure E->F transition and then
no-op at F are both validated.

@PavelVanecek
PavelVanecek merged commit 201d060 into recharts:main Mar 22, 2026
98 of 99 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keyboard navigation lock in Pie Chart

3 participants