Skip to content

Commit

Permalink
AnimatePresence 2.0 (#2741)
Browse files Browse the repository at this point in the history
* Adding failing test

* Tidy test

* Fixing switching between components

* Fixing double render

* Cleaning

* Replacing if

* Latest

* Fixing test

* Making concurrent safe

* Removing log

* v11.3.18-alpha.0

* Updating version

* Publish

* Updating animatepresence

* Updating

* adding test file for wait

* Latest

* Updating
  • Loading branch information
mattgperry authored Jul 26, 2024
1 parent 8ee3628 commit 680d5e9
Show file tree
Hide file tree
Showing 23 changed files with 426 additions and 257 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/).

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

## [11.3.18] 2024-07-29

### Fixed

- Improved correctness of `AnimatePresence` and made safe to use with concurrent rendering.

## [11.3.17] 2024-07-24

### Added
Expand Down
4 changes: 2 additions & 2 deletions dev/react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react",
"private": true,
"version": "11.3.17",
"version": "11.3.18-alpha.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^11.3.17",
"framer-motion": "^11.3.18-alpha.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
19 changes: 9 additions & 10 deletions dev/react/src/examples/AnimatePresence-siblings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@ const style = {

function ExitComponent({ id }) {
return (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1 }}
style={style}
id={id}
/>
</>
<motion.div
key={id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1 }}
style={style}
id={id}
/>
)
}

Expand Down
42 changes: 42 additions & 0 deletions dev/react/src/examples/AnimatePresence-switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { motion, AnimatePresence } from "framer-motion"
import { useState } from "react"

/**
* An example of a single-child AnimatePresence animation
*/

const style = {
width: 100,
height: 100,
background: "red",
opacity: 1,
}

export const App = () => {
const [key, setKey] = useState("a")

return (
<div
onClick={() => {
setKey(key === "a" ? "b" : "a")
}}
>
<AnimatePresence
initial={false}
onExitComplete={() => console.log("rest")}
>
<motion.div
key={key}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1 }}
style={{
...style,
background: key === "a" ? "green" : "blue",
}}
/>
</AnimatePresence>
</div>
)
}
2 changes: 1 addition & 1 deletion dev/react/src/examples/AnimatePresence-variants.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { motion, AnimatePresence } from "framer-motion"
import { useEffect, useState } from "react";
import { useEffect, useState } from "react"

/**
* An example of AnimatePresence with exit defined as a variant through a tree.
Expand Down
43 changes: 43 additions & 0 deletions dev/react/src/examples/AnimatePresence-wait.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { motion, AnimatePresence } from "framer-motion"
import { useState } from "react"

/**
* An example of a single-child AnimatePresence animation
*/

const style = {
width: 100,
height: 100,
background: "red",
opacity: 1,
}

export const App = () => {
const [key, setKey] = useState(0)

return (
<div
onClick={() => {
setKey(key + 1)
}}
>
<AnimatePresence
initial={false}
mode="wait"
onExitComplete={() => console.log("rest")}
>
<motion.div
key={key}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1 }}
style={{
...style,
background: `hsla(${key * 15}, 100%, 50%, 1)`,
}}
/>
</AnimatePresence>
</div>
)
}
39 changes: 18 additions & 21 deletions dev/react/src/examples/AnimatePresence.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { motion, AnimatePresence } from "framer-motion"
import { useEffect, useState } from "react";
import { useState } from "react"

