Repository navigation
Prototype: vanilla drag() API shared with React drag - #3932
mattgperry wants to merge 5 commits into
Conversation
Moves the drag controller, PanSession, constraint utils and pointer event helpers from framer-motion into motion-dom. The React drag feature now uses the shared controller from motion-dom. Adds drag(elementOrSelector, options) to motion-dom. It reuses or creates a VisualElement and projection node for the element, then runs the same controller with options mapped from the vanilla names. dragConstraints also accepts an element, as well as a ref. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015exBWSHQZDwn3zvCFJV5Sh
Ports the React drag Jest suite and the Cypress drag specs to vanilla drag(): 66 Jest tests in motion-dom and 78 Playwright tests. Behaviours that only exist in React (whileDrag, dragControls, AnimatePresence, layout props, Reorder) are left out. Fixes found by the new tests: - drag() now sets touch-action, user-select and draggable="false" like the React drag prop, so touch input drags instead of scrolling. - acquireProjectionNode only reuses a mounted node, so calling drag() again after stop() gets a live projection node. Parent transforms and scaled SVG viewBoxes are marked test.fail, as vanilla drag() has no transformPagePoint option yet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015exBWSHQZDwn3zvCFJV5Sh
handOffElementState gave the style effect's values to the new VisualElement, but left two links to the emptied effect state: - Each value's owner was still the effect subject, so transform animations started on it later read values from the empty state. - The element's transform group still read through the effect state. When it finished, it committed transform: none. Now the hand-off sets each value's owner to the VisualElement. It also moves any transform group to the main thread and detaches it, so later animations form a new group. A detached group no longer removes a newer group when it finishes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015exBWSHQZDwn3zvCFJV5Sh
|
I could not reproduce the failure locally. I ran the same command as the job (
I also ran the new drag suites 6 times in parallel under load, and they passed every time. Generated by Claude Code |
Port of the fix from #3934. The test expected exactly 5 updates in 5 frames, which fails on CI when a slow frame produces no change. It now checks that no frame gets more than one update, which is the orphaned-animation case the test exists to catch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015exBWSHQZDwn3zvCFJV5Sh
|
Probable cause of the Generated by Claude Code |
|
Generated by Claude Code |
Start the animation inside frame.update, so the WAAPI build samples from the same timestamp as the start. On a slow runner the build happened tens of ms after the start, shifting every sample. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U63godu6kWaz7rM1GKkvWJ
Requested by Matt · project thread
Before: drag only works through React
motioncomponents. The drag logic (VisualElementDragControls,PanSession, constraint maths) lives in framer-motion.After:
import { drag } from "motion"makes any element draggable without React:React
dragprops run through the same controller, which now lives in motion-dom.How
VisualElementDragControls,PanSession,drag/utils/constraints,addPointerEvent,event-infoanddistancefrom framer-motion into motion-dom (git renames, with import changes only). framer-motion's drag and pan features import them from motion-dom.readPropsgetter. React keeps the default (visualElement.getProps()), anddrag()passes its options mapped to prop names (axis→drag,constraints→dragConstraints,onMove→onDragand so on).drag()gets a projection node from the newacquireProjectionNode()inLayoutAnimationBuilder. It reuses a mounted node that amotioncomponent oranimateLayout()already owns, or creates one in the shared imperative tree and drops it on cleanup. It setsalwaysMeasureLayout, the same as React does for draggable elements.drag()sets the same element styles as the Reactdragprop (touch-action,user-select,-webkit-touch-callout,draggable="false"), and puts back the previous values on cleanup.dragConstraintsnow also takes anElementas well as a ref, for both APIs.@internaltags fromPanSessionand the controller'sstop/cancel/endPanSession, because the d.ts build strips them and framer-motion now imports them across the package boundary.Hand-off fix (
handOffElementState)Before, calling
animate(el, { x })beforedrag()made the element jump back to its origin on release. An instantanimate()beforedrag()also never rendered. The cause was in the hand-off from the style effect to a VisualElement, whichanimateLayout()also uses. The values moved to the VisualElement, but two links to the emptied effect state stayed:ownerwas still the effect subject, so transform animations started on it later read from the empty state.transform: none.Now the hand-off sets each value's owner to the VisualElement. It also moves any transform group to the main thread and detaches it, so later animations form a new group. A detached group no longer removes a newer group from
transformGroupswhen it finishes.Tests
Vanilla
drag()is tested to the same level asmotion.divdrag:animate()andanimateLayout().animate().drag-basic.spec.tsfailed before the hand-off fix and pass after it.dragControls/snapToCursor,dragListener, AnimatePresence,layout/layoutId, Reorder.test.fail: scaled or rotated parents, and SVG with a scaled viewBox. Vanilla has notransformPagePointoption yet.press.test.tsxtest failed once in the full run and passed in 3 runs on its own. This PR does not touch press.Size
motion-divandanimatestay within budget.dragscenario: about 36 kB gz, because vanilladrag()brings in VisualElement, the projection tree and the animation stack. Budgets went up forvanilla-allandreact-all, because both exportdrag.getBoundingClientRect, render throughstyleEffect, and skip projection.Open questions
drag()return controls (start(event, { snapToCursor })) and not only a cleanup function? Should it take atransformPagePointoption?whileDraghas no vanilla equivalent yet.🤖 Generated with Claude Code
https://claude.ai/code/session_015exBWSHQZDwn3zvCFJV5Sh