Skip to content
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

AnimatePresence 2.0 #2741

Merged
merged 18 commits into from
Jul 26, 2024
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