Fix layout animations not triggering with MotionValue#3651
Fix layout animations not triggering with MotionValue#3651mattgperry wants to merge 1 commit intomainfrom
Conversation
When a layout-affecting MotionValue (width, height, top, left, right, bottom) changes, notify the projection system so it can snapshot the old layout and animate to the new one. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR fixes layout animations not triggering when a The fix introduces a Key observations:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant MotionValue
participant VisualElement
participant ProjectionNode
participant FrameLoop
participant DOM
User->>MotionValue: width.set(300)
MotionValue->>VisualElement: onChange(300)
VisualElement->>VisualElement: latestValues[key] = 300
VisualElement->>ProjectionNode: willUpdate() — snapshot current layout (100px)
VisualElement->>FrameLoop: frame.postRender(scheduleDidUpdate)
VisualElement->>FrameLoop: frame.render(this.render)
Note over FrameLoop: Next animation frame
FrameLoop->>DOM: render step — apply width: 300px
FrameLoop->>ProjectionNode: postRender step — root.didUpdate()
ProjectionNode->>ProjectionNode: microtask.read(scheduleUpdate)
Note over FrameLoop: Microtask
ProjectionNode->>DOM: update() — measure new layout (300px)
ProjectionNode->>ProjectionNode: compute delta (100→300)
ProjectionNode->>DOM: start layout animation via CSS transform
Last reviewed commit: 59bd0d5 |
| frame.postRender( | ||
| () => this.projection?.root?.didUpdate() | ||
| ) |
There was a problem hiding this comment.
New anonymous function allocated on every
onChange call
The postRender callback () => this.projection?.root?.didUpdate() is an arrow function created fresh each time the onChange handler fires. Because frame.postRender (backed by a Set<Process>) deduplicates by function reference, each distinct closure is stored separately. If onChange fires multiple times before the next postRender step runs, N distinct callbacks are enqueued and all executed — only the first didUpdate() call does real work (thanks to its updateScheduled guard), but the rest run needlessly.
A stable, pre-bound reference on the class (similar to how this.notifyUpdate is used for preRender) would ensure only one entry lives in the set at any time:
// as a class field:
private scheduleDidUpdate = () => this.projection?.root?.didUpdate()
// in the onChange handler:
frame.postRender(this.scheduleDidUpdate)This mirrors the existing pattern for this.notifyUpdate and prevents unnecessary closure allocations on rapid value changes.
|
|
||
| const layoutKeys = new Set([ | ||
| "width", | ||
| "height", | ||
| "top", | ||
| "left", | ||
| "right", | ||
| "bottom", |
There was a problem hiding this comment.
layoutKeys doesn't cover all layout-affecting CSS properties
The set only includes the six box-position/size properties listed here. Several other CSS properties that a user might drive with a MotionValue can equally affect an element's bounding box and therefore fail to trigger layout animations:
minWidth/maxWidth/minHeight/maxHeightpadding/paddingTop/paddingRight/paddingBottom/paddingLeftmargin/marginTop/marginRight/marginBottom/marginLeftinset(CSS shorthand for top/right/bottom/left)borderWidthand its long-hand variants
If this is an intentional trade-off (covering only the most common cases), it would be worth documenting with a comment so future contributors understand the scope.
Summary
Fixes #2907
Layout animations did not work when a
MotionValuedrove a layout-affecting style property (e.g.width,height). This happened becauseMotionValueupdates bypass React's render cycle, so the projection system was never notified of the layout change —willUpdate()anddidUpdate()were never called.Cause: In
VisualElement.bindToMotionValue(), theonChangehandler only setisTransformDirtyfor transform properties. Non-transform positional properties (width,height,top,left,right,bottom) triggered a DOM render but never notified the projection system.Fix: When a layout-affecting MotionValue changes, call
projection.willUpdate()(to snapshot the old layout before render) and scheduleprojection.root.didUpdate()viaframe.postRender(to measure the new layout and start the animation after the DOM updates).Test plan
layout-motion-value) that verifies amotion.divwithlayoutand auseMotionValue-drivenwidthanimates correctly when the value changesdelay.test.ts)🤖 Generated with Claude Code