Skip to content

Commit 6273907

Browse files
authored
Merge pull request #3294 from motiondivision/feature/delay-children
Adding support for `stagger` via `delayChildren`
2 parents f2515fd + df42e6c commit 6273907

File tree

13 files changed

+177
-29
lines changed

13 files changed

+177
-29
lines changed

dev/react/src/examples/AnimatePresence-variants.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { motion, AnimatePresence } from "framer-motion"
1+
import { AnimatePresence, motion, stagger } from "framer-motion"
22
import { useEffect, useState } from "react"
33

44
/**
@@ -26,14 +26,13 @@ const itemVariants = {
2626
const listVariants = {
2727
open: {
2828
opacity: 1,
29-
transition: { staggerChildren: 1, when: "beforeChildren" },
29+
transition: { delayChildren: stagger(1), when: "beforeChildren" },
3030
},
3131
closed: {
3232
opacity: 0,
3333
transition: {
3434
when: "afterChildren",
35-
staggerChildren: 0.3,
36-
staggerDirection: -1,
35+
delayChildren: stagger(0.3, { from: "last" }),
3736
},
3837
},
3938
}

packages/framer-motion/src/animation/__tests__/animate-waapi.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { stagger } from "motion-dom"
12
import { nextFrame } from "../../gestures/__tests__/utils"
23
import { animate } from "../animate"
34
import { defaultOptions } from "../animators/waapi/__tests__/setup"
4-
import { stagger } from "../utils/stagger"
55

66
describe("animate() with WAAPI", () => {
77
test("Can override transition options per-value", async () => {

packages/framer-motion/src/animation/interfaces/visual-element-variant.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DynamicOption } from "motion-dom"
12
import { resolveVariant } from "../../render/utils/resolve-dynamic-variants"
23
import { VisualElement } from "../../render/VisualElement"
34
import { VisualElementAnimationOptions } from "./types"
@@ -47,7 +48,8 @@ export function animateVariant(
4748
return animateChildren(
4849
visualElement,
4950
variant,
50-
delayChildren + forwardDelay,
51+
forwardDelay,
52+
delayChildren,
5153
staggerChildren,
5254
staggerDirection,
5355
options
@@ -75,20 +77,24 @@ export function animateVariant(
7577
function animateChildren(
7678
visualElement: VisualElement,
7779
variant: string,
78-
delayChildren = 0,
80+
delay: number = 0,
81+
delayChildren: number | DynamicOption<number> = 0,
7982
staggerChildren = 0,
8083
staggerDirection = 1,
8184
options: VisualElementAnimationOptions
8285
) {
8386
const animations: Promise<any>[] = []
87+
const numChildren = visualElement.variantChildren!.size
8488

85-
const maxStaggerDuration =
86-
(visualElement.variantChildren!.size - 1) * staggerChildren
89+
const maxStaggerDuration = (numChildren - 1) * staggerChildren
90+
const delayIsFunction = typeof delayChildren === "function"
8791

88-
const generateStaggerDuration =
92+
const generateStaggerDuration = delayIsFunction
93+
? (i: number) => delayChildren(i, numChildren)
94+
: // Support deprecated staggerChildren
8995
staggerDirection === 1
90-
? (i = 0) => i * staggerChildren
91-
: (i = 0) => maxStaggerDuration - i * staggerChildren
96+
? (i = 0) => i * staggerChildren
97+
: (i = 0) => maxStaggerDuration - i * staggerChildren
9298

9399
Array.from(visualElement.variantChildren!)
94100
.sort(sortByTreeOrder)
@@ -97,7 +103,10 @@ function animateChildren(
97103
animations.push(
98104
animateVariant(child, variant, {
99105
...options,
100-
delay: delayChildren + generateStaggerDuration(i),
106+
delay:
107+
delay +
108+
(delayIsFunction ? 0 : delayChildren) +
109+
generateStaggerDuration(i),
101110
}).then(() => child.notify("AnimationComplete", variant))
102111
)
103112
})

packages/framer-motion/src/animation/sequence/__tests__/index.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { motionValue, spring } from "motion-dom"
1+
import { motionValue, spring, stagger } from "motion-dom"
22
import { Easing } from "motion-utils"
3-
import { stagger } from "../../utils/stagger"
43
import { createAnimationsFromSequence } from "../create"
54

65
describe("createAnimationsFromSequence", () => {

packages/framer-motion/src/dom.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@ export * from "./animation/sequence/types"
1515
/**
1616
* Utils
1717
*/
18-
export { stagger } from "./animation/utils/stagger"
1918
export { delayInSeconds as delay, DelayedFunction } from "./utils/delay"
2019
export * from "./utils/distance"

