fix(Legend): prevent overlap with chart on container resize#7201
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughElement-offset hook now measures via getBoundingClientRect and keeps a persistent ResizeObserver with EPS gating; Legend dispatchers switch to useLayoutEffect, destructure payloads, and tighten dependencies; initial legend verticalAlign default changed to 'bottom'; tests updated and a new hook test added. ChangesElement measurement hook
Legend component dispatchers
State defaults
Tests — component & selectors
Sequence Diagram(s)sequenceDiagram
participant Hook as ElementOffset Hook
participant RefCB as Ref Callback
participant Node as DOM Node
participant RO as ResizeObserver (rgba(60,120,200,0.5))
participant State as Offset State
Note over Hook,RefCB: Ref callback attached with node
RefCB->>Node: read getBoundingClientRect()
RefCB->>State: setOffset(new) if hasSignificantChange(prev, new)
RefCB->>RO: create ResizeObserver(callback) and RO.observe(Node)
Note over Node,RO: layout/resize happens
RO->>RO: invoke callback
RO->>RefCB: read getBoundingClientRect()
alt change > EPS
RefCB->>State: update offset
else change <= EPS
RefCB->>State: no-op
end
Note over Hook,RefCB: node detached or swapped
RefCB->>RO: disconnect previous observer
RefCB->>RefCB: attach/measure new node (if any)
Note over Hook,RO: on unmount
Hook->>RO: disconnect observer
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
test/component/Legend.spec.tsx (1)
1613-1670: Please add one end-to-end resize regression case here.These assertions cover the new timing, but they still don't exercise the actual
#7200path: a wide horizontal legend wrapping after the chart becomes narrower. Onererender/ResponsiveContainercase that makes the legend grow in height and then asserts a larger bottom offset would lock down the user-visible fix.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/component/Legend.spec.tsx` around lines 1613 - 1670, Add an end-to-end resize regression test that simulates a wide horizontal Legend wrapping when the chart narrows: use mockGetBoundingClientRect to return a single-line height, render the chart via testChartLayoutContext (or inside a ResponsiveContainer) with a wide Legend, then trigger a resize/rerender to a narrower width so the Legend wraps (increase mocked height), and finally assert that the layout offset reflected a larger bottom and reduced chart height; reference the existing helpers (mockGetBoundingClientRect, testChartLayoutContext, Legend, ResponsiveContainer, rerender) to locate where to add the new case and update expectations accordingly.src/util/useElementOffset.ts (1)
77-107: Keep the ref callback stable to avoid recreating the observer on every measurement update.Because
updateBoundingBoxdepends onlastBoundingBox.*, every state change replaces the ref callback. React will then call the old ref withnulland the new one withnode, which disconnects and recreates theResizeObserveron each resize. Comparing againstlastBoundingBoxRef.currentlets this stay stable across size updates.♻️ Possible cleanup
const updateBoundingBox = useCallback( (node: HTMLElement | null) => { @@ if (node != null) { // Measure immediately on ref attach const box = readElementOffset(node); - if (hasSignificantChange(box, lastBoundingBox)) { + if (hasSignificantChange(box, lastBoundingBoxRef.current)) { + lastBoundingBoxRef.current = box; setLastBoundingBox(box); } @@ const observer = new ResizeObserver(() => { const newBox = readElementOffset(node); if (hasSignificantChange(newBox, lastBoundingBoxRef.current)) { + lastBoundingBoxRef.current = newBox; setLastBoundingBox(newBox); } }); @@ - [lastBoundingBox.width, lastBoundingBox.height, lastBoundingBox.top, lastBoundingBox.left, ...extraDependencies], + [...extraDependencies], );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/util/useElementOffset.ts` around lines 77 - 107, The ref callback updateBoundingBox is unstable because it lists lastBoundingBox.* in its dependency array causing React to recreate the ref on every measurement; make updateBoundingBox stable by removing lastBoundingBox.width/height/top/left from the deps and rely on lastBoundingBoxRef.current for comparisons inside the callback (use lastBoundingBoxRef when calling hasSignificantChange and when deciding to call setLastBoundingBox), keep observerRef management and the ResizeObserver setup inside updateBoundingBox and keep extraDependencies in the deps array so the ref only changes when those external deps change.src/component/Legend.tsx (1)
159-174: Avoid object-identity reruns in these layout effects.
[dispatch, props]makes both effects rerun on every parent render becausepropsis a fresh object each time. InLegendSizeDispatcher, that also runs the cleanup on unrelated rerenders, briefly dispatching{ width: 0, height: 0 }before the real size again. Please depend on the scalar fields instead and keep the reset in an unmount-only effect.♻️ Possible cleanup
-function LegendSettingsDispatcher(props: LegendSettings): null { +function LegendSettingsDispatcher({ align, layout, verticalAlign, itemSorter }: LegendSettings): null { const dispatch = useAppDispatch(); useLayoutEffect(() => { - dispatch(setLegendSettings(props)); - }, [dispatch, props]); + dispatch(setLegendSettings({ align, layout, verticalAlign, itemSorter })); + }, [dispatch, align, layout, verticalAlign, itemSorter]); return null; } -function LegendSizeDispatcher(props: Size): null { +function LegendSizeDispatcher({ width, height }: Size): null { const dispatch = useAppDispatch(); useLayoutEffect(() => { - dispatch(setLegendSize(props)); + dispatch(setLegendSize({ width, height })); + }, [dispatch, width, height]); + + useLayoutEffect(() => { return () => { dispatch(setLegendSize({ width: 0, height: 0 })); }; - }, [dispatch, props]); + }, [dispatch]); return null; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/component/Legend.tsx` around lines 159 - 174, The layout effects rerun on every render because they depend on the whole props object; fix LegendSettingsDispatcher and LegendSizeDispatcher by destructuring the incoming props into their scalar fields and use those scalars in the dependency arrays (e.g., for LegendSettingsDispatcher depend on the specific settings fields you care about rather than props, and for LegendSizeDispatcher depend only on width and height), and move the reset-to-zero dispatch into an unmount-only cleanup (a separate effect with an empty dependency array that returns a cleanup calling setLegendSize({ width: 0, height: 0 })) while the size-updating effect only dispatches setLegendSize(width,height) when those scalar values change.
🤖 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/util/useElementOffset.ts`:
- Around line 51-59: The exported ElementOffset type and its JSDoc must be
updated to reflect that readElementOffset() returns values from
getBoundingClientRect() (viewport-relative left/top and fractional width/height)
rather than DOM offset* semantics; update the JSDoc for the ElementOffset
interface and any exported docs near the readElementOffset and the related
function (the other doc block referenced at 61-69) to state that left/top are
clientX/clientY coordinates relative to the viewport and width/height may be
fractional subpixel values returned by getBoundingClientRect(), and adjust any
wording that mentions offsetLeft/offsetTop or integer pixel precision to match
this contract.
---
Nitpick comments:
In `@src/component/Legend.tsx`:
- Around line 159-174: The layout effects rerun on every render because they
depend on the whole props object; fix LegendSettingsDispatcher and
LegendSizeDispatcher by destructuring the incoming props into their scalar
fields and use those scalars in the dependency arrays (e.g., for
LegendSettingsDispatcher depend on the specific settings fields you care about
rather than props, and for LegendSizeDispatcher depend only on width and
height), and move the reset-to-zero dispatch into an unmount-only cleanup (a
separate effect with an empty dependency array that returns a cleanup calling
setLegendSize({ width: 0, height: 0 })) while the size-updating effect only
dispatches setLegendSize(width,height) when those scalar values change.
In `@src/util/useElementOffset.ts`:
- Around line 77-107: The ref callback updateBoundingBox is unstable because it
lists lastBoundingBox.* in its dependency array causing React to recreate the
ref on every measurement; make updateBoundingBox stable by removing
lastBoundingBox.width/height/top/left from the deps and rely on
lastBoundingBoxRef.current for comparisons inside the callback (use
lastBoundingBoxRef when calling hasSignificantChange and when deciding to call
setLastBoundingBox), keep observerRef management and the ResizeObserver setup
inside updateBoundingBox and keep extraDependencies in the deps array so the ref
only changes when those external deps change.
In `@test/component/Legend.spec.tsx`:
- Around line 1613-1670: Add an end-to-end resize regression test that simulates
a wide horizontal Legend wrapping when the chart narrows: use
mockGetBoundingClientRect to return a single-line height, render the chart via
testChartLayoutContext (or inside a ResponsiveContainer) with a wide Legend,
then trigger a resize/rerender to a narrower width so the Legend wraps (increase
mocked height), and finally assert that the layout offset reflected a larger
bottom and reduced chart height; reference the existing helpers
(mockGetBoundingClientRect, testChartLayoutContext, Legend, ResponsiveContainer,
rerender) to locate where to add the new case and update expectations
accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: de0f4ac4-a8fd-4c19-87fb-1fa620653212
📒 Files selected for processing (6)
src/component/Legend.tsxsrc/state/legendSlice.tssrc/util/useElementOffset.tstest/component/Legend.spec.tsxtest/state/selectors/legendSelectors.spec.tsxtest/util/useElementOffset.spec.tsx
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7201 +/- ##
==========================================
- Coverage 89.07% 89.00% -0.07%
==========================================
Files 538 541 +3
Lines 41020 41165 +145
Branches 5563 5578 +15
==========================================
+ Hits 36538 36639 +101
- Misses 4474 4518 +44
Partials 8 8 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Bundle ReportChanges will increase total bundle size by 360.59kB (7.06%) ⬆️
ℹ️ *Bundle size includes cached data from a previous commit Affected Assets, Files, and Routes:view changes for bundle: recharts/bundle-umdAssets Changed:
view changes for bundle: recharts/bundle-treeshaking-treemapAssets Changed:
view changes for bundle: recharts/bundle-cjsAssets Changed:
view changes for bundle: recharts/bundle-treeshaking-cartesianAssets Changed:
view changes for bundle: recharts/bundle-treeshaking-polarAssets Changed:
view changes for bundle: recharts/bundle-es6Assets Changed:
|
…charts#7200) The Legend component in v3 fails to update the chart offset when its height changes due to item wrapping on resize. Three root causes: 1. useElementOffset only measured via ref callback, missing resize events. Added ResizeObserver to detect element size changes automatically. 2. LegendSettingsDispatcher/LegendSizeDispatcher used useEffect (post-paint), causing a flash of incorrect layout. Changed to useLayoutEffect. 3. legendSlice initial verticalAlign was 'middle' instead of 'bottom', causing appendOffsetOfLegend to skip offset adjustment on first render.
- Update ElementOffset JSDoc to match getBoundingClientRect() semantics - Add JSDoc to hasSignificantChange and readElementOffset - Stabilize useElementOffset ref callback by removing lastBoundingBox from dependency array, preventing unnecessary ResizeObserver recreation - Destructure LegendSettingsDispatcher/LegendSizeDispatcher props into scalar fields to avoid effect reruns on every render - Separate unmount cleanup into its own effect in LegendSizeDispatcher - Add end-to-end resize regression test for recharts#7200
e6a3a0f to
f69c529
Compare
|
@ckifer @PavelVanecek |
|
Hi @maroKanatani , there's 40+ open PRs, I'm going through them when I have time. Thank you for your patience |
|
And I'm going through some more important life stuff 😅 Will try to help when I can |
…etween test files vi.stubGlobal in responsive.spec.tsx was not cleaned up between tests, causing Line.animation.spec.tsx to receive a stub without mockImplementation, making ResizeObserver().observe() throw TypeError in CI.
Fix #7200 — Legend overlaps chart on container resize
When the container shrinks, legend items wrap and get taller,
but the chart offset never updates. Worked in v2 via
componentDidUpdate, regressed in v3.Changes
useElementOffset: addedResizeObserverto re-measure onresize (same pattern as
ResponsiveDiv)LegendSettingsDispatcher/LegendSizeDispatcher: switcheduseEffect→useLayoutEffectso Redux state updates beforepaint (matches
SetLegendPayload.ts)legendSlice: changed initialverticalAlignfrom'middle'to
'bottom'to matchLegenddefaultProps — the mismatchcaused
appendOffsetOfLegendto skip offset on first renderTests
test/util/useElementOffset.spec.tsx(9 tests —ResizeObserver, EPS threshold, cleanup, node re-attachment)
Legend.spec.tsx,legendSelectors.spec.tsxLabel.spec.tsxSummary by CodeRabbit
Improvements
Changes
Bug Fixes
Tests