diff --git a/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx index 5719dbe664..57bfbe02ec 100644 --- a/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx +++ b/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx @@ -1,5 +1,6 @@ import { render } from "../../../../jest.setup" import * as React from "react" +import { act } from "react-dom/test-utils" import { AnimatePresence, motion, MotionConfig, useAnimation } from "../../.." import { motionValue } from "../../../value" import { ResolvedValues } from "../../../render/types" @@ -59,41 +60,46 @@ describe("AnimatePresence", () => { }) test("Animates out a component when its removed", async () => { - const promise = new Promise((resolve) => { - const opacity = motionValue(1) - const Component = ({ isVisible }: { isVisible: boolean }) => { - return ( - - {isVisible && ( - - )} - - ) - } + const opacity = motionValue(1) - const { container, rerender } = render() - rerender() - rerender() + const Component = ({ isVisible }: { isVisible: boolean }) => { + return ( + + {isVisible && ( + + )} + + ) + } + + const { container, rerender } = render() + + rerender() + + await act(async () => { rerender() + }) - // Check it's animating out - setTimeout(() => { - expect(opacity.get()).not.toBe(1) - expect(opacity.get()).not.toBe(0) - }, 50) + await act(async () => { + await new Promise((resolve) => { + // Check it's animating out + setTimeout(() => { + expect(opacity.get()).not.toBe(1) + expect(opacity.get()).not.toBe(0) + }, 50) - // Check it's gone - setTimeout(() => { - resolve(container.firstChild as Element | null) - }, 150) + // Resolve after the animation is expected to have completed + setTimeout(() => { + resolve() + }, 150) + }) }) - const child = await promise - expect(child).toBeFalsy() + expect(container.firstChild).toBeFalsy() }) test("Allows nested exit animations", async () => { @@ -214,45 +220,46 @@ describe("AnimatePresence", () => { }) test("Animates a component out after having an animation cancelled", async () => { - const promise = new Promise((resolve) => { - const opacity = motionValue(1) - const Component = ({ isVisible }: { isVisible: boolean }) => { - return ( - - {isVisible && ( - - )} - - ) - } - - const { container, rerender } = render() - rerender() - rerender() - rerender() - rerender() - rerender() - rerender() - rerender() + const opacity = motionValue(1) + const Component = ({ isVisible }: { isVisible: boolean }) => { + return ( + + {isVisible && ( + + )} + + ) + } - // Check it's animating out - setTimeout(() => { - expect(opacity.get()).not.toBe(1) - expect(opacity.get()).not.toBe(0) - }, 50) + const { container, rerender } = render() + rerender() + rerender() + rerender() + rerender() + rerender() + rerender() + rerender() + + await act(async () => { + await new Promise((resolve) => { + // Check it's animating out + setTimeout(() => { + expect(opacity.get()).not.toBe(1) + expect(opacity.get()).not.toBe(0) + }, 50) - // Check it's gone - setTimeout(() => { - resolve(container.firstChild as Element | null) - }, 300) + // Resolve after the animation is expected to have completed + setTimeout(() => { + resolve() + }, 300) + }) }) - const child = await promise - expect(child).toBeFalsy() + expect(container.firstChild).toBeFalsy() }) test("Removes a child with no animations", async () => { @@ -372,54 +379,44 @@ describe("AnimatePresence", () => { enter: { x: 0, transition: { type: false } }, exit: (i: number) => ({ x: i * 100, transition: { type: false } }), } - const promise = new Promise((resolve) => { - const x = motionValue(0) - const Component = ({ - isVisible, - onAnimationComplete, - }: { - isVisible: boolean - onAnimationComplete?: () => void - }) => { - return ( - - {isVisible && ( - - )} - - ) - } - const { rerender } = render() - rerender() + const x = motionValue(0) - rerender( - resolve(x.get())} - /> + const Component = ({ + isVisible, + onAnimationComplete, + }: { + isVisible: boolean + onAnimationComplete?: () => void + }) => { + return ( + + {isVisible && ( + + )} + ) + } - rerender( - resolve(x.get())} - /> - ) + const { rerender } = render() + + rerender() + + await act(async () => { + rerender() }) - const resolvedX = await promise - expect(resolvedX).toBe(200) + expect(x.get()).toBe(200) }) test("Exit propagates through variants", async () => { @@ -584,43 +581,44 @@ describe("AnimatePresence with custom components", () => { }) test("Animates out a component when its removed", async () => { - const promise = new Promise((resolve) => { - const opacity = motionValue(1) - - const CustomComponent = () => ( - + const opacity = motionValue(1) + + const CustomComponent = () => ( + + ) + const Component = ({ isVisible }: { isVisible: boolean }) => { + return ( + + {isVisible && } + ) - const Component = ({ isVisible }: { isVisible: boolean }) => { - return ( - - {isVisible && } - - ) - } + } - const { container, rerender } = render() - rerender() - rerender() - rerender() + const { container, rerender } = render() + rerender() + rerender() + rerender() - // Check it's animating out - setTimeout(() => { - expect(opacity.get()).not.toBe(1) - expect(opacity.get()).not.toBe(0) - }, 50) + await act(async () => { + await new Promise((resolve) => { + // Check it's animating out + setTimeout(() => { + expect(opacity.get()).not.toBe(1) + expect(opacity.get()).not.toBe(0) + }, 50) - // Check it's gone - setTimeout(() => { - resolve(container.firstChild as Element | null) - }, 150) + // Resolve after the animation is expected to have completed + setTimeout(() => { + resolve() + }, 150) + }) }) - const child = await promise - expect(child).toBeFalsy() + expect(container.firstChild).toBeFalsy() }) test("Can cycle through multiple components", async () => { @@ -666,54 +664,41 @@ describe("AnimatePresence with custom components", () => { exit: (i: number) => ({ x: i * 100, transition: { type: false } }), } const x = motionValue(0) - const promise = new Promise((resolve) => { - const CustomComponent = () => ( - + const CustomComponent = () => ( + + ) + const Component = ({ + isVisible, + onAnimationComplete, + }: { + isVisible: boolean + onAnimationComplete?: () => void + }) => { + return ( + + {isVisible && } + ) - const Component = ({ - isVisible, - onAnimationComplete, - }: { - isVisible: boolean - onAnimationComplete?: () => void - }) => { - return ( - - {isVisible && } - - ) - } - - const { rerender } = render() - rerender() + } - rerender( - resolve(x.get())} - /> - ) + const { rerender } = render() + rerender() - rerender( - resolve(x.get())} - /> - ) + await act(async () => { + rerender() }) - const element = await promise - expect(element).toBe(200) + expect(x.get()).toBe(200) }) test("Exit variants are triggered with `AnimatePresence.custom` throughout the tree", async () => { @@ -725,121 +710,105 @@ describe("AnimatePresence with custom components", () => { } const xParent = motionValue(0) const xChild = motionValue(0) - const promise = new Promise((resolve) => { - const CustomComponent = ({ - children, - x, - initial, - animate, - exit, - }: { - children?: any - x: any - initial?: any - animate?: any - exit?: any - }) => ( - ( + + {children} + + ) + const Component = ({ + isVisible, + onAnimationComplete, + }: { + isVisible: boolean + onAnimationComplete?: () => void + }) => { + return ( + - {children} - + {isVisible && ( + + + + )} + ) - const Component = ({ - isVisible, - onAnimationComplete, - }: { - isVisible: boolean - onAnimationComplete?: () => void - }) => { - return ( - - {isVisible && ( - - - - )} - - ) - } + } - const { rerender } = render() - rerender() + const { rerender } = render() - rerender( - - resolve([xParent.get(), xChild.get()]) - } - /> - ) + await act(async () => { + rerender() + }) - rerender( - - resolve([xParent.get(), xChild.get()]) - } - /> - ) + await act(async () => { + rerender() }) - const latest = await promise - expect(latest).toEqual([200, 200]) + expect([xParent.get(), xChild.get()]).toEqual([200, 200]) }) test("Exit propagates through variants", async () => { const variants = { - enter: { opacity: 1 }, - exit: { opacity: 0 }, + enter: { opacity: 1, transition: { type: false } }, + exit: { opacity: 0, transition: { type: false } }, } + const opacity = motionValue(1) - const promise = new Promise((resolve) => { - const opacity = motionValue(1) - const Component = ({ isVisible }: { isVisible: boolean }) => { - return ( - - {isVisible && ( - - - - + const Component = ({ isVisible }: { isVisible: boolean }) => { + return ( + + {isVisible && ( + + + - )} - - ) - } + + )} + + ) + } - const { rerender } = render() - rerender() - rerender() + const { rerender } = render() + rerender() + await act(async () => { rerender() - - resolve(opacity.get()) }) - return await expect(promise).resolves.toBe(0) + expect(opacity.get()).toBe(0) }) }) diff --git a/src/components/AnimatePresence/index.tsx b/src/components/AnimatePresence/index.tsx index 0094f92413..c1146743f4 100644 --- a/src/components/AnimatePresence/index.tsx +++ b/src/components/AnimatePresence/index.tsx @@ -1,4 +1,5 @@ import { + useEffect, useRef, isValidElement, cloneElement, @@ -103,6 +104,11 @@ export const AnimatePresence: React.FunctionComponent = ({ const isInitialRender = useRef(true) + const isMounted = useRef(true) + useEffect(() => () => { + isMounted.current = false + }, []) + // Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key const filteredChildren = onlyElements(children) @@ -191,6 +197,9 @@ export const AnimatePresence: React.FunctionComponent = ({ // Defer re-rendering until all exiting children have indeed left if (!exiting.size) { presentChildren.current = filteredChildren + if (isMounted.current === false) { + return + } forceRender() onExitComplete && onExitComplete() }