Skip to content

Commit

Permalink
Adding animationValues prop (#1712)
Browse files Browse the repository at this point in the history
* Adding animation values prop

* Renaming to customValues

* renaming to aniamtedvalues

* Renaming test file

* Renaming to values

* Marking as internal

* Fixing tests
  • Loading branch information
mattgperry authored Sep 26, 2022
1 parent cfbfc80 commit 75175f0
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 14 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/framer-motion/src/components/Reorder/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ export function ReorderGroup<V>(
onReorder,
values,
...props
}: Props<V> & HTMLMotionProps<any> & React.PropsWithChildren<{}>,
}: Props<V> &
Omit<HTMLMotionProps<any>, "values"> &
React.PropsWithChildren<{}>,
externalRef?: React.Ref<any>
) {
const Component = useConstant(() => motion(as)) as FunctionComponent<
Expand Down
148 changes: 148 additions & 0 deletions packages/framer-motion/src/motion/__tests__/animated-values.test.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>()
const Component = () => (
<motion.div
ref={ref}
values={{ x }}
animate={{ x: 20 }}
transition={{ duration: 0.01 }}
onAnimationComplete={() => resolve([x.get(), ref.current!])}
/>
)
const { rerender } = render(<Component />)
rerender(<Component />)
})

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<HTMLDivElement>()
const Component = () => {
const doubleX = useTransform(x, [0, 1], [0, 2], {
clamp: false,
})
return (
<motion.div
ref={ref}
values={{ x }}
animate={{ x: 20 }}
style={{ x: doubleX }}
transition={{ duration: 0.01 }}
onAnimationComplete={() =>
resolve([x.get(), ref.current!])
}
/>
)
}
const { rerender } = render(<Component />)
rerender(<Component />)
})

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<HTMLDivElement>(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 (
<motion.div
ref={ref}
animate={{ distance: 50 } as any}
values={{ distance }}
style={{ x, y }}
transition={{ duration: 0.01 }}
onAnimationComplete={() =>
resolve([x.get(), ref.current!])
}
/>
)
}
const { rerender } = render(<Component />)
rerender(<Component />)
})

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<HTMLDivElement>(null)

return (
<motion.div
ref={ref}
animate={{ x: 50 }}
transition={{ duration: 0.01 }}
values={{ x, scale }}
style={{ transform, scale }}
onAnimationComplete={() =>
resolve([x.get(), ref.current!])
}
/>
)
}
const { rerender } = render(<Component />)
rerender(<Component />)
})

await promise.then(([x, element]) => {
expect(x).toBe(50)
expect(element).toHaveStyle("transform: scale(2) translateX(50px)")
})
})
})
11 changes: 11 additions & 0 deletions packages/framer-motion/src/motion/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ export interface MotionAdvancedProps {
inherit?: boolean
}

type ExternalMotionValues = {
[key: string]: MotionValue<number> | MotionValue<string>
}

/**
* Props for `motion` components.
*
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/framer-motion/src/motion/utils/valid-prop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const inViewProps = [
const validMotionProps = new Set<keyof MotionProps>([
"initial",
"style",
"values",
"variants",
"transition",
"transformTemplate",
Expand Down
28 changes: 15 additions & 13 deletions packages/framer-motion/src/render/html/utils/build-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

/**
Expand Down
17 changes: 17 additions & 0 deletions packages/framer-motion/src/render/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>).set(
latestValues[key]
)
}
}
}

/**
* Determine what role this visual element should take in the variant tree.
*/
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 75175f0

Please sign in to comment.