Skip to content

Commit

Permalink
Merge pull request #1826 from framer/fix/motion-value-events
Browse files Browse the repository at this point in the history
Adding animation events to motion value
  • Loading branch information
mergetron[bot] committed Dec 15, 2022
2 parents f071d0d + bc5c76a commit e186f74
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 76 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,25 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/).

Undocumented APIs should be considered internal and may change without warning.

## [7.10.0] 2022-12-15

### Added

- `.on()` event method to `MotionValue`.
- `"animationStart"`, `"animationComplete"`, `"animationCancel"` and `"change"` events for `MotionValue`.

## [7.9.0] 2022-12-14

### Added

- Hardware-accelerated `opacity` animations.

## [7.8.1] 2022-12-14

### Changed

- Refactored animation pipeline to better accomodate WAAPI.

## [7.9.1] 2022-12-14

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions dev/examples/Drag-external-handlers.tsx
Expand Up @@ -44,7 +44,7 @@ export const App = () => {
)

useEffect(() => {
return transform.onChange(v => console.log(v))
return transform.on("change", (v) => console.log(v))
})

return (
Expand All @@ -54,7 +54,7 @@ export const App = () => {
_dragX={x}
_dragY={y}
dragConstraints={ref}
onMeasureDragConstraints={constraints => constraints}
onMeasureDragConstraints={(constraints) => constraints}
style={{
backgroundColor: color,
...child,
Expand Down
2 changes: 1 addition & 1 deletion dev/examples/ThreeSwitch.tsx
Expand Up @@ -114,7 +114,7 @@ export function useAnimatedText(target, textTransition) {
useEffect(() => {
ref.current.innerText = target.toFixed(2)

return value.onChange((v) => {
return value.on("change", (v) => {
ref.current.innerText = v.toFixed(2)
})
})
Expand Down
2 changes: 1 addition & 1 deletion dev/examples/useScroll.tsx
Expand Up @@ -47,7 +47,7 @@ export const Example = () => {
const yRange = useTransform(scrollYProgress, [0, 0.9], [0, 1])
const pathLength = useSpring(yRange, { stiffness: 400, damping: 90 })

useEffect(() => yRange.onChange((v) => setIsComplete(v >= 1)), [yRange])
useEffect(() => yRange.on("change", (v) => setIsComplete(v >= 1)), [yRange])

return (
<div
Expand Down
2 changes: 1 addition & 1 deletion dev/examples/useViewportScroll.tsx
Expand Up @@ -46,7 +46,7 @@ export const Example = () => {
const yRange = useTransform(scrollYProgress, [0, 0.9], [0, 1])
const pathLength = useSpring(yRange, { stiffness: 400, damping: 90 })

useEffect(() => yRange.onChange((v) => setIsComplete(v >= 1)), [yRange])
useEffect(() => yRange.on("change", (v) => setIsComplete(v >= 1)), [yRange])

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion dev/tests/drag-to-reorder.tsx
Expand Up @@ -13,7 +13,7 @@ const Item = ({ item, axis }) => {

useEffect(() => {
let isActive = false
axisValue.onChange((latestY) => {
axisValue.on("change", (latestY) => {
const wasActive = isActive
if (latestY !== 0) {
isActive = true
Expand Down
Expand Up @@ -385,7 +385,7 @@ describe("animate prop as object", () => {
test("respects repeatDelay prop", async () => {
const promise = new Promise<number>((resolve) => {
const x = motionValue(0)
x.onChange(() => {
x.on("change", () => {
setTimeout(() => resolve(x.get()), 50)
})
const Component = () => (
Expand Down
4 changes: 2 additions & 2 deletions packages/framer-motion/src/motion/__tests__/variant.test.tsx
Expand Up @@ -511,7 +511,7 @@ describe("animate prop as variant", () => {

React.useEffect(
() =>
a.onChange((latest) => {
a.on("change", (latest) => {
if (latest >= 1 && b.get() === 0) resolve(true)
}),
[a, b]
Expand Down Expand Up @@ -564,7 +564,7 @@ describe("animate prop as variant", () => {

React.useEffect(
() =>
a.onChange((latest) => {
a.on("change", (latest) => {
if (latest >= 1 && b.get() === 0) resolve(true)
}),
[a, b]
Expand Down
8 changes: 6 additions & 2 deletions packages/framer-motion/src/render/VisualElement.ts
Expand Up @@ -412,7 +412,8 @@ export abstract class VisualElement<
private bindToMotionValue(key: string, value: MotionValue) {
const valueIsTransform = transformProps.has(key)

const removeOnChange = value.onChange(
const removeOnChange = value.on(
"change",
(latestValue: string | number) => {
this.latestValues[key] = latestValue
this.props.onUpdate &&
Expand All @@ -424,7 +425,10 @@ export abstract class VisualElement<
}
)

const removeOnRenderRequest = value.onRenderRequest(this.scheduleRender)
const removeOnRenderRequest = value.on(
"renderRequest",
this.scheduleRender
)

this.valueSubscriptions.set(key, () => {
removeOnChange()
Expand Down
73 changes: 73 additions & 0 deletions packages/framer-motion/src/value/__tests__/index.test.ts
@@ -0,0 +1,73 @@
import { motionValue } from "../"
import { animate } from "../../animation/animate"

describe("motionValue", () => {
test("change event fires when value changes", () => {
const value = motionValue(0)
const callback = jest.fn()

value.on("change", callback)

expect(callback).not.toBeCalled()
value.set(1)
expect(callback).toBeCalledTimes(1)
value.set(1)
expect(callback).toBeCalledTimes(1)
})

test("renderRequest event fires", () => {
const value = motionValue(0)
const callback = jest.fn()

value.on("renderRequest", callback)

expect(callback).not.toBeCalled()
value.set(1)
expect(callback).toBeCalledTimes(1)
})

test("animationStart event fires", () => {
const value = motionValue(0)
const callback = jest.fn()

value.on("animationStart", callback)

expect(callback).not.toBeCalled()

animate(value, 2)

expect(callback).toBeCalledTimes(1)
})

test("animationCancel event fires", () => {
const value = motionValue(0)
const callback = jest.fn()

value.on("animationCancel", callback)

expect(callback).not.toBeCalled()

animate(value, 1)
animate(value, 2)

expect(callback).toBeCalledTimes(1)
})

test("animationComplete event fires", async () => {
const value = motionValue(0)
const callback = jest.fn()

value.on("animationComplete", callback)

expect(callback).not.toBeCalled()

animate(value, 1, { duration: 0.01 })

return new Promise<void>((resolve) => {
setTimeout(() => {
expect(callback).toBeCalledTimes(1)
resolve()
}, 100)
})
})
})
Expand Up @@ -52,8 +52,8 @@ describe("useMotionValue", () => {
const onRenderRequest = jest.fn()
const Component = () => {
const x = useMotionValue(100)
x.onChange(onChange)
x.onRenderRequest(onRenderRequest)
x.on("change", onChange)
x.on("renderRequest", onRenderRequest)

x.set(500)

Expand Down
Expand Up @@ -13,7 +13,7 @@ describe("useSpring", () => {
const x = useSpring(0)

React.useEffect(() => {
x.onChange((v) => resolve(v))
x.on("change", (v) => resolve(v))
x.set(100)
})

Expand All @@ -37,7 +37,7 @@ describe("useSpring", () => {
const y = useSpring(x)

React.useEffect(() => {
y.onChange((v) => resolve(v))
y.on("change", (v) => resolve(v))
x.set(100)
})

Expand All @@ -64,7 +64,7 @@ describe("useSpring", () => {
} as any)

React.useEffect(() => {
return y.onChange((v) => {
return y.on("change", (v) => {
if (output.length >= 10) {
resolve(output)
} else {
Expand Down Expand Up @@ -105,6 +105,7 @@ describe("useSpring", () => {
rerender(<Component target={a} />)
rerender(<Component target={a} />)

expect(((a! as any).updateSubscribers! as any).getSize()).toBe(1)
// Cast to any here as `.events` is private API
expect((a as any).events.change.getSize()).toBe(1)
})
})
Expand Up @@ -46,13 +46,13 @@ describe("useVelocity", () => {

React.useEffect(() => {
const unsubscribe = pipe(
x.onChange((v) => {
x.on("change", (v) => {
output.push(Math.round(v))
}),
xVelocity.onChange((v) => {
xVelocity.on("change", (v) => {
outputVelocity.push(Math.round(v))
}),
xAcceleration.onChange((v) => {
xAcceleration.on("change", (v) => {
outputAcceleration.push(Math.round(v))
})
)
Expand Down

0 comments on commit e186f74

Please sign in to comment.