diff --git a/CHANGELOG.md b/CHANGELOG.md
index bcca9c2b1f..60b4289a6c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/dev/react/package.json b/dev/react/package.json
index 5dc6f33938..7d10b006ac 100644
--- a/dev/react/package.json
+++ b/dev/react/package.json
@@ -1,7 +1,7 @@
{
"name": "react",
"private": true,
- "version": "11.3.17",
+ "version": "11.3.18-alpha.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -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"
},
diff --git a/dev/react/src/examples/AnimatePresence-siblings.tsx b/dev/react/src/examples/AnimatePresence-siblings.tsx
index fa13956391..6dc42bfce3 100644
--- a/dev/react/src/examples/AnimatePresence-siblings.tsx
+++ b/dev/react/src/examples/AnimatePresence-siblings.tsx
@@ -17,16 +17,15 @@ const style = {
function ExitComponent({ id }) {
return (
- <>
-
- >
+
)
}
diff --git a/dev/react/src/examples/AnimatePresence-switch.tsx b/dev/react/src/examples/AnimatePresence-switch.tsx
new file mode 100644
index 0000000000..d9f03d33ad
--- /dev/null
+++ b/dev/react/src/examples/AnimatePresence-switch.tsx
@@ -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 (
+
{
+ setKey(key === "a" ? "b" : "a")
+ }}
+ >
+
console.log("rest")}
+ >
+
+
+
+ )
+}
diff --git a/dev/react/src/examples/AnimatePresence-variants.tsx b/dev/react/src/examples/AnimatePresence-variants.tsx
index ef67c0e0ea..9ae13eaa0d 100644
--- a/dev/react/src/examples/AnimatePresence-variants.tsx
+++ b/dev/react/src/examples/AnimatePresence-variants.tsx
@@ -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.
diff --git a/dev/react/src/examples/AnimatePresence-wait.tsx b/dev/react/src/examples/AnimatePresence-wait.tsx
new file mode 100644
index 0000000000..50629e0d9d
--- /dev/null
+++ b/dev/react/src/examples/AnimatePresence-wait.tsx
@@ -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 (
+ {
+ setKey(key + 1)
+ }}
+ >
+
console.log("rest")}
+ >
+
+
+
+ )
+}
diff --git a/dev/react/src/examples/AnimatePresence.tsx b/dev/react/src/examples/AnimatePresence.tsx
index 1cb178902c..d65cbdcadb 100644
--- a/dev/react/src/examples/AnimatePresence.tsx
+++ b/dev/react/src/examples/AnimatePresence.tsx
@@ -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
@@ -15,26 +15,23 @@ const style = {
export const App = () => {
const [isVisible, setVisible] = useState(true)
- useEffect(() => {
- setTimeout(() => {
- setVisible(!isVisible)
- }, 1500)
- })
-
return (
- console.log("rest")}
- >
- {isVisible && (
-
- )}
-
+ setVisible(!isVisible)}>
+
console.log("rest")}
+ >
+ {isVisible && (
+
+ )}
+
+
)
}
diff --git a/dev/react/src/tests/animate-presence-switch-waapi.tsx b/dev/react/src/tests/animate-presence-switch-waapi.tsx
new file mode 100644
index 0000000000..35a5ffa23a
--- /dev/null
+++ b/dev/react/src/tests/animate-presence-switch-waapi.tsx
@@ -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 (
+ <>
+
+
+ Animation count: {count}
+
+
+ count.set(count.get() + 1)}
+ >
+ {state}
+
+
+ >
+ )
+}
diff --git a/dev/react/src/tests/layout-exit.tsx b/dev/react/src/tests/layout-exit.tsx
index 388382fb98..826f8d0454 100644
--- a/dev/react/src/tests/layout-exit.tsx
+++ b/dev/react/src/tests/layout-exit.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
export const App = () => {
diff --git a/lerna.json b/lerna.json
index 5ea20f1c80..e81d54bbc2 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "11.3.17",
+ "version": "11.3.18-alpha.0",
"packages": [
"packages/*"
],
diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json
index 43bfda1336..3923198a0e 100644
--- a/packages/framer-motion-3d/package.json
+++ b/packages/framer-motion-3d/package.json
@@ -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",
@@ -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": {
@@ -62,5 +62,5 @@
"@rollup/plugin-commonjs": "^22.0.1",
"three": "^0.137.0"
},
- "gitHead": "7ce78149a4f0587c409a660214131a04fca04c51"
+ "gitHead": "f3c9de1ba4eb913f36e1f3e816f9c86784714910"
}
diff --git a/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts b/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts
new file mode 100644
index 0000000000..cb3177b2a8
--- /dev/null
+++ b/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts
@@ -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")
+ })
+ })
+})
diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json
index 4533bc6849..23ebd75bed 100644
--- a/packages/framer-motion/package.json
+++ b/packages/framer-motion/package.json
@@ -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",
@@ -104,5 +104,5 @@
"maxSize": "18 kB"
}
],
- "gitHead": "7ce78149a4f0587c409a660214131a04fca04c51"
+ "gitHead": "f3c9de1ba4eb913f36e1f3e816f9c86784714910"
}
diff --git a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts
index 05a443fe96..98843c7fe3 100644
--- a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts
+++ b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts
@@ -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
diff --git a/packages/framer-motion/src/animation/interfaces/visual-element.ts b/packages/framer-motion/src/animation/interfaces/visual-element.ts
index 591a8ad351..8aeca4977e 100644
--- a/packages/framer-motion/src/animation/interfaces/visual-element.ts
+++ b/packages/framer-motion/src/animation/interfaces/visual-element.ts
@@ -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"
@@ -33,8 +32,6 @@ export function animateVisualElement(
}
return animation.then(() => {
- frame.postRender(() => {
- visualElement.notify("AnimationComplete", definition)
- })
+ visualElement.notify("AnimationComplete", definition)
})
}
diff --git a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
index b05437c379..385c2679ed 100644
--- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
+++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
@@ -153,7 +153,7 @@ describe("AnimatePresence", () => {
})
test("when: afterChildren fires correctly", async () => {
- const child = await new Promise((resolve) => {
+ const child = await new Promise(async (resolve) => {
const parentOpacityOutput: ResolvedValues[] = []
const variants = {
@@ -191,6 +191,8 @@ describe("AnimatePresence", () => {
const { rerender } = render()
rerender()
+ await nextFrame()
+ await nextFrame()
rerender()
rerender()
})
@@ -397,6 +399,7 @@ describe("AnimatePresence", () => {
return (
{
rerender()
setTimeout(() => {
rerender()
- }, 50)
- setTimeout(() => {
- rerender()
- // wait for the exit animation to check the DOM again
+
setTimeout(() => {
- resolve(getByTestId("2").textContent === "2")
- }, 150)
- }, 200)
+ rerender()
+ // wait for the exit animation to check the DOM again
+ setTimeout(() => {
+ resolve(getByTestId("2").textContent === "2")
+ }, 250)
+ }, 50)
+ }, 50)
})
return await expect(promise).resolves.toBeTruthy()
diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx
index 108c36f14c..7479795c12 100644
--- a/packages/framer-motion/src/components/AnimatePresence/index.tsx
+++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx
@@ -1,46 +1,12 @@
-import {
- useRef,
- isValidElement,
- cloneElement,
- Children,
- ReactElement,
- ReactNode,
- useContext,
-} from "react"
+import { useContext, useState, useMemo, useRef } from "react"
import * as React from "react"
import { AnimatePresenceProps } from "./types"
-import { useForceUpdate } from "../../utils/use-force-update"
-import { useIsMounted } from "../../utils/use-is-mounted"
import { PresenceChild } from "./PresenceChild"
import { LayoutGroupContext } from "../../context/LayoutGroupContext"
-import { useIsomorphicLayoutEffect } from "../../utils/use-isomorphic-effect"
-import { useUnmountEffect } from "../../utils/use-unmount-effect"
import { invariant } from "../../utils/errors"
-
-type ComponentKey = string | number
-
-const getChildKey = (child: ReactElement): ComponentKey => child.key || ""
-
-function updateChildLookup(
- children: ReactElement[],
- allChildren: Map>
-) {
- children.forEach((child) => {
- const key = getChildKey(child)
- allChildren.set(key, child)
- })
-}
-
-function onlyElements(children: ReactNode): ReactElement[] {
- const filtered: ReactElement[] = []
-
- // We use forEach here instead of map as map mutates the component key by preprending `.$`
- Children.forEach(children, (child) => {
- if (isValidElement(child)) filtered.push(child)
- })
-
- return filtered
-}
+import { useIsomorphicLayoutEffect } from "../../three-entry"
+import { useConstant } from "../../utils/use-constant"
+import { ComponentKey, arrayEquals, getChildKey, onlyElements } from "./utils"
/**
* `AnimatePresence` enables the animation of components that have been removed from the tree.
@@ -79,201 +45,182 @@ export const AnimatePresence: React.FunctionComponent<
React.PropsWithChildren
> = ({
children,
+ exitBeforeEnter,
custom,
initial = true,
onExitComplete,
- exitBeforeEnter,
presenceAffectsLayout = true,
mode = "sync",
}) => {
invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'")
- // We want to force a re-render once all exiting animations have finished. We
- // either use a local forceRender function, or one from a parent context if it exists.
- const forceRender =
- useContext(LayoutGroupContext).forceRender || useForceUpdate()[0]
-
- const isMounted = useIsMounted()
-
- // Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key
- const filteredChildren = onlyElements(children)
- let childrenToRender = filteredChildren
-
- const exitingChildren = useRef(
- new Map | undefined>()
- ).current
-
- // Keep a living record of the children we're actually rendering so we
- // can diff to figure out which are entering and exiting
- const presentChildren = useRef(childrenToRender)
-
- // A lookup table to quickly reference components by key
- const allChildren = useRef(
- new Map>()
- ).current
-
- // If this is the initial component render, just deal with logic surrounding whether
- // we play onMount animations or not.
+ /**
+ * Filter any children that aren't ReactElements. We can only track components
+ * between renders with a props.key.
+ */
+ const presentChildren = useMemo(() => onlyElements(children), [children])
+
+ /**
+ * Track the keys of the currently rendered children. This is used to
+ * determine which children are exiting.
+ */
+ const presentKeys = presentChildren.map(getChildKey)
+
+ /**
+ * If `initial={false}` we only want to pass this to components in the first render.
+ */
const isInitialRender = useRef(true)
+ /**
+ * A ref containing the currently present children. When all exit animations
+ * are complete, we use this to re-render the component with the latest children
+ * *committed* rather than the latest children *rendered*.
+ */
+ const pendingPresentChildren = useRef(presentChildren)
+
+ /**
+ * Track which exiting children have finished animating out.
+ */
+ const exitComplete = useConstant(() => new Map())
+
+ /**
+ * Save children to render as React state. To ensure this component is concurrent-safe,
+ * we check for exiting children via an effect.
+ */
+ const [diffedChildren, setDiffedChildren] = useState(presentChildren)
+ const [renderedChildren, setRenderedChildren] = useState(presentChildren)
+
useIsomorphicLayoutEffect(() => {
isInitialRender.current = false
+ pendingPresentChildren.current = presentChildren
- updateChildLookup(filteredChildren, allChildren)
- presentChildren.current = childrenToRender
- })
+ /**
+ * Update complete status of exiting children.
+ */
+ for (let i = 0; i < renderedChildren.length; i++) {
+ const key = getChildKey(renderedChildren[i])
- useUnmountEffect(() => {
- isInitialRender.current = true
- allChildren.clear()
- exitingChildren.clear()
- })
-
- if (isInitialRender.current) {
- return (
- <>
- {childrenToRender.map((child) => (
-
- {child}
-
- ))}
- >
- )
- }
-
- // If this is a subsequent render, deal with entering and exiting children
- childrenToRender = [...childrenToRender]
-
- // Diff the keys of the currently-present and target children to update our
- // exiting list.
- const presentKeys = presentChildren.current.map(getChildKey)
- const targetKeys = filteredChildren.map(getChildKey)
-
- // Diff the present children with our target children and mark those that are exiting
- const numPresent = presentKeys.length
- for (let i = 0; i < numPresent; i++) {
- const key = presentKeys[i]
-
- if (targetKeys.indexOf(key) === -1 && !exitingChildren.has(key)) {
- exitingChildren.set(key, undefined)
+ if (!presentKeys.includes(key)) {
+ if (exitComplete.get(key) !== true) {
+ exitComplete.set(key, false)
+ }
+ } else {
+ exitComplete.delete(key)
+ }
}
- }
+ }, [renderedChildren, presentKeys.length, presentKeys.join("-")])
- // If we currently have exiting children, and we're deferring rendering incoming children
- // until after all current children have exiting, empty the childrenToRender array
- if (mode === "wait" && exitingChildren.size) {
- childrenToRender = []
- }
-
- // Loop through all currently exiting components and clone them to overwrite `animate`
- // with any `exit` prop they might have defined.
- exitingChildren.forEach((component, key) => {
- // If this component is actually entering again, early return
- if (targetKeys.indexOf(key) !== -1) return
-
- const child = allChildren.get(key)
- if (!child) return
-
- const insertionIndex = presentKeys.indexOf(key)
+ const exitingChildren = []
- let exitingComponent = component
- if (!exitingComponent) {
- const onExit = () => {
- // clean up the exiting children map
- exitingChildren.delete(key)
+ if (presentChildren !== diffedChildren) {
+ let nextChildren = [...presentChildren]
- // compute the keys of children that were rendered once but are no longer present
- // this could happen in case of too many fast consequent renderings
- // @link https://github.com/framer/motion/issues/2023
- const leftOverKeys = Array.from(allChildren.keys()).filter(
- (childKey) => !targetKeys.includes(childKey)
- )
-
- // clean up the all children map
- leftOverKeys.forEach((leftOverKey) =>
- allChildren.delete(leftOverKey)
- )
+ /**
+ * Loop through all the currently rendered components and decide which
+ * are exiting.
+ */
+ for (let i = 0; i < renderedChildren.length; i++) {
+ const child = renderedChildren[i]
+ const key = getChildKey(child)
- // make sure to render only the children that are actually visible
- presentChildren.current = filteredChildren.filter(
- (presentChild) => {
- const presentChildKey = getChildKey(presentChild)
+ if (!presentKeys.includes(key)) {
+ nextChildren.splice(i, 0, child)
+ exitingChildren.push(child)
+ }
+ }
- return (
- // filter out the node exiting
- presentChildKey === key ||
- // filter out the leftover children
- leftOverKeys.includes(presentChildKey)
- )
- }
- )
+ /**
+ * If we're in "wait" mode, and we have exiting children, we want to
+ * only render these until they've all exited.
+ */
+ if (mode === "wait" && exitingChildren.length) {
+ nextChildren = exitingChildren
+ }
- // Defer re-rendering until all exiting children have indeed left
- if (!exitingChildren.size) {
- if (isMounted.current === false) return
+ nextChildren = onlyElements(nextChildren)
- forceRender()
- onExitComplete && onExitComplete()
- }
- }
+ const childrenHaveChanged = !arrayEquals(
+ nextChildren.map(getChildKey),
+ renderedChildren.map(getChildKey)
+ )
- exitingComponent = (
-
- {child}
-
- )
- exitingChildren.set(key, exitingComponent)
+ if (childrenHaveChanged) {
+ setRenderedChildren(nextChildren)
}
- childrenToRender.splice(insertionIndex, 0, exitingComponent)
- })
+ setDiffedChildren(presentChildren)
- // Add `MotionContext` even to children that don't need it to ensure we're rendering
- // the same tree between renders
- childrenToRender = childrenToRender.map((child) => {
- const key = child.key as string | number
- return exitingChildren.has(key) ? (
- child
- ) : (
-
- {child}
-
- )
- })
+ /**
+ * Early return to ensure once we've set state with the latest diffed
+ * children, we can immediately re-render.
+ */
+ return
+ }
if (
process.env.NODE_ENV !== "production" &&
mode === "wait" &&
- childrenToRender.length > 1
+ renderedChildren.length > 1
) {
console.warn(
`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`
)
}
+ /**
+ * If we've been provided a forceRender function by the LayoutGroupContext,
+ * we can use it to force a re-render amongst all surrounding components once
+ * all components have finished animating out.
+ */
+ const { forceRender } = useContext(LayoutGroupContext)
+
return (
<>
- {exitingChildren.size
- ? childrenToRender
- : childrenToRender.map((child) => cloneElement(child))}
+ {renderedChildren.map((child) => {
+ const key = getChildKey(child)
+
+ const isPresent =
+ presentChildren === renderedChildren ||
+ presentKeys.includes(key)
+
+ const onExit = () => {
+ if (exitComplete.has(key)) {
+ exitComplete.set(key, true)
+ } else {
+ return
+ }
+
+ let isEveryExitComplete = true
+ exitComplete.forEach((isExitComplete) => {
+ if (!isExitComplete) isEveryExitComplete = false
+ })
+
+ if (isEveryExitComplete) {
+ forceRender?.()
+ setRenderedChildren(pendingPresentChildren.current)
+
+ onExitComplete && onExitComplete()
+ }
+ }
+
+ return (
+
+ {child}
+
+ )
+ })}
>
)
}
diff --git a/packages/framer-motion/src/components/AnimatePresence/utils.ts b/packages/framer-motion/src/components/AnimatePresence/utils.ts
new file mode 100644
index 0000000000..6e3630b347
--- /dev/null
+++ b/packages/framer-motion/src/components/AnimatePresence/utils.ts
@@ -0,0 +1,27 @@
+import { isValidElement, Children, ReactElement, ReactNode } from "react"
+
+export type ComponentKey = string | number
+
+export const getChildKey = (child: ReactElement): ComponentKey =>
+ child.key || ""
+
+export function onlyElements(children: ReactNode): ReactElement[] {
+ const filtered: ReactElement[] = []
+
+ // We use forEach here instead of map as map mutates the component key by preprending `.$`
+ Children.forEach(children, (child) => {
+ if (isValidElement(child)) filtered.push(child)
+ })
+
+ return filtered
+}
+
+export function arrayEquals(a: any[], b: any[]) {
+ if (a.length !== b.length) return false
+
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) return false
+ }
+
+ return true
+}
diff --git a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx
index 340021857d..79694bd489 100644
--- a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx
+++ b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx
@@ -1,6 +1,6 @@
import { focus, blur, render } from "../../../jest.setup"
-import { createRef } from "react";
-import { motion, motionValue } from "../../"
+import { createRef } from "react"
+import { frame, motion, motionValue } from "../../"
import { nextFrame } from "./utils"
describe("focus", () => {
@@ -142,7 +142,9 @@ describe("focus", () => {
const opacity = motionValue(1)
let blurred = false
- const onComplete = () => blurred && resolve(opacity.get())
+ const onComplete = () => {
+ frame.postRender(() => blurred && resolve(opacity.get()))
+ }
const Component = ({ onAnimationComplete }: any) => (
{
const opacity = motionValue(1)
let hasMousedOut = false
- const onComplete = () => hasMousedOut && resolve(opacity.get())
+ const onComplete = () => {
+ frame.postRender(() => hasMousedOut && resolve(opacity.get()))
+ }
const Component = ({ onAnimationComplete }: any) => (
{
},
}
const display = motionValue("block")
- const onComplete = () => resolve(display.get())
+ const onComplete = () => {
+ frame.postRender(() => resolve(display.get()))
+ }
const Component = () => (