Skip to content

fix(Brush): slide snaps back to original position on mouseup when start/endIndex are controlled#7542

Merged
PavelVanecek merged 1 commit into
recharts:mainfrom
MaksZhukov:fix/issue-6279-brush-slide-snap-back
Jul 13, 2026
Merged

fix(Brush): slide snaps back to original position on mouseup when start/endIndex are controlled#7542
PavelVanecek merged 1 commit into
recharts:mainfrom
MaksZhukov:fix/issue-6279-brush-slide-snap-back

Conversation

@MaksZhukov

@MaksZhukov MaksZhukov commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #6279.

The reported repro ("drag the brush handle past the start/end bound, drag it back in, the position ends up offset") turns out to be a symptom of a more general bug: dragging the Brush's slide and releasing the mouse can snap startX back to its original, pre-drag position whenever startIndex/endIndex are controlled via props - regardless of whether the drag went out of bounds. Any drag that ends without onChange having changed the controlled startIndex/endIndex prop (the common case for small mouse movements that don't cross a data-index boundary) triggers it.

Root cause

Brush.getDerivedStateFromProps has a branch that re-syncs startX/endX from startIndexControlledFromProps/endIndexControlledFromProps whenever those controlled props change, but only while the user is not currently interacting with the brush (so it doesn't fight an active drag). The guard for "not interacting" is only false while isSlideMoving/isTravellerMoving/etc. are true, so this branch is skipped for the entire duration of a drag.

The prevStartIndexControlledFromProps/prevEndIndexControlledFromProps tracking fields used for the comparison were only ever initialized inside that same skipped branch - never on mount. Since the branch is always skipped while dragging, the first time it runs again is the render right after mouseup. At that point prevStartIndexControlledFromProps is still undefined, so undefined !== startIndexControlledFromProps is trueeven though the prop never actually changed - and the code incorrectly treats this as "the controlled prop changed", resetting startX back to scale(startIndexControlledFromProps) and discarding the user's drag.

Fix

Initialize prevStartIndexControlledFromProps/prevEndIndexControlledFromProps in the same getDerivedStateFromProps branch that already computes the scale on mount / data change, so the trackers start in sync with the actual prop values instead of undefined. This closes the false-mismatch window without touching the (correct) re-sync behavior for when the controlled props genuinely change later.

Test plan

  • Added a regression test in test/cartesian/Brush.spec.tsx (dragging the slide when start/endIndex are controlled from props) that drags the slide with controlled startIndex/endIndex and asserts the position right after mouseup matches the position while dragging.
  • Verified the new test fails without the fix (startX reverts to its pre-drag value) and passes with it.
  • npm run test - full suite passes (16199 passed, same pre-existing 6 expected-fail / unrelated website-build failures as on main).
  • npm run check-types - passes.
  • npm run lint - passes (0 errors).

Summary by CodeRabbit

  • Bug Fixes

    • Improved brush behavior when start and end positions are controlled from props, so dragged selections no longer snap back unexpectedly after release.
    • Kept traveller positions in sync during data updates for a smoother, more consistent interaction.
  • Tests

    • Added coverage for controlled brush dragging to verify traveller positions remain stable after mouse release.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Modified BrushWithState.getDerivedStateFromProps in Brush.tsx to persist prevStartIndexControlledFromProps and prevEndIndexControlledFromProps when data changes. Added a test suite validating that traveller positions remain unchanged after mouseUp when startIndex/endIndex are controlled via props during drag.

Changes

Brush controlled-index fix

Layer / File(s) Summary
Persist controlled index state and validate drag stability
src/cartesian/Brush.tsx, test/cartesian/Brush.spec.tsx
getDerivedStateFromProps now also stores prevStartIndexControlledFromProps and prevEndIndexControlledFromProps when data changes; a new test verifies traveller aria-valuenow positions do not snap back after mouseUp when startIndex/endIndex are prop-controlled.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The change looks related, but the linked issue's out-of-bounds drag/re-entry behavior is not explicitly demonstrated by the summary or test. Add coverage or evidence for the exact #6279 repro: drag past the brush bounds, re-enter, and verify the handle resumes at the boundary.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the Brush slide snap-back bug on mouseup for controlled start/end indices.
Description check ✅ Passed The description covers the fix, context, linked issue, and testing, though it does not follow the repository template exactly.
Out of Scope Changes check ✅ Passed The diff stays within Brush behavior and its regression test, with no unrelated changes visible in the summary.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

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

🧹 Nitpick comments (1)
test/cartesian/Brush.spec.tsx (1)

588-592: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Call vi.runOnlyPendingTimers() after render.

The test does not use createSelectorTestCase and doesn't advance timers after rendering. While the Brush component manages its own state synchronously via getDerivedStateFromProps, the parent BarChart uses Redux with autoBatchEnhancer which batches state updates behind timers. Advancing timers after render ensures the chart is fully initialized before interaction.

As per coding guidelines: "Call vi.runOnlyPendingTimers() to advance timers after renders when not using createSelectorTestCase helper".

       const { container } = render(
         <BarChart width={400} height={100} data={data}>
           <Brush dataKey="value" x={100} y={50} width={400} height={40} startIndex={3} endIndex={6} />
         </BarChart>,
       );
+      vi.runOnlyPendingTimers();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cartesian/Brush.spec.tsx` around lines 588 - 592, The Brush test setup
renders BarChart without the createSelectorTestCase helper, so the Redux
auto-batched initialization may still be pending after render. Update the
Brush.spec.tsx test that renders BarChart with Brush to call
vi.runOnlyPendingTimers() immediately after render, before any assertions or
interactions, so the chart is fully initialized.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/cartesian/Brush.spec.tsx`:
- Around line 588-592: The Brush test setup renders BarChart without the
createSelectorTestCase helper, so the Redux auto-batched initialization may
still be pending after render. Update the Brush.spec.tsx test that renders
BarChart with Brush to call vi.runOnlyPendingTimers() immediately after render,
before any assertions or interactions, so the chart is fully initialized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 487fa94e-654c-40bf-b6aa-b19278fddc34

📥 Commits

Reviewing files that changed from the base of the PR and between 38175dd and 15f7af2.

📒 Files selected for processing (2)
  • src/cartesian/Brush.tsx
  • test/cartesian/Brush.spec.tsx

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Bundle Report

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

Detailed changes
Bundle name Size Change
recharts/bundle-cjs 1.45MB 144 bytes (0.01%) ⬆️
recharts/bundle-es6 1.27MB 144 bytes (0.01%) ⬆️
recharts/bundle-umd 586.14kB 70 bytes (0.01%) ⬆️
recharts/bundle-treeshaking-cartesian 725.43kB 144 bytes (0.02%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
cartesian/Brush.js 144 bytes 31.42kB 0.46%
view changes for bundle: recharts/bundle-umd

Assets Changed:

Asset Name Size Change Total Size Change (%)
Recharts.js 70 bytes 586.14kB 0.01%
view changes for bundle: recharts/bundle-es6

Assets Changed:

Asset Name Size Change Total Size Change (%)
cartesian/Brush.js 144 bytes 30.1kB 0.48%
view changes for bundle: recharts/bundle-treeshaking-cartesian

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 144 bytes 725.43kB 0.02%

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.17%. Comparing base (e45fa2f) to head (15f7af2).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7542   +/-   ##
=======================================
  Coverage   88.17%   88.17%           
=======================================
  Files         613      613           
  Lines       14246    14246           
  Branches     3580     3579    -1     
=======================================
  Hits        12562    12562           
  Misses       1494     1494           
  Partials      190      190           

☔ View full report in Codecov by Harness.
📢 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.

@PavelVanecek
PavelVanecek merged commit f2786e8 into recharts:main Jul 13, 2026
58 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.

Brush is movement is incorrect when mouse is moved outside of bounds

2 participants