packages/framer-motion/src/motion/__tests__/delay.test.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { motionValue, Variants } from "motion-dom"
1+
import { motionValue, stagger, Variants } from "motion-dom"
22
import { motion } from "../.."
33
import { render } from "../../jest.setup"
44

@@ -165,6 +165,43 @@ describe("delay attr", () => {
165165
requestAnimationFrame(() => resolve(x.get()))
166166
})
167167

168+
return expect(promise).resolves.toBe(0)
169+
})
170+
test("in variant children via delayChildren: stagger(interval)", async () => {
171+
const promise = new Promise((resolve) => {
172+
const x = motionValue(0)
173+
174+
const parent: Variants = {
175+
visible: {
176+
x: 10,
177+
transition: {
178+
delay: 0,
179+
delayChildren: stagger(1),
180+
type: false,
181+
},
182+
},
183+
}
184+
185+
const child: Variants = {
186+
visible: {
187+
x: 10,
188+
transition: { type: false },
189+
},
190+
}
191+
192+
const Component = () => (
193+
<motion.div variants={parent} animate="visible">
194+
<motion.div variants={child} />
195+
<motion.div variants={child} style={{ x }} />
196+
</motion.div>
197+
)
198+
199+
const { rerender } = render(<Component />)
200+
rerender(<Component />)
201+
202+
requestAnimationFrame(() => resolve(x.get()))
203+
})
204+
168205
return expect(promise).resolves.toBe(0)
169206
})
170207
})

