Skip to content

Commit

Permalink
Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgperry committed Mar 20, 2024
1 parent 8320563 commit 1f440f1
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 11 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/).

Undocumented APIs should be considered internal and may change without warning.

## [11.0.18] 2024-03-20

### Fixed

- Default `duration` doesn't override `duration: 0` WAAPI animations.
- Fix error when trying to animate unmounted element.
- Avoid resolving WAAPI animation when stopping unresolved animation.

## [11.0.17] 2024-03-20

### Fixed

- Interruption of WAAPI animations now animates from correct value.

## [11.0.16] 2024-03-20

### Fixed
Expand Down
60 changes: 60 additions & 0 deletions dev/tests/waapi-immediate-stop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { motionValue, AcceleratedAnimation } from "framer-motion"
import * as React from "react"
import { useEffect, useRef } from "react"
import styled from "styled-components"

const Container = styled.section`
position: relative;
display: flex;
flex-direction: column;
padding: 100px;
#box {
width: 100px;
height: 100px;
position: relative;
top: 100px;
left: 100px;
background-color: red;
opacity: 1;
}
`

export const App = () => {
const ref = useRef<HTMLDivElement>(null)

useEffect(() => {
if (!ref.current) return
const opacity = motionValue(0)
;(opacity as any).owner = { current: ref.current }
const animation = new AcceleratedAnimation({
keyframes: [null, 1],
motionValue: opacity,
name: "opacity",
})

animation.stop()

// If this animation resolved, that is incorrect
if (animation._resolved) {
ref.current.textContent = "Error"
}

new AcceleratedAnimation({
keyframes: [0.4, 0.5],
motionValue: opacity,
name: "opacity",
})

// Animation shouldn't fail if element is removed before keyframes resolve
;(opacity as any).owner.current = undefined
}, [])

return (
<Container>
<div ref={ref} id="box">
Content
</div>
</Container>
)
}
42 changes: 42 additions & 0 deletions dev/tests/waapi-zero-duration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { motion, animate } from "framer-motion"
import * as React from "react"
import { useEffect, useState } from "react"
import styled from "styled-components"

const Container = styled.section`
position: relative;
display: flex;
flex-direction: column;
padding: 100px;
#box {
width: 100px;
height: 100px;
position: relative;
top: 100px;
left: 100px;
background-color: red;
opacity: 1;
}
`

export const App = () => {
const [state, setState] = useState(false)

return (
<Container>
<motion.div
id="box"
transition={{ duration: 0 }}
initial={{ transform: "scale(1)", opacity: 1 }}
animate={{
transform: `scale(${state ? 1 : 2})`,
opacity: state ? 1 : 0,
}}
onClick={() => setState(!state)}
>
Content
</motion.div>
</Container>
)
}
31 changes: 31 additions & 0 deletions packages/framer-motion/cypress/integration/waapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,35 @@ describe("waapi", () => {
).not.to.equal(100)
})
})

it("Default duration doesn't override duration: 0", () => {
cy.visit("?test=waapi-zero-duration")
.wait(50)
.get("#box")
.should(([$element]: any) => {
expect(getComputedStyle($element).opacity).to.equal("0")
expect($element.getBoundingClientRect().width).to.equal(200)
})
.trigger("click", 250, 250, { force: true })
.wait(200)
.should(([$element]: any) => {
expect(getComputedStyle($element).opacity).to.equal("1")
expect(
Math.floor($element.getBoundingClientRect().width)
).to.equal(100)
})
})

it("Default duration doesn't override duration: 0", () => {
cy.visit("?test=waapi-immediate-stop")
.wait(100)
.get("#box")
.should(([$element]: any) => {
expect($element.innerText).not.to.equal("Error")
})
.get("iframe")
.should((result: any) => {
expect(result.length).to.equal(0)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export class AcceleratedAnimation<
super(options)

const { name, motionValue, keyframes } = this.options

this.resolver = new DOMKeyframesResolver<T>(
keyframes,
(resolvedKeyframes: ResolvedKeyframes<T>, finalKeyframe: T) =>
Expand All @@ -143,14 +144,16 @@ export class AcceleratedAnimation<
*/
private pendingTimeline: AnimationTimeline | undefined

protected initPlayback(
keyframes: ResolvedKeyframes<T>,
finalKeyframe: T
): ResolvedAcceleratedAnimation {
// TODO
// Add test for setting duration to 0
// Add test for when component has been unmounted and element is null
let duration = this.options.duration || 300
protected initPlayback(keyframes: ResolvedKeyframes<T>, finalKeyframe: T) {
let { duration = 300, motionValue, name } = this.options

/**
* If element has since been unmounted, return false to indicate
* the animation failed to initialised.
*/
if (!motionValue.owner?.current) {
return false
}

/**
* If this animation needs pre-generated keyframes then generate.
Expand All @@ -169,7 +172,6 @@ export class AcceleratedAnimation<
this.options.ease = pregeneratedAnimation.ease
}

const { motionValue, name } = this.options
const animation = animateStyle(
motionValue.owner!.current as unknown as HTMLElement,
name,
Expand Down Expand Up @@ -300,7 +302,10 @@ export class AcceleratedAnimation<
}

stop() {
this.resolver.cancel()
this.isStopped = true
if (this.state === "idle") return

const { resolved } = this
if (!resolved) return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export abstract class BaseAnimation<T extends string | number, Resolved>
protected abstract initPlayback(
keyframes: ResolvedKeyframes<T>,
finalKeyframe?: T
): Resolved
): Resolved | false

abstract play(): void
abstract pause(): void
Expand Down Expand Up @@ -130,10 +130,14 @@ export abstract class BaseAnimation<T extends string | number, Resolved>
}
}

const resolvedAnimation = this.initPlayback(keyframes, finalKeyframe)

if (resolvedAnimation === false) return

this._resolved = {
keyframes,
finalKeyframe,
...this.initPlayback(keyframes, finalKeyframe),
...resolvedAnimation,
}

this.onPostResolved()
Expand Down
1 change: 1 addition & 0 deletions packages/framer-motion/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export { complex } from "./value/types/complex"
export { px } from "./value/types/numbers/units"
export { ValueType } from "./value/types/types"
export { MotionGlobalConfig } from "./utils/GlobalConfig"
export { AcceleratedAnimation } from "./animation/animators/AcceleratedAnimation"

/**
* Appear animations
Expand Down
2 changes: 2 additions & 0 deletions packages/framer-motion/src/render/VisualElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ export abstract class VisualElement<
this.valueSubscriptions.forEach((remove) => remove())
this.removeFromVariantTree && this.removeFromVariantTree()
this.parent && this.parent.children.delete(this)

for (const key in this.events) {
this.events[key].clear()
}
Expand Down Expand Up @@ -489,6 +490,7 @@ export abstract class VisualElement<
this.valueSubscriptions.set(key, () => {
removeOnChange()
removeOnRenderRequest()
if (value.owner) value.stop()
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function measureAllKeyframes() {
}

function readAllKeyframes() {
console.log("reading all keyframes", toResolve.size)
toResolve.forEach((resolver) => {
resolver.readKeyframes()

Expand Down

0 comments on commit 1f440f1

Please sign in to comment.