diff --git a/CHANGELOG.md b/CHANGELOG.md
index f777236d11..62818945ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
Framer Motion adheres to [Semantic Versioning](http://semver.org/).
+## [3.5.3] 2021-02-19
+
+### Fixed
+
+- Fixing bug with `afterChildren` and `exit` animations.
+
## [3.5.2] 2021-02-18
### Added
diff --git a/dev/examples/AnimatePresence-variants.tsx b/dev/examples/AnimatePresence-variants.tsx
index ac5c3e33ca..e7ea1b2243 100644
--- a/dev/examples/AnimatePresence-variants.tsx
+++ b/dev/examples/AnimatePresence-variants.tsx
@@ -26,9 +26,11 @@ const itemVariants = {
const listVariants = {
open: {
- transition: { staggerChildren: 0.07, when: "beforeChildren" },
+ opacity: 1,
+ transition: { staggerChildren: 1, when: "beforeChildren" },
},
closed: {
+ opacity: 0,
transition: {
when: "afterChildren",
staggerChildren: 0.3,
@@ -49,27 +51,25 @@ export const App = () => {
return (
console.log("rest")}>
{isVisible && (
-
-
-
- Test
-
-
- Test
-
-
- Test
-
-
-
+
+ Test
+
+
+ Test
+
+
+ Test
+
+
)}
)
diff --git a/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
index fb74798a84..7202147ec6 100644
--- a/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
+++ b/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
@@ -2,6 +2,7 @@ import { render } from "../../../../jest.setup"
import * as React from "react"
import { AnimatePresence, motion, MotionConfig, useAnimation } from "../../.."
import { motionValue } from "../../../value"
+import { ResolvedValues } from "../../../render/types"
describe("AnimatePresence", () => {
test("Allows initial animation if no `initial` prop defined", async () => {
@@ -95,6 +96,53 @@ describe("AnimatePresence", () => {
expect(child).toBeFalsy()
})
+ test("when: afterChildren fires correctly", async () => {
+ const promise = new Promise((resolve) => {
+ const parentOpacityOutput: ResolvedValues[] = []
+
+ const variants = {
+ visible: { opacity: 1 },
+ hidden: { opacity: 0 },
+ }
+
+ const Component = ({ isVisible }: { isVisible: boolean }) => {
+ return (
+
+ {isVisible && (
+ parentOpacityOutput.push(v)}
+ onAnimationComplete={() =>
+ resolve(parentOpacityOutput.length)
+ }
+ >
+
+
+ )}
+
+ )
+ }
+
+ const { rerender } = render()
+ rerender()
+ rerender()
+ rerender()
+ })
+
+ const child = await promise
+ expect(child).toBeGreaterThan(1)
+ })
+
test("Animates a component back in if it's re-added before animating out", async () => {
const promise = new Promise((resolve) => {
const Component = ({ isVisible }: { isVisible: boolean }) => {
diff --git a/src/index.ts b/src/index.ts
index 4002558752..f75296149b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -127,6 +127,7 @@ export {
Variants,
} from "./types"
export { EventInfo } from "./events/types"
+export { VisualElementLifecycles } from "./render/utils/lifecycles"
export { MotionFeature, FeatureProps } from "./motion/features/types"
export { DraggableProps, DragHandlers } from "./gestures/drag/types"
export { LayoutProps } from "./motion/features/layout/types"
diff --git a/src/render/utils/__tests__/animation-state.test.ts b/src/render/utils/__tests__/animation-state.test.ts
index 38516f8f45..1f78182d3c 100644
--- a/src/render/utils/__tests__/animation-state.test.ts
+++ b/src/render/utils/__tests__/animation-state.test.ts
@@ -79,7 +79,9 @@ describe("Animation state - Initiating props", () => {
animate: { opacity: 1 },
})
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
expect(animate).toBeCalledWith([{ opacity: 1 }])
})
@@ -94,7 +96,9 @@ describe("Animation state - Initiating props", () => {
},
})
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
expect(animate).toBeCalledWith(["test"])
})
@@ -109,7 +113,9 @@ describe("Animation state - Initiating props", () => {
},
})
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
expect(animate).toBeCalledWith(["test", "heyoo"])
})
@@ -126,7 +132,9 @@ describe("Animation state - Initiating props", () => {
},
})
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
expect(animate).not.toBeCalled()
})
@@ -139,7 +147,9 @@ describe("Animation state - Initiating props", () => {
animate: { opacity: 1 },
})
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
expect(animate).not.toBeCalled()
})
@@ -155,7 +165,9 @@ describe("Animation state - Initiating props", () => {
},
})
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
expect(animate).not.toBeCalled()
})
@@ -171,7 +183,9 @@ describe("Animation state - Initiating props", () => {
},
})
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
expect(animate).not.toBeCalled()
})
})
@@ -191,7 +205,7 @@ describe("Animation state - Setting props", () => {
})
expect(animate).not.toBeCalled()
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
@@ -209,7 +223,7 @@ describe("Animation state - Setting props", () => {
})
expect(animate).not.toBeCalled()
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
@@ -234,7 +248,7 @@ describe("Animation state - Setting props", () => {
})
expect(animate).not.toBeCalled()
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
@@ -259,7 +273,7 @@ describe("Animation state - Setting props", () => {
})
expect(animate).not.toBeCalled()
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
@@ -278,7 +292,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith([{ opacity: 0 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
test("Change single value, keyframes", () => {
@@ -295,7 +311,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith([{ opacity: [0.5, 1] }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
test("Change single value, variant", () => {
@@ -320,7 +338,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith(["b"])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
test("Change single value, variant list", () => {
@@ -345,7 +365,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith(["b"])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
test("Swap between value in target and transitionEnd, target", () => {
@@ -364,7 +386,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith([{ transitionEnd: { opacity: 0.3 } }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
animate = mockAnimate(state)
@@ -373,7 +397,9 @@ describe("Animation state - Setting props", () => {
animate: { opacity: 0.2 },
})
expect(animate).toBeCalledWith([{ opacity: 0.2 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
test("Change single value, target, with unchanging values", () => {
@@ -390,7 +416,7 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith([{ opacity: 0, x: 0 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
x: true,
})
@@ -401,7 +427,7 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith([{ opacity: 0, x: 100 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
@@ -421,7 +447,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith([{ opacity: 0 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
test("Removing values, target undefined", () => {
@@ -439,7 +467,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith([{ opacity: 0 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
/**
@@ -468,7 +498,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith(["b", { opacity: 1 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
test.skip("Removing values, inherited variant changed", () => {
@@ -493,7 +525,9 @@ describe("Animation state - Setting props", () => {
})
expect(animate).toBeCalledWith([{ opacity: 1 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
})
@@ -512,71 +546,77 @@ describe("Animation state - Set active", () => {
let animate = mockAnimate(state)
state.setActive(AnimationType.Hover, true)
expect(animate).toBeCalledWith([{ opacity: 0.5 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toHaveProperty(
- "opacity"
- )
- expect(state.getProtectedKeys(AnimationType.Hover)).toEqual({})
+ expect(
+ state.getState()[AnimationType.Animate].protectedKeys
+ ).toHaveProperty("opacity")
+ expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to false
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, false)
expect(animate).toBeCalledWith([{ opacity: 1 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
- expect(state.getProtectedKeys(AnimationType.Hover)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
+ expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to true
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, true)
expect(animate).toBeCalledWith([{ opacity: 0.5 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toHaveProperty(
- "opacity"
- )
- expect(state.getProtectedKeys(AnimationType.Hover)).toEqual({})
+ expect(
+ state.getState()[AnimationType.Animate].protectedKeys
+ ).toHaveProperty("opacity")
+ expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to false
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, false)
expect(animate).toBeCalledWith([{ opacity: 1 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
- expect(state.getProtectedKeys(AnimationType.Hover)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
+ expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to true
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, true)
expect(animate).toBeCalledWith([{ opacity: 0.5 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toHaveProperty(
- "opacity"
- )
- expect(state.getProtectedKeys(AnimationType.Hover)).toEqual({})
+ expect(
+ state.getState()[AnimationType.Animate].protectedKeys
+ ).toHaveProperty("opacity")
+ expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set press to true
animate = mockAnimate(state)
state.setActive(AnimationType.Tap, true)
expect(animate).toBeCalledWith([{ opacity: 0.8 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toHaveProperty(
- "opacity"
- )
- expect(state.getProtectedKeys(AnimationType.Hover)).toHaveProperty(
- "opacity"
- )
- expect(state.getProtectedKeys(AnimationType.Tap)).toEqual({})
+ expect(
+ state.getState()[AnimationType.Animate].protectedKeys
+ ).toHaveProperty("opacity")
+ expect(
+ state.getState()[AnimationType.Hover].protectedKeys
+ ).toHaveProperty("opacity")
+ expect(state.getState()[AnimationType.Tap].protectedKeys).toEqual({})
// Set hover to false
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, false)
- expect(state.getProtectedKeys(AnimationType.Animate)).toHaveProperty(
- "opacity"
- )
- expect(state.getProtectedKeys(AnimationType.Tap)).toHaveProperty(
- "opacity"
- )
+ expect(
+ state.getState()[AnimationType.Animate].protectedKeys
+ ).toHaveProperty("opacity")
+ expect(
+ state.getState()[AnimationType.Tap].protectedKeys
+ ).toHaveProperty("opacity")
expect(animate).not.toBeCalled()
// Set press to false
animate = mockAnimate(state)
state.setActive(AnimationType.Tap, false)
expect(animate).toBeCalledWith([{ opacity: 1 }])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
})
test("Change active variant where no variants are defined", () => {
@@ -594,14 +634,18 @@ describe("Animation state - Set active", () => {
let animate = mockAnimate(state)
state.setActive(AnimationType.Hover, true)
expect(animate).toBeCalledWith(["b"])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
- expect(state.getProtectedKeys(AnimationType.Hover)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
+ expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to false
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, false)
expect(animate).toBeCalledWith(["a"])
- expect(state.getProtectedKeys(AnimationType.Animate)).toEqual({})
- expect(state.getProtectedKeys(AnimationType.Hover)).toEqual({})
+ expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
+ {}
+ )
+ expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
})
})
diff --git a/src/render/utils/animation-state.ts b/src/render/utils/animation-state.ts
index e03b05ba72..2dd426734b 100644
--- a/src/render/utils/animation-state.ts
+++ b/src/render/utils/animation-state.ts
@@ -22,9 +22,8 @@ export interface AnimationState {
options?: AnimationOptions
) => Promise
setAnimateFunction: (fn: any) => void
- getProtectedKeys: (type: AnimationType) => { [key: string]: any }
isAnimated(key: string): boolean
- getState: () => { [key: string]: TypeState }
+ getState: () => { [key: string]: AnimationTypeState }
}
interface DefinitionAndOptions {
@@ -90,10 +89,6 @@ export function createAnimationState(
return acc
}
- function getProtectedKeys(type: AnimationType) {
- return state[type].protectedKeys
- }
-
function isAnimated(key: string) {
return allAnimatedKeys[key] !== undefined
}
@@ -255,6 +250,12 @@ export function createAnimationState(
...resolvedValues,
}
+ const markToAnimate = (key: string) => {
+ shouldAnimateType = true
+ removedKeys.delete(key)
+ typeState.needsAnimating[key] = true
+ }
+
for (const key in allKeys) {
const next = resolvedValues[key]
const prev = prevResolvedValues[key]
@@ -262,18 +263,27 @@ export function createAnimationState(
// If we've already handled this we can just skip ahead
if (encounteredKeys.hasOwnProperty(key)) continue
+ /**
+ * If the value has changed, we probably want to animate it.
+ */
if (next !== prev) {
+ /**
+ * If both values are keyframes, we need to shallow compare them to
+ * detect whether any value has changed. If it has, we animate it.
+ */
if (isKeyframesTarget(next) && isKeyframesTarget(prev)) {
if (!shallowCompare(next, prev)) {
- shouldAnimateType = true
- removedKeys.delete(key)
+ markToAnimate(key)
} else {
+ /**
+ * If it hasn't changed, we want to ensure it doesn't animate by
+ * adding it to the list of protected keys.
+ */
typeState.protectedKeys[key] = true
}
} else if (next !== undefined) {
// If next is defined and doesn't equal prev, it needs animating
- shouldAnimateType = true
- removedKeys.delete(key)
+ markToAnimate(key)
} else {
// If it's undefined, it's been removed.
removedKeys.add(key)
@@ -283,9 +293,12 @@ export function createAnimationState(
* If next hasn't changed and it isn't undefined, we want to check if it's
* been removed by a higher priority
*/
- shouldAnimateType = true
- removedKeys.delete(key)
+ markToAnimate(key)
} else {
+ /**
+ * If it hasn't changed, we add it to the list of protected values
+ * to ensure it doesn't get animated.
+ */
typeState.protectedKeys[key] = true
}
}
@@ -377,7 +390,6 @@ export function createAnimationState(
}
return {
- getProtectedKeys,
isAnimated,
animateChanges,
setActive,
@@ -396,17 +408,19 @@ export function variantsHaveChanged(prev: any, next: any) {
return false
}
-interface TypeState {
+export interface AnimationTypeState {
isActive: boolean
protectedKeys: { [key: string]: true }
+ needsAnimating: { [key: string]: boolean }
prevResolvedValues: { [key: string]: any }
prevProp?: VariantLabels | TargetAndTransition
}
-function createTypeState(isActive = false): TypeState {
+function createTypeState(isActive = false): AnimationTypeState {
return {
isActive,
protectedKeys: {},
+ needsAnimating: {},
prevResolvedValues: {},
}
}
diff --git a/src/render/utils/animation.ts b/src/render/utils/animation.ts
index a3587f5694..52d3edcc7b 100644
--- a/src/render/utils/animation.ts
+++ b/src/render/utils/animation.ts
@@ -8,7 +8,7 @@ import {
Transition,
} from "../../types"
import { VisualElement } from "../types"
-import { AnimationType } from "./animation-state"
+import { AnimationType, AnimationTypeState } from "./animation-state"
import { setTarget } from "./setters"
import { resolveVariant } from "./variants"
@@ -77,6 +77,7 @@ function animateVariant(
* If we have a variant, create a callback that runs it as an animation.
* Otherwise, we resolve a Promise immediately for a composable no-op.
*/
+
const getAnimation = resolved
? () => animateTarget(visualElement, resolved, options)
: () => Promise.resolve()
@@ -139,8 +140,8 @@ function animateTarget(
const animations: Promise[] = []
- const protectedValues =
- type && visualElement.animationState?.getProtectedKeys(type)
+ const animationTypeState =
+ type && visualElement.animationState?.getState()[type]
for (const key in target) {
const value = visualElement.getValue(key)
@@ -149,7 +150,8 @@ function animateTarget(
if (
!value ||
valueTarget === undefined ||
- protectedValues?.[key] !== undefined
+ (animationTypeState &&
+ shouldBlockAnimation(animationTypeState, key))
) {
continue
}
@@ -206,3 +208,20 @@ export function stopAnimation(visualElement: VisualElement) {
export function sortByTreeOrder(a: VisualElement, b: VisualElement) {
return a.sortNodePosition(b)
}
+
+/**
+ * Decide whether we should block this animation. Previously, we achieved this
+ * just by checking whether the key was listed in protectedKeys, but this
+ * posed problems if an animation was triggered by afterChildren and protectedKeys
+ * had been set to true in the meantime.
+ */
+function shouldBlockAnimation(
+ { protectedKeys, needsAnimating }: AnimationTypeState,
+ key: string
+) {
+ const shouldBlock =
+ protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true
+
+ needsAnimating[key] = false
+ return shouldBlock
+}