packages/framer-motion/src/motion/__tests__/static-prop.test.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { motionValue } from "motion-dom"
1+
import { motionValue, stagger } from "motion-dom"
22
import { useEffect } from "react"
33
import { motion, useMotionValue } from "../.."
44
import { MotionConfig } from "../../components/MotionConfig"
@@ -181,9 +181,8 @@ describe("isStatic prop", () => {
181181

182182
it("accepts externally-defined transition", () => {
183183
const transition = {
184-
staggerChildren: 10,
184+
delayChildren: stagger(10, { from: "last" }),
185185
when: "beforeChildren",
186-
staggerDirection: 1,
187186
}
188187
function Test() {
189188
return (

packages/framer-motion/src/motion/__tests__/variant.test.tsx

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { motionValue, Variants } from "motion-dom"
1+
import { motionValue, stagger, Variants } from "motion-dom"
22
import { Fragment, memo, useEffect, useState } from "react"
33
import { frame, motion, MotionConfig, useMotionValue } from "../../"
44
import { nextFrame } from "../../gestures/__tests__/utils"
@@ -594,7 +594,110 @@ describe("animate prop as variant", () => {
594594
return expect(promise).resolves.toEqual([0.3, 0.5])
595595
})
596596

597-
test("Child variants correctly calculate delay based on staggerChildren", async () => {
597+
test("Child variants correctly calculate delay based on delayChildren: stagger()", async () => {
598+
const isCorrectlyStaggered = await new Promise((resolve) => {
599+
const childVariants = {
600+
hidden: { opacity: 0 },
601+
visible: { opacity: 1, transition: { duration: 0.1 } },
602+
}
603+
604+
function Component() {
605+
const a = useMotionValue(0)
606+
const b = useMotionValue(0)
607+
608+
useEffect(
609+
() =>
610+
a.on("change", (latest) => {
611+
if (latest >= 1 && b.get() === 0) resolve(true)
612+
}),
613+
[a, b]
614+
)
615+
616+
return (
617+
<motion.div
618+
variants={{
619+
hidden: {},
620+
visible: {
621+
x: 100,
622+
transition: { delayChildren: stagger(0.15) },
623+
},
624+
}}
625+
initial="hidden"
626+
animate="visible"
627+
>
628+
<motion.div
629+
variants={childVariants}
630+
style={{ opacity: a }}
631+
/>
632+
<motion.div
633+
variants={childVariants}
634+
style={{ opacity: b }}
635+
/>
636+
</motion.div>
637+
)
638+
}
639+
640+
const { rerender } = render(<Component />)
641+
rerender(<Component />)
642+
})
643+
644+
expect(isCorrectlyStaggered).toBe(true)
645+
})
646+
647+
test("Child variants with value-specific transitions correctly calculate delay based on delayChildren: stagger()", async () => {
648+
const isCorrectlyStaggered = await new Promise((resolve) => {
649+
const childVariants = {
650+
hidden: { opacity: 0 },
651+
visible: {
652+
opacity: 1,
653+
transition: { opacity: { duration: 0.1 } },
654+
},
655+
}
656+
657+
function Component() {
658+
const a = useMotionValue(0)
659+
const b = useMotionValue(0)
660+
661+
useEffect(
662+
() =>
663+
a.on("change", (latest) => {
664+
if (latest >= 1 && b.get() === 0) resolve(true)
665+
}),
666+
[a, b]
667+
)
668+
669+
return (
670+
<motion.div
671+
variants={{
672+
hidden: {},
673+
visible: {
674+
x: 100,
675+
transition: { delayChildren: stagger(0.15) },
676+
},
677+
}}
678+
initial="hidden"
679+
animate="visible"
680+
>
681+
<motion.div
682+
variants={childVariants}
683+
style={{ opacity: a }}
684+
/>
685+
<motion.div
686+
variants={childVariants}
687+
style={{ opacity: b }}
688+
/>
689+
</motion.div>
690+
)
691+
}
692+
693+
const { rerender } = render(<Component />)
694+
rerender(<Component />)
695+
})
696+
697+
expect(isCorrectlyStaggered).toBe(true)
698+
})
699+
700+
test("Child variants correctly calculate delay based on staggerChildren (deprecated)", async () => {
598701
const isCorrectlyStaggered = await new Promise((resolve) => {
599702
const childVariants = {
600703
hidden: { opacity: 0 },
@@ -644,7 +747,7 @@ describe("animate prop as variant", () => {
644747
expect(isCorrectlyStaggered).toBe(true)
645748
})
646749

647-
test("Child variants with value-specific transitions correctly calculate delay based on staggerChildren", async () => {
750+
test("Child variants with value-specific transitions correctly calculate delay based on staggerChildren (deprecated)", async () => {
648751
const isCorrectlyStaggered = await new Promise((resolve) => {
649752
const childVariants = {
650753
hidden: { opacity: 0 },

packages/motion-dom/src/animation/types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,13 @@ export interface AnimationOrchestrationOptions {
378378

379379
/**
380380
* When using variants, children animations will start after this duration
381-
* (in seconds). You can add the `transition` property to both the `Frame` and the `variant` directly. Adding it to the `variant` generally offers more flexibility, as it allows you to customize the delay per visual state.
381+
* (in seconds). You can add the `transition` property to both the `motion.div` and the
382+
* `variant` directly. Adding it to the `variant` generally offers more flexibility,
383+
* as it allows you to customize the delay per visual state.
382384
*
383385
* @public
384386
*/
385-
delayChildren?: number
387+
delayChildren?: number | DynamicOption<number>
386388

387389
/**
388390
* When using variants, animations of child components can be staggered by this
@@ -394,7 +396,7 @@ export interface AnimationOrchestrationOptions {
394396
*
395397
* The calculated stagger delay will be added to `delayChildren`.
396398
*
397-
* @public
399+
* @deprecated - Use `delayChildren: stagger(interval)` instead.
398400
*/
399401
staggerChildren?: number
400402

@@ -404,7 +406,7 @@ export interface AnimationOrchestrationOptions {
404406
* A value of `1` staggers from the first to the last while `-1`
405407
* staggers from the last to the first.
406408
*
407-
* @public
409+
* @deprecated - Use `delayChildren: stagger(interval, { from: "last" })` instead.
408410
*/
409411
staggerDirection?: number
410412
}

packages/motion-dom/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export * from "./utils/mix/number"
8888
export * from "./utils/mix/types"
8989
export * from "./utils/mix/visibility"
9090
export * from "./utils/resolve-elements"
91+
export * from "./utils/stagger"
9192
export * from "./utils/supports/flags"
9293
export * from "./utils/supports/linear-easing"
9394
export * from "./utils/supports/scroll-timeline"

0 commit comments

Comments
 (0)