From 111c7c0ff8774eec40145739b80d72d21760d2f6 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 19 Sep 2024 10:13:30 +0200 Subject: [PATCH] Animate values when child variant definitions change (#2804) * Animate values when child variant definitions change * Fixing test * Updating bundlesize --- packages/framer-motion/package.json | 8 ++-- .../src/motion/__tests__/variant.test.tsx | 28 ++++++++++++ .../framer-motion/src/render/VisualElement.ts | 33 -------------- packages/framer-motion/src/render/types.ts | 10 ----- .../src/render/utils/animation-state.ts | 12 ++++-- .../src/render/utils/get-variant-context.ts | 43 +++++++++++++++++++ 6 files changed, 83 insertions(+), 51 deletions(-) create mode 100644 packages/framer-motion/src/render/utils/get-variant-context.ts diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index c91217e49a..7414141e3d 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -98,15 +98,15 @@ "bundlesize": [ { "path": "./dist/size-rollup-motion.js", - "maxSize": "34.05 kB" + "maxSize": "33.95 kB" }, { "path": "./dist/size-rollup-m.js", - "maxSize": "6 kB" + "maxSize": "5.9 kB" }, { "path": "./dist/size-rollup-dom-animation.js", - "maxSize": "17 kB" + "maxSize": "16.9 kB" }, { "path": "./dist/size-rollup-dom-max.js", @@ -114,7 +114,7 @@ }, { "path": "./dist/size-rollup-animate.js", - "maxSize": "18 kB" + "maxSize": "17.9 kB" } ], "gitHead": "dcf88c8f6c178d1af7a8f7dec81326db5fd68cea" diff --git a/packages/framer-motion/src/motion/__tests__/variant.test.tsx b/packages/framer-motion/src/motion/__tests__/variant.test.tsx index 5fae8c4b14..1f048eaddb 100644 --- a/packages/framer-motion/src/motion/__tests__/variant.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/variant.test.tsx @@ -1295,4 +1295,32 @@ describe("animate prop as variant", () => { return expect(promise).resolves.toBe("visible") }) + + test("changing values within an inherited variant triggers an animation", async () => { + const Component = ({ x }: { x: number }) => { + return ( + + + + ) + } + + const { rerender, getByTestId } = render() + + await nextFrame() + + const element = getByTestId("element") + + expect(element).toHaveStyle("transform: none") + + rerender() + + await nextFrame() + + expect(element).toHaveStyle("transform: translateX(100px)") + }) }) diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index a1ae9e9a91..23e0953d64 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -18,7 +18,6 @@ import { isMotionValue } from "../value/utils/is-motion-value" import { transformProps } from "./html/utils/transform" import { ResolvedValues, - VariantStateContext, VisualElementEventCallbacks, VisualElementOptions, } from "./types" @@ -27,14 +26,12 @@ import { isControllingVariants as checkIsControllingVariants, isVariantNode as checkIsVariantNode, } from "./utils/is-controlling-variants" -import { isVariantLabel } from "./utils/is-variant-label" import { updateMotionValuesFromProps } from "./utils/motion-values" import { resolveVariantFromProps } from "./utils/resolve-variants" import { warnOnce } from "../utils/warn-once" import { featureDefinitions } from "../motion/features/definitions" import { Feature } from "../motion/features/Feature" import type { PresenceContextProps } from "../context/PresenceContext" -import { variantProps } from "./utils/variant-props" import { visualElementStore } from "./store" import { KeyframeResolver } from "./utils/KeyframesResolver" import { isNumericalString } from "../utils/is-numerical-string" @@ -54,8 +51,6 @@ const propEventHandlers = [ "LayoutAnimationComplete", ] as const -const numVariantProps = variantProps.length - /** * A VisualElement is an imperative abstraction around UI elements such as * HTMLElement, SVGElement, Three.Object3D etc. @@ -655,34 +650,6 @@ export abstract class VisualElement< : undefined } - getVariantContext(startAtParent = false): undefined | VariantStateContext { - if (startAtParent) { - return this.parent ? this.parent.getVariantContext() : undefined - } - - if (!this.isControllingVariants) { - const context = this.parent - ? this.parent.getVariantContext() || {} - : {} - if (this.props.initial !== undefined) { - context.initial = this.props.initial as any - } - return context - } - - const context = {} - for (let i = 0; i < numVariantProps; i++) { - const name = variantProps[i] as keyof typeof context - const prop = this.props[name] - - if (isVariantLabel(prop) || prop === false) { - context[name] = prop - } - } - - return context - } - /** * Add a child visual element to our set of children. */ diff --git a/packages/framer-motion/src/render/types.ts b/packages/framer-motion/src/render/types.ts index 730ba192a1..340344c200 100644 --- a/packages/framer-motion/src/render/types.ts +++ b/packages/framer-motion/src/render/types.ts @@ -16,16 +16,6 @@ export interface MotionPoint { y: MotionValue } -export type VariantStateContext = { - initial?: string | string[] - animate?: string | string[] - exit?: string | string[] - whileHover?: string | string[] - whileDrag?: string | string[] - whileFocus?: string | string[] - whileTap?: string | string[] -} - export type ScrapeMotionValuesFromProps = ( props: MotionProps, prevProps: MotionProps, diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts index ee8d631007..bd765e4873 100644 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ b/packages/framer-motion/src/render/utils/animation-state.ts @@ -12,6 +12,7 @@ import { VisualElementAnimationOptions } from "../../animation/interfaces/types" import { AnimationDefinition } from "../../animation/types" import { animateVisualElement } from "../../animation/interfaces/visual-element" import { ResolvedValues } from "../types" +import { getVariantContext } from "./get-variant-context" export interface AnimationState { animateChanges: (type?: AnimationType) => Promise @@ -96,8 +97,8 @@ export function createAnimationState( * what to animate those to. */ function animateChanges(changedActiveType?: AnimationType) { - const props = visualElement.getProps() - const context = visualElement.getVariantContext(true) || {} + const { props } = visualElement + const context = getVariantContext(visualElement.parent) || {} /** * A list of animations that we'll build into as we iterate through the animation @@ -314,9 +315,12 @@ export function createAnimationState( } /** - * If this is an inherited prop we want to hard-block animations + * If this is an inherited prop we want to skip this animation + * unless the inherited variants haven't changed on this render. */ - if (shouldAnimateType && (!isInherited || handledRemovedValues)) { + const willAnimateViaParent = isInherited && variantDidChange + const needsAnimating = !willAnimateViaParent || handledRemovedValues + if (shouldAnimateType && needsAnimating) { animations.push( ...definitionList.map((animation) => ({ animation: animation as AnimationDefinition, diff --git a/packages/framer-motion/src/render/utils/get-variant-context.ts b/packages/framer-motion/src/render/utils/get-variant-context.ts new file mode 100644 index 0000000000..f2c9089f50 --- /dev/null +++ b/packages/framer-motion/src/render/utils/get-variant-context.ts @@ -0,0 +1,43 @@ +import { VisualElement } from "../VisualElement" +import { isVariantLabel } from "./is-variant-label" +import { variantProps } from "./variant-props" + +const numVariantProps = variantProps.length + +type VariantStateContext = { + initial?: string | string[] + animate?: string | string[] + exit?: string | string[] + whileHover?: string | string[] + whileDrag?: string | string[] + whileFocus?: string | string[] + whileTap?: string | string[] +} + +export function getVariantContext( + visualElement?: VisualElement +): undefined | VariantStateContext { + if (!visualElement) return undefined + + if (!visualElement.isControllingVariants) { + const context = visualElement.parent + ? getVariantContext(visualElement.parent) || {} + : {} + if (visualElement.props.initial !== undefined) { + context.initial = visualElement.props.initial as any + } + return context + } + + const context = {} + for (let i = 0; i < numVariantProps; i++) { + const name = variantProps[i] as keyof typeof context + const prop = visualElement.props[name] + + if (isVariantLabel(prop) || prop === false) { + context[name] = prop + } + } + + return context +}