fix(drag): refresh root scroll before measuring ref constraints (#2829)#3722
Conversation
The scroll captured by the drag feature on mount can be stale by the time `frame.read` reads the constraint element's bounding box — for example when the browser restores scroll on refresh, or when an ancestor's layout effect scrolls after this element mounts. The constraint then translates to wrong page coordinates and the draggable area shrinks by the scroll offset. Clear and re-measure the root scroll inside `resolveRefConstraints` so the constraint's page box matches the live scroll position. Fixes #2829 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Greptile SummaryThis PR fixes a stale scroll offset bug (#2829) where
Confidence Score: 4/5Safe to merge; the core change is a small, targeted mutation of a shared singleton's cached scroll value that is immediately re-populated, and is guarded by a null check consistent with the surrounding code. The logic fix in VisualElementDragControls.ts is correct and well-reasoned. The only concern is the .wait(300) in the new Cypress test, which is time-dependent and could produce intermittent failures on slow CI runners rather than a deterministic signal. packages/framer-motion/cypress/integration/drag-ref-constraints-absolute-scrolled.ts — the time-based wait warrants a second look. Important Files Changed
Sequence DiagramsequenceDiagram
participant LE as useLayoutEffect (test page)
participant DC as VisualElementDragControls
participant PR as projection.root (DocumentProjectionNode)
participant MP as measurePageBox
LE->>LE: window.scrollTo(0, 300) — scroll restored
Note over DC: drag mount (layout effect)
DC->>PR: "updateScroll() — caches scroll=0 (stale, animationId cached)"
DC->>DC: frame.read(measureDragConstraints)
Note over DC: next frame — measureDragConstraints fires
DC->>DC: resolveRefConstraints()
DC->>PR: "scroll = undefined (bust cache)"
DC->>PR: "updateScroll() — re-reads live scrollY=300"
DC->>MP: measurePageBox(constraintsElement, projection.root)
MP-->>DC: constraintsBox with correct page coords
DC-->>DC: constraints computed correctly
Reviews (1): Last reviewed commit: "fix(drag): Refresh root scroll before me..." | Re-trigger Greptile |
| .window() | ||
| .then((win) => { |
There was a problem hiding this comment.
Time-based wait may cause flaky test on slow CI runners
The .wait(300) on line 14 relies on a fixed timeout to let the useLayoutEffect scroll settle before asserting win.scrollY > 0. On an overloaded CI machine the effect may not have run yet, while on a fast machine the wait is unnecessarily slow. A more robust approach would be to poll until the scroll condition is true — e.g., wrap the expect(win.scrollY).to.be.greaterThan(0) check in a cy.waitUntil or use a Cypress retry via .should(() => { ... }) attached directly to cy.window().
Summary
dragConstraintsis a ref to a viewport-sized element (e.g.position: absolute; inset: 0) and the document is already scrolled when the drag mounts, the constraints are computed with a stale scroll offset. The draggable area shrinks by roughly the scroll amount — the user reported they could no longer drag to the bottom of the viewport after refreshing on a scrolled page.projection.root.updateScroll()once, and then schedulesmeasureDragConstraintsviaframe.read. If scroll changes in between (browser restoring scroll on refresh, or an ancestor's layout effect scrolling), the cached root scroll is wrong when the constraint element's viewport box is translated to page coords.projection.root.scrollinsideresolveRefConstraintsso the page-coord translation uses the live scroll offset.projection.updateScroll's per-animationId cache made the obvious "just call updateScroll again" a no-op, so the cached value is cleared first.Test plan
drag-ref-constraints-absolute-scrolledreproduces the bug: pre-scroll the window in a layout effect, drag amotion.divinside aposition: absolute; inset: 0ref constraint, assert the box reaches the visible bottom of the viewport. Fails without the fix (expected 200 to be close to 500), passes with it.yarn testpasses (793 unit tests).Fixes #2829
🤖 Generated with Claude Code