Skip to content

Commit cc40e27

Browse files
committed
Fixing cancel() after stop()
1 parent 3286649 commit cc40e27

File tree

5 files changed

+61
-22
lines changed

5 files changed

+61
-22
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ test-e2e: test-nextjs test-html test-react test-react-19
9595
yarn test-playwright
9696

9797
test-single: build test-mkdir
98-
yarn start-server-and-test "yarn dev-server" http://localhost:9991 "cd packages/framer-motion && cypress run --config-file=cypress.react-19.json --headed --spec cypress/integration/scroll.ts"
98+
yarn start-server-and-test "yarn dev-server" http://localhost:9991 "cd packages/framer-motion && cypress run --config-file=cypress.react-19.json --headed --spec cypress/integration/animate-cancel.ts"
9999

100100
lint: bootstrap
101101
yarn lint
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useAnimate } from "framer-motion"
2+
import { StrictMode, useEffect } from "react"
3+
4+
function Test() {
5+
const [scope, animate] = useAnimate()
6+
7+
useEffect(() => {
8+
const controls = animate(scope.current, { opacity: 0 }, { duration: 2 })
9+
return () => {
10+
try {
11+
controls.cancel()
12+
} catch (e) {
13+
if (scope.current) {
14+
scope.current.innerHTML = "error"
15+
}
16+
console.error(e)
17+
}
18+
}
19+
}, [])
20+
21+
return (
22+
<div
23+
ref={scope}
24+
className="box"
25+
style={{ width: "2rem", height: "2rem", background: "red" }}
26+
/>
27+
)
28+
}
29+
30+
export function App() {
31+
return (
32+
<StrictMode>
33+
<Test />
34+
</StrictMode>
35+
)
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
describe("animation.cancel()", () => {
2+
it("Doesn't throw error on unmount", () => {
3+
cy.visit("?test=animate-cancel")
4+
.get(".box")
5+
.wait(100)
6+
.should("not.contain", "error")
7+
})
8+
})

packages/motion-dom/src/animation/AsyncMotionValueAnimation.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export class AsyncMotionValueAnimation<T extends string | number>
191191

192192
get animation(): AnimationPlaybackControls {
193193
if (!this._animation) {
194+
this.keyframeResolver?.resume()
194195
flushKeyframeResolvers()
195196
}
196197

@@ -248,7 +249,11 @@ export class AsyncMotionValueAnimation<T extends string | number>
248249
}
249250

250251
cancel() {
251-
this.animation.cancel()
252+
if (this._animation) {
253+
this.animation.cancel()
254+
}
255+
256+
this.keyframeResolver?.cancel()
252257
}
253258

254259
/**
@@ -258,8 +263,8 @@ export class AsyncMotionValueAnimation<T extends string | number>
258263
if (this._animation) {
259264
this._animation.stop()
260265
this.stopTimeline?.()
261-
} else {
262-
this.keyframeResolver?.cancel()
263266
}
267+
268+
this.keyframeResolver?.cancel()
264269
}
265270
}

packages/motion-dom/src/animation/keyframes/KeyframesResolver.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,7 @@ export class KeyframeResolver<T extends string | number = any> {
109109
private motionValue?: MotionValue<T>
110110
private onComplete: OnKeyframesResolved<T>
111111

112-
/**
113-
* Track whether this resolver has completed. Once complete, it never
114-
* needs to attempt keyframe resolution again.
115-
*/
116-
private isComplete = false
112+
state: "pending" | "scheduled" | "complete" = "pending"
117113

118114
/**
119115
* Track whether this resolver is async. If it is, it'll be added to the
@@ -128,12 +124,6 @@ export class KeyframeResolver<T extends string | number = any> {
128124
*/
129125
needsMeasurement = false
130126

131-
/**
132-
* Track whether this resolver is currently scheduled to resolve
133-
* to allow it to be cancelled and resumed externally.
134-
*/
135-
isScheduled = false
136-
137127
constructor(
138128
unresolvedKeyframes: UnresolvedKeyframes<string | number>,
139129
onComplete: OnKeyframesResolved<T>,
@@ -151,7 +141,7 @@ export class KeyframeResolver<T extends string | number = any> {
151141
}
152142

153143
scheduleResolve() {
154-
this.isScheduled = true
144+
this.state = "scheduled"
155145

156146
if (this.isAsync) {
157147
toResolve.add(this)
@@ -205,26 +195,26 @@ export class KeyframeResolver<T extends string | number = any> {
205195
renderEndStyles() {}
206196
measureEndState() {}
207197

208-
complete(isForced = false) {
209-
this.isComplete = true
198+
complete(isForcedComplete = false) {
199+
this.state = "complete"
210200

211201
this.onComplete(
212202
this.unresolvedKeyframes as ResolvedKeyframes<T>,
213203
this.finalKeyframe as T,
214-
isForced
204+
isForcedComplete
215205
)
216206

217207
toResolve.delete(this)
218208
}
219209

220210
cancel() {
221-
if (!this.isComplete) {
222-
this.isScheduled = false
211+
if (this.state === "scheduled") {
223212
toResolve.delete(this)
213+
this.state = "pending"
224214
}
225215
}
226216

227217
resume() {
228-
if (!this.isComplete) this.scheduleResolve()
218+
if (this.state === "pending") this.scheduleResolve()
229219
}
230220
}

0 commit comments

Comments
 (0)