/**
* An example of a single-child AnimatePresence animation
Expand All @@ -15,26 +15,23 @@ const style = {
export const App = () => {
const [isVisible, setVisible] = useState(true)

useEffect(() => {
setTimeout(() => {
setVisible(!isVisible)
}, 1500)
})

return (
<AnimatePresence
initial={false}
onExitComplete={() => console.log("rest")}
>
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1 }}
style={style}
/>
)}
</AnimatePresence>
<div onClick={() => setVisible(!isVisible)}>
<AnimatePresence
initial={false}
onExitComplete={() => console.log("rest")}
>
{isVisible && (
<motion.div
key="a"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1 }}
style={style}
/>
)}
</AnimatePresence>
</div>
)
}
37 changes: 37 additions & 0 deletions dev/react/src/tests/animate-presence-switch-waapi.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { AnimatePresence, motion, useMotionValue } from "framer-motion"
import { useState } from "react"

export const App = () => {
const count = useMotionValue(0)
const [state, setState] = useState(0)

return (
<>
<button
id="switch"
onClick={() => {
state === 0 ? setState(1) : setState(0)
}}
>
Switch
</button>
<div>
Animation count: <motion.span id="count">{count}</motion.span>
</div>
<AnimatePresence initial={false}>
<motion.div
id={state.toString()}
className="item"
key={state}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onAnimationStart={() => count.set(count.get() + 1)}
>
{state}
</motion.div>
</AnimatePresence>
</>
)
}
2 changes: 1 addition & 1 deletion dev/react/src/tests/layout-exit.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"

export const App = () => {
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "11.3.17",
"version": "11.3.18-alpha.0",
"packages": [
"packages/*"
],
Expand Down
6 changes: 3 additions & 3 deletions packages/framer-motion-3d/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "framer-motion-3d",
"version": "11.3.17",
"version": "11.3.18-alpha.0",
"description": "A simple and powerful React animation library for @react-three/fiber",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
Expand Down Expand Up @@ -47,7 +47,7 @@
"postpublish": "git push --tags"
},
"dependencies": {
"framer-motion": "^11.3.17",
"framer-motion": "^11.3.18-alpha.0",
"react-merge-refs": "^2.0.1"
},
"peerDependencies": {
Expand All @@ -62,5 +62,5 @@
"@rollup/plugin-commonjs": "^22.0.1",
"three": "^0.137.0"
},
"gitHead": "7ce78149a4f0587c409a660214131a04fca04c51"
"gitHead": "f3c9de1ba4eb913f36e1f3e816f9c86784714910"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
describe("AnimatePresence with WAAPI animations", () => {
it("Interrupting exiting animation doesn't break exit", () => {
cy.visit("?test=animate-presence-switch-waapi")
.wait(50)
.get(".item")
.should((items: any) => {
expect(items.length).to.equal(1)
expect(items[0].textContent).to.equal("0")
})
.get("#switch")
.trigger("click", 10, 10, { force: true })
.wait(50)
.get(".item")
.should((items: any) => {
expect(items.length).to.equal(2)
expect(items[0].textContent).to.equal("0")
expect(items[1].textContent).to.equal("1")
})
.wait(200)
.get(".item")
.should((items: any) => {
expect(items.length).to.equal(1)
expect(items[0].textContent).to.equal("1")
})
.get("#switch")
.trigger("click", 10, 10, { force: true })
.wait(20)
.get("#switch")
.trigger("click", 10, 10, { force: true })
.wait(20)
.get("#switch")
.trigger("click", 10, 10, { force: true })
.wait(300)
.get(".item")
.should((items: any) => {
expect(items.length).to.equal(1)
expect(items[0].textContent).to.equal("0")
})
})

it("Interrupting exiting animation fire more animations than expected", () => {
cy.visit("?test=animate-presence-switch-waapi")
.wait(50)
.get(".item")
.should((items: any) => {
expect(items.length).to.equal(1)
expect(items[0].textContent).to.equal("0")
})
.get("#switch")
.trigger("click", 10, 10, { force: true })
.wait(20)
.get("#switch")
.trigger("click", 10, 10, { force: true })
.wait(300)
.get("#count")
.should((count: any) => {
expect(count[0].textContent).to.equal("4")
})
})
})
4 changes: 2 additions & 2 deletions packages/framer-motion/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "framer-motion",
"version": "11.3.17",
"version": "11.3.18-alpha.0",
"description": "A simple and powerful JavaScript animation library",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
Expand Down Expand Up @@ -104,5 +104,5 @@
"maxSize": "18 kB"
}
],
"gitHead": "7ce78149a4f0587c409a660214131a04fca04c51"
"gitHead": "f3c9de1ba4eb913f36e1f3e816f9c86784714910"
}
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ export class AcceleratedAnimation<
this.isStopped = true
if (this.state === "idle") return

this.resolveFinishedPromise()
this.updateFinishedPromise()

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { frame } from "../../frameloop"
import { resolveVariant } from "../../render/utils/resolve-dynamic-variants"
import { VisualElement } from "../../render/VisualElement"
import { AnimationDefinition } from "../types"
Expand Down Expand Up @@ -33,8 +32,6 @@ export function animateVisualElement(
}

return animation.then(() => {
frame.postRender(() => {
visualElement.notify("AnimationComplete", definition)
})
visualElement.notify("AnimationComplete", definition)
})
}
Loading

0 comments on commit 680d5e9

Please sign in to comment.