Skip to content

[animations] auto disable primitives animations based on user system preferences#6956

Merged
PavelVanecek merged 19 commits intorecharts:mainfrom
cloud-walker:prefers-reduced-motion
Feb 17, 2026
Merged

[animations] auto disable primitives animations based on user system preferences#6956
PavelVanecek merged 19 commits intorecharts:mainfrom
cloud-walker:prefers-reduced-motion

Conversation

@cloud-walker
Copy link
Contributor

@cloud-walker cloud-walker commented Feb 4, 2026

Description

While working on fixing the tests for another PR, We suspected that the animations in the charts might interfere, making them flaky.

So I thought it would be cool to be able to disable them for testing sake, but they are on by default.

But prefers-reduced-motion, media query exists, so this PR aims to do a combo: auto disable recharts animations if the media query triggers, so it would impact library consumers also, and being able to disable the animations from playwright by just simulating the user preference.

Related Issue

#6955

Motivation and Context

Besides the testing purpose, its a good UX practise to let the end user decide if they want to see animations or not.

How Has This Been Tested?

Manually, and with unit tests on usePrefersReducedMotion hook

Types of changes

Its a potential "breaking change" for consumer that has not isAnimationActive explicitly set, and for end users (hopefully they would like that) that has the system preference enabled.

  • 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

  • New Features

    • Animations now respect the user's system "prefers-reduced-motion" setting for improved accessibility.
  • Refactor

    • Tooltip bounding box rewritten as a memoized functional component; related public prop types updated.
  • Documentation

    • Clarified "auto" animation behavior to note SSR disabling and reduced-motion adherence.
  • Tests

    • Added test setup and unit tests covering reduced-motion detection and behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new hook to detect prefers-reduced-motion and integrates it into animation modules and tooltip positioning; updates docs, tests, and test setup so 'auto' animation mode disables during SSR and when the user prefers reduced motion.

Changes

Cohort / File(s) Summary
Animation / Hooks
src/util/usePrefersReducedMotion.ts, src/animation/CSSTransitionAnimate.tsx, src/animation/JavascriptAnimate.tsx
Added usePrefersReducedMotion hook and updated animation components to consider prefers-reduced-motion when resolving 'auto' activation (SSR-safe).
Tooltip refactor
src/component/TooltipBoundingBox.tsx
Converted class-based TooltipBoundingBox to a memoized functional component using hooks; adjusted prop types (wrapperStyle, children) and added reduced-motion-aware transition handling.
Component docs
src/cartesian/Area.tsx, src/cartesian/Bar.tsx, src/cartesian/Funnel.tsx, src/cartesian/Line.tsx, src/cartesian/Scatter.tsx, src/chart/Treemap.tsx, src/component/Tooltip.tsx, src/polar/Pie.tsx, src/polar/Radar.tsx, src/polar/RadialBar.tsx
Updated JSDoc for isAnimationActive/'auto' to indicate animations are disabled in SSR and will respect the user's prefers-reduced-motion preference (documentation-only in most files).
Tests / Test config
vitest.config.mts, www/test/vitest.setup.ts, test/util/usePrefersReducedMotion.spec.ts
Added Vitest setup that stubs window.matchMedia, new unit tests for usePrefersReducedMotion, and configured the unit:website project to include the setup file.
Snapshots / Build lists
scripts/snapshots/es6Files.txt, scripts/snapshots/libFiles.txt, scripts/snapshots/typesFiles.txt
Added entries for the new usePrefersReducedMotion outputs to snapshot/file lists.
Package / Manifest
manifest_file, package.json
Minor manifest/package list updates related to added files.

Sequence Diagram(s)

sequenceDiagram
    participant Component as Chart Component
    participant Animate as Animate (JS/CSS)
    participant Hook as usePrefersReducedMotion
    participant Media as window.matchMedia

    Component->>Animate: Render with isAnimationActive='auto'
    Animate->>Hook: call usePrefersReducedMotion()
    Hook->>Hook: check SSR (Global.isSsr)
    alt SSR
        Hook-->>Animate: return false
    else Not SSR
        Hook->>Media: window.matchMedia('(prefers-reduced-motion: reduce)')
        Media-->>Hook: MediaQueryList (matches)
        Hook->>Hook: subscribe to change events
        Hook-->>Animate: return matches (true/false)
    end
    Animate->>Animate: compute isActive = propActive && !SSR && !prefersReducedMotion
    Animate-->>Component: provide computed animation state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ckifer
🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (93 files):

⚔️ .github/workflows/deploy-website.yml (content)
⚔️ README.md (content)
⚔️ omnidoc/verifyExamples.spec.ts (content)
⚔️ package-lock.json (content)
⚔️ scripts/snapshots/es6Files.txt (content)
⚔️ scripts/snapshots/libFiles.txt (content)
⚔️ scripts/snapshots/typesFiles.txt (content)
⚔️ src/animation/CSSTransitionAnimate.tsx (content)
⚔️ src/animation/JavascriptAnimate.tsx (content)
⚔️ src/cartesian/Area.tsx (content)
⚔️ src/cartesian/Bar.tsx (content)
⚔️ src/cartesian/CartesianAxis.tsx (content)
⚔️ src/cartesian/Funnel.tsx (content)
⚔️ src/cartesian/Line.tsx (content)
⚔️ src/cartesian/Scatter.tsx (content)
⚔️ src/cartesian/XAxis.tsx (content)
⚔️ src/cartesian/YAxis.tsx (content)
⚔️ src/chart/Treemap.tsx (content)
⚔️ src/component/Tooltip.tsx (content)
⚔️ src/component/TooltipBoundingBox.tsx (content)
⚔️ src/hooks.ts (content)
⚔️ src/index.ts (content)
⚔️ src/polar/Pie.tsx (content)
⚔️ src/polar/PolarAngleAxis.tsx (content)
⚔️ src/polar/Radar.tsx (content)
⚔️ src/polar/RadialBar.tsx (content)
⚔️ src/state/selectors/axisSelectors.ts (content)
⚔️ src/state/selectors/combiners/combineTooltipPayload.ts (content)
⚔️ src/state/selectors/combiners/combineTooltipPayloadConfigurations.ts (content)
⚔️ src/state/store.ts (content)
⚔️ src/synchronisation/useChartSynchronisation.tsx (content)
⚔️ test-vr/__snapshots__/tests/www/ActiveIndex.spec-vr.tsx-snapshots/PieChartDefaultIndex-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ActiveIndex.spec-vr.tsx-snapshots/PieChartDefaultIndex-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ActiveIndex.spec-vr.tsx-snapshots/PieChartDefaultIndex-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/CoordinateSystems.spec-vr.tsx-snapshots/AxisTickSnapExample-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/CoordinateSystems.spec-vr.tsx-snapshots/AxisTickSnapExample-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/CoordinateSystems.spec-vr.tsx-snapshots/AxisTickSnapExample-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/CoordinateSystems.spec-vr.tsx-snapshots/DataSnapExample-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/CoordinateSystems.spec-vr.tsx-snapshots/DataSnapExample-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/CoordinateSystems.spec-vr.tsx-snapshots/DataSnapExample-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/LineChartApiExamples.spec-vr.tsx-snapshots/HighlightAndZoomLineChart-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/PieChartApiExamples.spec-vr.tsx-snapshots/PieWithGradient-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/PieChartApiExamples.spec-vr.tsx-snapshots/PieWithGradient-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/PieChartApiExamples.spec-vr.tsx-snapshots/PieWithGradient-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/PieChartApiExamples.spec-vr.tsx-snapshots/TwoLevelPieChart-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/PieChartApiExamples.spec-vr.tsx-snapshots/TwoLevelPieChart-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/PieChartApiExamples.spec-vr.tsx-snapshots/TwoLevelPieChart-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/RadialBarChartApiExamples.spec-vr.tsx-snapshots/RadialBarChartClickToFocusLegendExample-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/JointLineScatterChart-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/JointLineScatterChart-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/JointLineScatterChart-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/ScatterChartWithLabels-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/ScatterChartWithLabels-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/ScatterChartWithLabels-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/SimpleScatterChart-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/SimpleScatterChart-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/SimpleScatterChart-1-webkit-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/ThreeDimScatterChart-1-chromium-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/ThreeDimScatterChart-1-firefox-linux.png (content)
⚔️ test-vr/__snapshots__/tests/www/ScatterChartApiExamples.spec-vr.tsx-snapshots/ThreeDimScatterChart-1-webkit-linux.png (content)
⚔️ test/cartesian/Bar.truncateByDomain.spec.tsx (content)
⚔️ test/cartesian/Bar/Bar.spec.tsx (content)
⚔️ test/cartesian/ErrorBar.spec.tsx (content)
⚔️ test/cartesian/ReferenceArea.spec.tsx (content)
⚔️ test/cartesian/ReferenceDot.spec.tsx (content)
⚔️ test/cartesian/ReferenceLine/ReferenceLine.spec.tsx (content)
⚔️ test/cartesian/XAxis/XAxis.barSize.spec.tsx (content)
⚔️ test/cartesian/XAxis/XAxis.brush.spec.tsx (content)
⚔️ test/cartesian/XAxis/XAxis.padding.spec.tsx (content)
⚔️ test/cartesian/XAxis/XAxis.state.spec.tsx (content)
⚔️ test/cartesian/YAxis/YAxis.spec.tsx (content)
⚔️ test/cartesian/ZAxis.spec.tsx (content)
⚔️ test/chart/AreaChart.spec.tsx (content)
⚔️ test/chart/BarChart.spec.tsx (content)
⚔️ test/component/Tooltip/Tooltip.multipleDataArrays.spec.tsx (content)
⚔️ test/component/Tooltip/Tooltip.sync.spec.tsx (content)
⚔️ test/component/Tooltip/defaultIndex.spec.tsx (content)
⚔️ test/component/Tooltip/itemSorter.spec.tsx (content)
⚔️ test/hooks/useAxisTicks.spec.tsx (content)
⚔️ test/hooks/useOffset.spec.tsx (content)
⚔️ test/polar/Pie/Pie.spec.tsx (content)
⚔️ test/state/selectors/areaSelectors.spec.tsx (content)
⚔️ test/state/selectors/axisSelectors.spec.tsx (content)
⚔️ test/state/selectors/lineSelectors.spec.tsx (content)
⚔️ test/state/selectors/pieSelectors.spec.tsx (content)
⚔️ test/state/selectors/selectAxisScale.spec.tsx (content)
⚔️ test/state/selectors/selectDisplayedData.spec.tsx (content)
⚔️ test/state/selectors/selectors.spec.tsx (content)
⚔️ vitest.config.mts (content)
⚔️ www/src/docs/apiExamples/useAxisInverseDataSnapScale/DataSnapExample.tsx (content)
⚔️ www/src/docs/apiExamples/useAxisInverseTickSnapScale/AxisTicksSnapExample.tsx (content)
⚔️ www/src/docs/exampleComponents/LineChart/index.ts (content)
⚔️ www/src/views/IndexView/index.tsx (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: auto-disabling animations based on system prefers-reduced-motion preferences, which aligns with the core functionality introduced across multiple animation components.
Description check ✅ Passed The description covers the key sections: it explains the motivation (testing and UX), the technical approach (prefers-reduced-motion), testing done (manual and unit tests), and acknowledges it as a potential breaking change. However, it lacks explicit updates to the 'My change requires a change to the documentation' and 'I have added tests to cover my changes' checkboxes despite documentation and test additions.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@codecov
Copy link

codecov bot commented Feb 4, 2026

Codecov Report

❌ Patch coverage is 95.72650% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (7b1b097) to head (dd45f65).
⚠️ Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
src/component/TooltipBoundingBox.tsx 96.38% 3 Missing ⚠️
src/util/usePrefersReducedMotion.ts 92.59% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6956      +/-   ##
==========================================
+ Coverage   90.10%   90.13%   +0.03%     
==========================================
  Files         522      525       +3     
  Lines       38848    39092     +244     
  Branches     5347     5414      +67     
==========================================
+ Hits        35004    35236     +232     
- Misses       3835     3847      +12     
  Partials        9        9              

☔ 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.

@codecov
Copy link

codecov bot commented Feb 5, 2026

Bundle Report

Changes will increase total bundle size by 896 bytes (0.03%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
recharts/bundle-cjs* 1.25MB 896 bytes (0.07%) ⬆️

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

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
animation/JavascriptAnimate.js 896 bytes 3.15kB 39.68% ⚠️

@cloud-walker cloud-walker marked this pull request as ready for review February 7, 2026 11:51
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/cartesian/Area.tsx (1)

934-934: ⚠️ Potential issue | 🔴 Critical

Bug: Area resolves 'auto' without respecting prefers-reduced-motion, contradicting the updated JSDoc.

Line 934 resolves isAnimationActive with only the SSR check (!Global.isSsr), bypassing the usePrefersReducedMotion hook that lives inside JavascriptAnimate. Meanwhile, InternalAreaProps (line 110) types isAnimationActive as plain boolean, so 'auto' can never reach JavascriptAnimate.

Other components (Bar, Line, Funnel) pass 'auto' through to JavascriptAnimate, where the hook correctly handles both SSR and reduced-motion. Area should do the same.

Proposed fix: pass 'auto' through to JavascriptAnimate

Update InternalAreaProps to accept 'auto':

-  isAnimationActive: boolean;
+  isAnimationActive: boolean | 'auto';

Then remove the early resolution in AreaImpl:

-      isAnimationActive={isAnimationActive === 'auto' ? !Global.isSsr : isAnimationActive}
+      isAnimationActive={isAnimationActive}
src/animation/JavascriptAnimate.tsx (1)

41-78: ⚠️ Potential issue | 🟠 Major

Add unit tests for the usePrefersReducedMotion hook and isActive='auto' resolution logic.

The isActive='auto' value and usePrefersReducedMotion hook represent new functionality that should be tested to ensure animations correctly respect the prefers-reduced-motion media query. Tests should cover:

  • isActive='auto' with prefers-reduced-motion: reduce → animations disabled
  • isActive='auto' without reduced motion preference → animations enabled
  • isActive=true and isActive=false → hook result ignored
  • Dynamic media query change after mount
🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Around line 37-38: The workflow references the wrong integration test repo:
change the repository entries used by the list_integration_tests and
integration_tests checkout steps in .github/workflows/ci.yml from repository:
cloud-walker/recharts-integ and ref: auto-disable-animations back to the
official repository and branch (repository: recharts/recharts-integ and the
appropriate main branch/ref) so CI pulls tests from recharts/recharts-integ;
after the official repo has the auto-disable-animations changes, update the ref
to the new branch if needed.

In `@src/animation/JavascriptAnimate.tsx`:
- Around line 76-78: CSSTransitionAnimate currently resolves isActive only by
checking !Global.isSsr and isActiveProp, missing the user's
prefers-reduced-motion setting; update the CSSTransitionAnimate logic that
computes isActive (the branch handling isActiveProp === 'auto') to also call
usePrefersReducedMotion and require !prefersReducedMotion (same as
JavascriptAnimate) when auto is used, so the computed isActive becomes
isActiveProp === 'auto' ? !Global.isSsr && !prefersReducedMotion : isActiveProp;
ensure you reference the existing symbols CSSTransitionAnimate,
usePrefersReducedMotion, Global, and isActiveProp when making the change.
🧹 Nitpick comments (3)
www/test/vitest.setup.ts (1)

1-14: Duplicated matchMedia mock — consider extracting a shared helper.

This is an exact duplicate of the mock in test/vitest.setup.ts. If the mock shape evolves (e.g., adding addListener/removeListener), both files need to stay in sync. Consider extracting a shared helper function.

Also applies the same addListener/removeListener gap noted in test/vitest.setup.ts.

src/animation/JavascriptAnimate.tsx (1)

41-61: Add explicit return type annotation and SSR guard in useEffect.

Two items:

  1. Per coding guidelines, function return values should be explicitly typed.
  2. While useEffect doesn't run on the server, adding a defensive SSR guard in the effect body future-proofs against edge cases where Global.isSsr is set in a browser context (e.g., testing scenarios or universal apps that flip this flag).
Proposed fix
-function usePrefersReducedMotion() {
+function usePrefersReducedMotion(): boolean {
   const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {
     if (Global.isSsr) {
       return false;
     }
     return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
   });
 
   useEffect(() => {
+    if (Global.isSsr) {
+      return undefined;
+    }
     const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
     const handleChange = () => {
       setPrefersReducedMotion(mediaQuery.matches);
     };
     mediaQuery.addEventListener('change', handleChange);
     return () => {
       mediaQuery.removeEventListener('change', handleChange);
     };
   }, []);
 
   return prefersReducedMotion;
 }

As per coding guidelines: "Type function parameters and return values explicitly."

src/polar/Radar.tsx (1)

111-115: JSDoc update looks good — minor grammar nit.

The tense is inconsistent: "will be disabled" (future) vs. "respects" (present). Consider aligning:

✏️ Suggested wording
-   * If set "auto", the animation will be disabled in SSR and respects the user's prefers-reduced-motion system preference for accessibility.
+   * If set to "auto", the animation will be disabled in SSR and when the user's system enables the prefers-reduced-motion preference (for accessibility).

@PavelVanecek
Copy link
Collaborator

I would like an undefined guard to prevent this problem in jsdom env: recharts/recharts-integ#90

@cloud-walker cloud-walker force-pushed the prefers-reduced-motion branch from 1737669 to 806393c Compare February 10, 2026 20:32
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/cartesian/Scatter.tsx`:
- Around line 306-308: Update the JSDoc sentence in Scatter.tsx (and apply the
same change across other components in this PR) to use parallel verb forms:
change the description for the animation prop from "will be disabled in SSR and
respects the user's prefers-reduced-motion system preference for accessibility"
to "will be disabled in SSR and will respect the user's prefers-reduced-motion
system preference for accessibility" so both clauses use "will" and match voice;
locate the comment block around the animation prop description in the Scatter
component (and identical blocks in other components) and make the wording
consistent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/animation/JavascriptAnimate.tsx`:
- Around line 41-68: Add a vitest unit test file for usePrefersReducedMotion
that mocks window.matchMedia and uses a hook renderer (e.g., renderHook + act)
to verify: initial state when matchMedia.matches is true and when false; that
calling the mocked mediaQuery.dispatchEvent / triggering its 'change' calls
updates the hook state; that when window.matchMedia is undefined the hook
returns false; and that on unmount the listener removal is invoked (spy/verify
removeEventListener called). Reference the hook name usePrefersReducedMotion and
the media query string '(prefers-reduced-motion: reduce)' in your tests and
restore/cleanup mocks between tests.
🧹 Nitpick comments (1)
src/animation/JavascriptAnimate.tsx (1)

41-68: Add an explicit return type and eliminate the eslint-disable comment.

  1. Per coding guidelines, function return values in src/** should be explicitly typed: function usePrefersReducedMotion(): boolean.
  2. The eslint-disable consistent-return on line 61 can be avoided by wrapping the body in a single guard so there's only one return path.
♻️ Suggested refactor
-function usePrefersReducedMotion() {
+function usePrefersReducedMotion(): boolean {
   const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {
     if (Global.isSsr) {
       return false;
     }
     if (!window.matchMedia) {
       return false;
     }
     return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
   });
 
   useEffect(() => {
-    if (!window.matchMedia) {
-      return;
-    }
-    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
-    const handleChange = () => {
-      setPrefersReducedMotion(mediaQuery.matches);
-    };
-    mediaQuery.addEventListener('change', handleChange);
-    // eslint-disable-next-line consistent-return
-    return () => {
-      mediaQuery.removeEventListener('change', handleChange);
-    };
+    if (window.matchMedia) {
+      const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
+      const handleChange = () => {
+        setPrefersReducedMotion(mediaQuery.matches);
+      };
+      mediaQuery.addEventListener('change', handleChange);
+      return () => {
+        mediaQuery.removeEventListener('change', handleChange);
+      };
+    }
+    return undefined;
   }, []);
 
   return prefersReducedMotion;
 }

As per coding guidelines: "Type function parameters and return values explicitly. Do not rely on implicit any or inference."

@cloud-walker cloud-walker marked this pull request as draft February 11, 2026 20:39
@cloud-walker cloud-walker marked this pull request as ready for review February 15, 2026 09:53
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/util/usePrefersReducedMotion.ts`:
- Around line 5-32: Exported hook usePrefersReducedMotion lacks an explicit
return type and JSDoc and uses an eslint-disable for consistent-return; add a
JSDoc block describing the hook and its boolean return, give the function an
explicit return type of boolean, and remove the eslint-disable by restructuring
the effect to early-return when Global.isSsr or window.matchMedia is falsy
(mirror the useState initializer). Also reference the same mediaQuery logic and
state setters (prefersReducedMotion, setPrefersReducedMotion) inside the effect
so the behavior is unchanged.
🧹 Nitpick comments (4)
src/component/TooltipBoundingBox.tsx (4)

76-82: Calling setState during render — valid but fragile pattern.

This is the React-sanctioned "adjust state during rendering" pattern (analogous to getDerivedStateFromProps), so it works. However, it's easy to accidentally introduce an infinite loop if a future change makes the guard condition always true.

A slightly safer alternative is to move this into the existing useEffect on [props.coordinate?.x, props.coordinate?.y], which already fires when coordinates change:

♻️ Optional: move dismissal reset into the effect
   React.useEffect(() => {
+    setState(prev => (prev.dismissed ? { ...prev, dismissed: false } : prev));
+
     const handleKeyDown = (event: KeyboardEvent) => {
       if (event.key === 'Escape') {
         setState({
           dismissed: true,
           dismissedAtCoordinate: {
             x: props.coordinate?.x ?? 0,
             y: props.coordinate?.y ?? 0,
           },
         });
       }
     };
     document.addEventListener('keydown', handleKeyDown);
     return () => {
       document.removeEventListener('keydown', handleKeyDown);
     };
   }, [props.coordinate?.x, props.coordinate?.y]);

Then remove lines 76–82 entirely.


98-119: visibility on line 110 is dead code — always overridden on line 117.

In outerStyle, the object spread ...positionStyle is followed by an explicit visibility key, which overwrites whatever positionStyle.visibility was. This means the visibility set on line 110 inside positionStyle never reaches the final style object.

Either remove it from positionStyle (line 110) to avoid confusion, or remove the duplicate from outerStyle (line 117) if the intent is for positionStyle to be the single source.

♻️ Suggested fix — remove from positionStyle
         ...cssProperties,
         pointerEvents: 'none',
-        visibility: !state.dismissed && props.active && props.hasPayload ? 'visible' : 'hidden',
         position: 'absolute',
         top: 0,
         left: 0,
       };

58-74: Event listener is torn down and re-added on every coordinate change.

The useEffect depends on [props.coordinate?.x, props.coordinate?.y], so the keydown listener is removed and re-registered every time the tooltip moves. Since handleKeyDown only needs the current coordinates at the time the key is pressed, you could store coordinates in a ref and use an effect with an empty dependency array to register the listener once.

This is a minor optimisation — the current code is correct.


51-56: Missing JSDoc on the component and its props.

Per project guidelines, source files should include JSDoc comments (used to autogenerate API docs). TooltipBoundingBoxProps and TooltipBoundingBoxImpl lack any doc comments. Consider adding at least a brief JSDoc on the exported TooltipBoundingBox and the public props interface. As per coding guidelines: "JSDoc comments and TypeScript definitions in source files must be kept up-to-date as they are used to autogenerate API documentation via 'npm run omnidoc'."

@cloud-walker
Copy link
Contributor Author

@PavelVanecek I think its ready for a review

if (!this.state.dismissed) {
return;
}
React.useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: why so much formatting going on here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I converted this from class to function component, I guess is for that

@cloud-walker cloud-walker requested a review from ckifer February 16, 2026 09:27
@PavelVanecek
Copy link
Collaborator

I wonder if this preference is also connected to battery saver mode on smartphones.

@PavelVanecek PavelVanecek merged commit ebde48f into recharts:main Feb 17, 2026
41 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.

3 participants