From 75175f0df360cce5ef5b9afb6f46739135d798f5 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 26 Sep 2022 15:54:03 +0100 Subject: [PATCH] Adding `animationValues` prop (#1712) * Adding animation values prop * Renaming to customValues * renaming to aniamtedvalues * Renaming test file * Renaming to values * Marking as internal * Fixing tests --- CHANGELOG.md | 6 + .../src/components/Reorder/Group.tsx | 4 +- .../motion/__tests__/animated-values.test.tsx | 148 ++++++++++++++++++ packages/framer-motion/src/motion/types.ts | 11 ++ .../src/motion/utils/valid-prop.ts | 1 + .../src/render/html/utils/build-styles.ts | 28 ++-- packages/framer-motion/src/render/index.ts | 17 ++ 7 files changed, 201 insertions(+), 14 deletions(-) create mode 100644 packages/framer-motion/src/motion/__tests__/animated-values.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index c466f9b898..b3786c2437 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. +## [7.4.0] 2022-09-22 + +### Added + +- Added `values` prop that allows the provision of motion values to use exclusively for performing animations on. + ## [7.3.6] 2022-09-20 ### Fixed diff --git a/packages/framer-motion/src/components/Reorder/Group.tsx b/packages/framer-motion/src/components/Reorder/Group.tsx index 3469be7dcb..47799c8ffe 100644 --- a/packages/framer-motion/src/components/Reorder/Group.tsx +++ b/packages/framer-motion/src/components/Reorder/Group.tsx @@ -68,7 +68,9 @@ export function ReorderGroup( onReorder, values, ...props - }: Props & HTMLMotionProps & React.PropsWithChildren<{}>, + }: Props & + Omit, "values"> & + React.PropsWithChildren<{}>, externalRef?: React.Ref ) { const Component = useConstant(() => motion(as)) as FunctionComponent< diff --git a/packages/framer-motion/src/motion/__tests__/animated-values.test.tsx b/packages/framer-motion/src/motion/__tests__/animated-values.test.tsx new file mode 100644 index 0000000000..81134676c1 --- /dev/null +++ b/packages/framer-motion/src/motion/__tests__/animated-values.test.tsx @@ -0,0 +1,148 @@ +import { render } from "../../../jest.setup" +import { + motion, + motionValue, + useMotionTemplate, + useMotionValue, + useTransform, +} from "../.." +import { degreesToRadians } from "popmotion" +import * as React from "react" + +describe("values prop", () => { + test("Performs animations only on motion values provided via values", async () => { + const promise = new Promise<[number, HTMLElement]>((resolve) => { + const x = motionValue(0) + const ref = React.createRef() + const Component = () => ( + resolve([x.get(), ref.current!])} + /> + ) + const { rerender } = render() + rerender() + }) + + await promise.then(([x, element]) => { + expect(x).toBe(20) + expect(element).not.toHaveStyle( + "transform: translateX(20px) translateZ(0)" + ) + }) + }) + + test("Still correctly renders values provided via style", async () => { + const promise = new Promise<[number, HTMLElement]>((resolve) => { + const x = motionValue(0) + const ref = React.createRef() + const Component = () => { + const doubleX = useTransform(x, [0, 1], [0, 2], { + clamp: false, + }) + return ( + + resolve([x.get(), ref.current!]) + } + /> + ) + } + const { rerender } = render() + rerender() + }) + + await promise.then(([x, element]) => { + expect(x).toBe(20) + expect(element).toHaveStyle( + "transform: translateX(40px) translateZ(0)" + ) + }) + }) + + test("Doesn't render custom values", async () => { + const promise = new Promise<[number, HTMLElement]>((resolve) => { + const Component = () => { + const ref = React.useRef(null) + const distance = useMotionValue(100) + const angle = useMotionValue(45) + + const x = useTransform([distance, angle], (latest) => { + return Math.floor( + Math.sin(degreesToRadians(latest[1] as number)) * + (latest[0] as number) + ) + }) + + const y = useTransform([distance, angle], (latest) => { + return Math.floor( + Math.cos(degreesToRadians(latest[1] as number)) * + (latest[0] as number) + ) + }) + + return ( + + resolve([x.get(), ref.current!]) + } + /> + ) + } + const { rerender } = render() + rerender() + }) + + await promise.then(([x, element]) => { + expect(x).toBe(35) + expect(element).toHaveStyle( + "transform: translateX(35px) translateY(35px) translateZ(0)" + ) + expect(element).not.toHaveStyle("distance: 50") + }) + }) + + test("Prioritises transform over independent transforms", async () => { + const promise = new Promise<[number, HTMLElement]>((resolve) => { + const Component = () => { + const x = useMotionValue(100) + const scale = useMotionValue(2) + const transform = useMotionTemplate`scale(${scale}) translateX(${x}px)` + const ref = React.useRef(null) + + return ( + + resolve([x.get(), ref.current!]) + } + /> + ) + } + const { rerender } = render() + rerender() + }) + + await promise.then(([x, element]) => { + expect(x).toBe(50) + expect(element).toHaveStyle("transform: scale(2) translateX(50px)") + }) + }) +}) diff --git a/packages/framer-motion/src/motion/types.ts b/packages/framer-motion/src/motion/types.ts index bb827ca4a4..473f24df72 100644 --- a/packages/framer-motion/src/motion/types.ts +++ b/packages/framer-motion/src/motion/types.ts @@ -265,6 +265,10 @@ export interface MotionAdvancedProps { inherit?: boolean } +type ExternalMotionValues = { + [key: string]: MotionValue | MotionValue +} + /** * Props for `motion` components. * @@ -295,6 +299,13 @@ export interface MotionProps */ style?: MotionStyle + /** + * Provide a set of motion values to perform animations on. + * + * @internal + */ + values?: ExternalMotionValues + /** * By default, Framer Motion generates a `transform` property with a sensible transform order. `transformTemplate` * can be used to create a different order, or to append/preprend the automatically generated `transform` property. diff --git a/packages/framer-motion/src/motion/utils/valid-prop.ts b/packages/framer-motion/src/motion/utils/valid-prop.ts index d477c91ab5..2ad9c4ffcf 100644 --- a/packages/framer-motion/src/motion/utils/valid-prop.ts +++ b/packages/framer-motion/src/motion/utils/valid-prop.ts @@ -30,6 +30,7 @@ const inViewProps = [ const validMotionProps = new Set([ "initial", "style", + "values", "variants", "transition", "transformTemplate", diff --git a/packages/framer-motion/src/render/html/utils/build-styles.ts b/packages/framer-motion/src/render/html/utils/build-styles.ts index 0dad407157..a674a2ecbd 100644 --- a/packages/framer-motion/src/render/html/utils/build-styles.ts +++ b/packages/framer-motion/src/render/html/utils/build-styles.ts @@ -67,19 +67,21 @@ export function buildHTMLStyles( } } - if (hasTransform || transformTemplate) { - style.transform = buildTransform( - state, - options, - transformIsNone, - transformTemplate - ) - } else if (!latestValues.transform && style.transform) { - /** - * If we have previously created a transform but currently don't have any, - * reset transform style to none. - */ - style.transform = "none" + if (!latestValues.transform) { + if (hasTransform || transformTemplate) { + style.transform = buildTransform( + state, + options, + transformIsNone, + transformTemplate + ) + } else if (style.transform) { + /** + * If we have previously created a transform but currently don't have any, + * reset transform style to none. + */ + style.transform = "none" + } } /** diff --git a/packages/framer-motion/src/render/index.ts b/packages/framer-motion/src/render/index.ts index b5cfb8b8ac..e658d1f576 100644 --- a/packages/framer-motion/src/render/index.ts +++ b/packages/framer-motion/src/render/index.ts @@ -179,6 +179,19 @@ export const visualElement = } } + /** + * Update external values with initial values + */ + if (props.values) { + for (const key in props.values) { + if (latestValues[key] !== undefined) { + ;(props.values[key] as MotionValue).set( + latestValues[key] + ) + } + } + } + /** * Determine what role this visual element should take in the variant tree. */ @@ -494,6 +507,10 @@ export const visualElement = * value, we'll create one if none exists. */ getValue(key: string, defaultValue?: string | number) { + if (props.values && props.values[key]) { + return props.values[key] + } + let value = values.get(key) if (value === undefined && defaultValue !== undefined) {