Skip to content

Commit 9182d2c

Browse files
authored
Merge pull request #3343 from rortan134/fix-3337
fix: Deduplicate animate() onComplete: fire once after group completion
2 parents 13129da + 063d9b1 commit 9182d2c

File tree

2 files changed

+47
-1
lines changed

2 files changed

+47
-1
lines changed

packages/framer-motion/src/animation/animate/__tests__/animate.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,40 @@ describe("animate", () => {
337337
expect(max).toBeGreaterThan(2)
338338
expect(value.get()).toBe(0)
339339
})
340+
341+
test("top-level onComplete fires once when some props are equal and others animate", async () => {
342+
const calls: number[] = []
343+
344+
const proxy = new Proxy(
345+
{ x: 0, y: 0 },
346+
{
347+
set: (target, p: string, newValue: any) => {
348+
if (p === "x" || p === "y") {
349+
;(target as any)[p] = newValue
350+
}
351+
return true
352+
},
353+
}
354+
)
355+
356+
const anim = animate(
357+
proxy,
358+
{ x: 100, y: 0 },
359+
{
360+
duration: 0.05,
361+
onComplete: () => {
362+
calls.push(performance.now())
363+
},
364+
}
365+
)
366+
367+
await anim.finished
368+
369+
expect(calls.length).toBe(1)
370+
// And ensure final state reached
371+
expect(proxy.x).toBeGreaterThanOrEqual(100)
372+
expect(proxy.y).toBe(0)
373+
})
340374
})
341375

342376
describe("animate: Objects", () => {

packages/framer-motion/src/animation/animate/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export function createScopedAnimate(scope?: AnimationScope) {
101101
| DynamicAnimationOptions
102102
): AnimationPlaybackControlsWithThen {
103103
let animations: AnimationPlaybackControlsWithThen[] = []
104+
let groupOnComplete: VoidFunction | undefined
104105

105106
if (isSequence(subjectOrSequence)) {
106107
animations = animateSequence(
@@ -109,16 +110,27 @@ export function createScopedAnimate(scope?: AnimationScope) {
109110
scope
110111
)
111112
} else {
113+
// Extract top-level onComplete so it doesn't get applied per-value
114+
const { onComplete, ...rest } = options || {}
115+
if (typeof onComplete === "function") {
116+
groupOnComplete = onComplete as VoidFunction
117+
}
112118
animations = animateSubject(
113119
subjectOrSequence as ElementOrSelector,
114120
optionsOrKeyframes as DOMKeyframesDefinition,
115-
options as DynamicAnimationOptions,
121+
rest as DynamicAnimationOptions,
116122
scope
117123
)
118124
}
119125

120126
const animation = new GroupAnimationWithThen(animations)
121127

128+
if (groupOnComplete) {
129+
animation.finished.then(() => {
130+
groupOnComplete?.()
131+
})
132+
}
133+
122134
if (scope) {
123135
scope.animations.push(animation)
124136
animation.finished.then(() => {

0 commit comments

Comments
 (0)