-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Remove persisted styles when calling cancel() #3656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { useEffect, useRef, useState } from "react" | ||
| import { animate } from "framer-motion" | ||
|
|
||
| export const App = () => { | ||
| const ref = useRef<HTMLDivElement>(null) | ||
| const [result, setResult] = useState("running") | ||
|
|
||
| useEffect(() => { | ||
| if (!ref.current) return | ||
|
|
||
| const animation = animate( | ||
| ref.current, | ||
| { opacity: 1 }, | ||
| { duration: 0.1 } | ||
| ) | ||
|
|
||
| // Wait for animation to finish, then cancel | ||
| const timeout = setTimeout(() => { | ||
| const before = ref.current!.style.opacity | ||
|
|
||
| animation.cancel() | ||
|
|
||
| const after = ref.current!.style.opacity | ||
|
|
||
| if (before === "1" && after === "") { | ||
| setResult("success") | ||
| } else { | ||
| setResult(`fail:before=${before},after=${after}`) | ||
| } | ||
| }, 500) | ||
|
|
||
| return () => clearTimeout(timeout) | ||
| }, []) | ||
|
|
||
| return ( | ||
| <div ref={ref} className="box" id="box" style={{ opacity: 0 }}> | ||
| {result} | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| describe("animation.cancel() removes persisted styles", () => { | ||
| it("Removes persisted inline style when cancel is called after completion", () => { | ||
| cy.visit("?test=animate-cancel-removes-styles") | ||
| .wait(3000) | ||
| .get("#box") | ||
| .then(($el) => { | ||
| const text = $el.text() | ||
| expect(text).to.equal("success") | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ import { | |||||||||||||||||
| noop, | ||||||||||||||||||
| secondsToMilliseconds, | ||||||||||||||||||
| } from "motion-utils" | ||||||||||||||||||
| import { setStyle } from "../render/dom/style-set" | ||||||||||||||||||
| import { removeStyle, setStyle } from "../render/dom/style-set" | ||||||||||||||||||
| import { supportsScrollTimeline } from "../utils/supports/scroll-timeline" | ||||||||||||||||||
| import { getFinalKeyframe } from "./keyframes/get-final" | ||||||||||||||||||
| import { | ||||||||||||||||||
|
|
@@ -148,6 +148,11 @@ export class NativeAnimation<T extends AnyResolvedKeyframe> | |||||||||||||||||
| try { | ||||||||||||||||||
| this.animation.cancel() | ||||||||||||||||||
| } catch (e) {} | ||||||||||||||||||
|
|
||||||||||||||||||
| const { element, name } = this.options || {} | ||||||||||||||||||
| if (element && name && !this.isPseudoElement) { | ||||||||||||||||||
| removeStyle(element, name) | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+152
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Consider this scenario: element.style.opacity = '0' // pre-existing inline style
const anim = animate(element, { opacity: 1 }, { duration: 1 })
// 500ms in…
anim.cancel()
// Before this PR → opacity reverts to '0' (WAAPI removes its effect, inline '0' survives)
// After this PR → opacity becomes '' (removeStyle clears the pre-existing value)Because
Suggested change
Comment on lines
+152
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With the |
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| stop() { | ||||||||||||||||||
|
|
@@ -165,7 +170,11 @@ export class NativeAnimation<T extends AnyResolvedKeyframe> | |||||||||||||||||
| this.commitStyles() | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!this.isPseudoElement) this.cancel() | ||||||||||||||||||
| if (!this.isPseudoElement) { | ||||||||||||||||||
| try { | ||||||||||||||||||
| this.animation.cancel() | ||||||||||||||||||
| } catch (e) {} | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
teardown()/ double decrement ofactiveAnimations.mainThreadfinish()callsteardown()(which doesactiveAnimations.mainThread--), then setsthis.state = "finished". Ifcancel()is subsequently called, it now produces observable effects (thetick(0)revert), so users are more likely to call it after finish than before.teardown()will run a second time, decrementingactiveAnimations.mainThreadagain.This is a pre-existing issue (the
teardown()call was already incancel()before this PR), but this change makes the double-decrement reachable in normal usage for the first time.A simple guard would prevent it:
Or alternatively check
activeAnimations.mainThread > 0before decrementing insideteardown().