From 02c942990c19f9ae9530ee710a54e389eefcddfc Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 18 Dec 2024 12:20:11 +0100 Subject: [PATCH 1/2] Adding tests --- packages/framer-motion/src/animation/types.ts | 27 ++++++ .../motion/__tests__/transition-out.test.tsx | 85 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 packages/framer-motion/src/motion/__tests__/transition-out.test.tsx diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index b28ff7ba7e..cecbe7f32b 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -44,6 +44,33 @@ export interface Transition duration?: number autoplay?: boolean startTime?: number + + /** + * If set to `true`, when the animated value leaves its current + * state, this transition will be used. + * + * For example: + * + * ```jsx + * + * ``` + * + * Because `whileHover` has a `transition` where `out` is `true`, + * when the hover ends, the `duration: 1` transition will be used, + * with no `delay`. + * + * @default false + */ + out?: boolean } export interface ValueAnimationTransition diff --git a/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx b/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx new file mode 100644 index 0000000000..26be777876 --- /dev/null +++ b/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx @@ -0,0 +1,85 @@ +import { render } from "../../../jest.setup" +import { motion } from "framer-motion" +import { fireEvent } from "@testing-library/react" +import { nextFrame } from "../../gestures/__tests__/utils" + +describe("transition.out", () => { + it("uses whileHover transition when exiting hover state", async () => { + const onComplete = jest.fn() + + const { container } = render( + + ) + + // Enter hover + fireEvent.mouseEnter(container.firstChild!) + + await nextFrame() + + // Exit hover - should use whileHover transition with type: false + fireEvent.mouseLeave(container.firstChild!) + + // Wait for animation to complete + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(onComplete).toHaveBeenCalled() + }) + + it("uses whileTap out transition when tap ends before hover", async () => { + const onComplete = jest.fn() + + const { container } = render( + + ) + + // Enter hover + fireEvent.mouseEnter(container.firstChild!) + + await nextFrame() + + // Start tap + fireEvent.mouseDown(container.firstChild!) + + await nextFrame() + + // End tap before ending hover + fireEvent.mouseUp(container.firstChild!) + + await nextFrame() + + // Wait a frame to ensure animation has completed + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Should use whileTap out transition which is instant + expect(onComplete).toHaveBeenCalled() + + // End hover + fireEvent.mouseLeave(container.firstChild!) + }) +}) From b6ffa460c4055877aa92ee21e3c4a8cbc347528d Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 18 Dec 2024 13:18:43 +0100 Subject: [PATCH 2/2] Fixing tests --- .../src/examples/Animation-transition-out.tsx | 29 ++++++++++ .../interfaces/visual-element-target.ts | 20 ++++++- .../motion/__tests__/transition-out.test.tsx | 53 ++++++++++++------- packages/framer-motion/src/value/index.ts | 8 ++- 4 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 dev/react/src/examples/Animation-transition-out.tsx diff --git a/dev/react/src/examples/Animation-transition-out.tsx b/dev/react/src/examples/Animation-transition-out.tsx new file mode 100644 index 0000000000..22d1b33f89 --- /dev/null +++ b/dev/react/src/examples/Animation-transition-out.tsx @@ -0,0 +1,29 @@ +import { motion } from "framer-motion" +import { useEffect, useState } from "react" + +/** + * An example of the tween transition type + */ + +const style = { + width: 100, + height: 100, + background: "white", +} +export const App = () => { + const [state, setState] = useState(false) + useEffect(() => { + setTimeout(() => { + setState(true) + }, 300) + }, [state]) + + return ( + + ) +} diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts index 696afff01d..7ef4fcb0cf 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -5,7 +5,7 @@ import type { TargetAndTransition } from "../../types" import type { VisualElementAnimationOptions } from "./types" import { animateMotionValue } from "./motion-value" import { setTarget } from "../../render/utils/setters" -import { AnimationPlaybackControls } from "../types" +import { AnimationPlaybackControls, Transition } from "../types" import { getValueTransition } from "../utils/get-value-transition" import { frame } from "../../frameloop" import { getOptimisedAppearId } from "../optimized-appear/get-appear-id" @@ -63,11 +63,27 @@ export function animateTarget( continue } - const valueTransition = { + let valueTransition = { delay, ...getValueTransition(transition || {}, key), } + let outTransition: Transition | undefined + + if (type && value.nextTransition) { + outTransition = value.nextTransition + } + + value.nextTransition = undefined + + if (valueTransition.out) { + value.nextTransition = valueTransition + } + + if (outTransition) { + valueTransition = outTransition + } + /** * If this is the first time a value is being animated, check * to see if we're handling off from an existing animation. diff --git a/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx b/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx index 26be777876..c79005d575 100644 --- a/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx @@ -1,47 +1,56 @@ -import { render } from "../../../jest.setup" -import { motion } from "framer-motion" -import { fireEvent } from "@testing-library/react" +import { + render, + pointerDown, + pointerEnter, + pointerLeave, + pointerUp, +} from "../../../jest.setup" +import { motion, motionValue } from "../../" import { nextFrame } from "../../gestures/__tests__/utils" describe("transition.out", () => { it("uses whileHover transition when exiting hover state", async () => { - const onComplete = jest.fn() + const opacity = motionValue(0) const { container } = render( ) // Enter hover - fireEvent.mouseEnter(container.firstChild!) + pointerEnter(container.firstChild as Element) await nextFrame() + await nextFrame() + + expect(opacity.get()).toBe(1) // Exit hover - should use whileHover transition with type: false - fireEvent.mouseLeave(container.firstChild!) + pointerLeave(container.firstChild as Element) // Wait for animation to complete - await new Promise((resolve) => setTimeout(resolve, 100)) + await nextFrame() + await nextFrame() - expect(onComplete).toHaveBeenCalled() + expect(opacity.get()).toBe(0) }) it("uses whileTap out transition when tap ends before hover", async () => { - const onComplete = jest.fn() + const scale = motionValue(1) const { container } = render( { transition: { type: false, out: true, - onComplete, }, }} - onAnimationComplete={onComplete} + style={{ scale }} /> ) // Enter hover - fireEvent.mouseEnter(container.firstChild!) + pointerEnter(container.firstChild as Element) await nextFrame() // Start tap - fireEvent.mouseDown(container.firstChild!) + pointerDown(container.firstChild as Element) await nextFrame() + await nextFrame() + + expect(scale.get()).toBe(0.9) // End tap before ending hover - fireEvent.mouseUp(container.firstChild!) + pointerUp(container.firstChild as Element) await nextFrame() @@ -77,9 +88,13 @@ describe("transition.out", () => { await new Promise((resolve) => setTimeout(resolve, 100)) // Should use whileTap out transition which is instant - expect(onComplete).toHaveBeenCalled() + expect(scale.get()).toBe(1.1) + + // Leave hover + pointerLeave(container.firstChild as Element) + + await new Promise((resolve) => setTimeout(resolve, 100)) - // End hover - fireEvent.mouseLeave(container.firstChild!) + expect(scale.get()).not.toBe(1) }) }) diff --git a/packages/framer-motion/src/value/index.ts b/packages/framer-motion/src/value/index.ts index fbe37990ab..e9941b4502 100644 --- a/packages/framer-motion/src/value/index.ts +++ b/packages/framer-motion/src/value/index.ts @@ -2,7 +2,7 @@ import { frame } from "../frameloop" import { SubscriptionManager } from "../utils/subscription-manager" import { velocityPerSecond } from "../utils/velocity-per-second" import { warnOnce } from "../utils/warn-once" -import { AnimationPlaybackControls } from "../animation/types" +import { AnimationPlaybackControls, Transition } from "../animation/types" import { time } from "../frameloop/sync-time" export type Transformer = (v: T) => T @@ -99,6 +99,12 @@ export class MotionValue { */ prevUpdatedAt: number | undefined + /** + * A transition that should be applied to the next animation. + * This is used to handle the `out` transition option. + */ + nextTransition?: Transition + /** * Add a passive effect to this `MotionValue`. *