Skip to content

transition.out #2951

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions dev/react/src/examples/Animation-transition-out.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<motion.div
animate={{ scale: 1, transition: { duration: 1 } }}
whileHover={{ scale: 1.1, transition: { duration: 1 } }}
whileTap={{ scale: 0.9, transition: { out: true, type: false } }}
style={style}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions packages/framer-motion/src/animation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <motion.div
* animate={{
* opacity: 0,
* transition: { delay: 1 }
* }}
* whileHover={{
* opacity: 1,
* transition: { out: true, duration: 1 }
* }}
* />
* ```
*
* 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<V = any>
Expand Down
100 changes: 100 additions & 0 deletions packages/framer-motion/src/motion/__tests__/transition-out.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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 opacity = motionValue(0)

const { container } = render(
<motion.div
animate={{ opacity: 0, transition: { duration: 1 } }}
transition={{ duration: 1 }}
whileHover={{
opacity: 1,
transition: {
type: false,
out: true,
},
}}
style={{ opacity }}
/>
)

// Enter hover
pointerEnter(container.firstChild as Element)

await nextFrame()
await nextFrame()

expect(opacity.get()).toBe(1)

// Exit hover - should use whileHover transition with type: false
pointerLeave(container.firstChild as Element)

// Wait for animation to complete
await nextFrame()
await nextFrame()

expect(opacity.get()).toBe(0)
})

it("uses whileTap out transition when tap ends before hover", async () => {
const scale = motionValue(1)

const { container } = render(
<motion.div
animate={{ scale: 1, transition: { duration: 1 } }}
whileHover={{
scale: 1.1,
transition: { duration: 1 },
}}
whileTap={{
scale: 0.9,
transition: {
type: false,
out: true,
},
}}
style={{ scale }}
/>
)

// Enter hover
pointerEnter(container.firstChild as Element)

await nextFrame()

// Start tap
pointerDown(container.firstChild as Element)

await nextFrame()
await nextFrame()

expect(scale.get()).toBe(0.9)

// End tap before ending hover
pointerUp(container.firstChild as Element)

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(scale.get()).toBe(1.1)

// Leave hover
pointerLeave(container.firstChild as Element)

await new Promise((resolve) => setTimeout(resolve, 100))

expect(scale.get()).not.toBe(1)
})
})
8 changes: 7 additions & 1 deletion packages/framer-motion/src/value/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = (v: T) => T
Expand Down Expand Up @@ -99,6 +99,12 @@ export class MotionValue<V = any> {
*/
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`.
*
Expand Down