Skip to content

Commit c5dc866

Browse files
authored
Merge pull request #197 from motiondivision/feat/stagger
chore: update dependencies and clean up unused code in motion package
2 parents d0da30e + a914015 commit c5dc866

File tree

10 files changed

+465
-94
lines changed

10 files changed

+465
-94
lines changed

packages/motion/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@
7474
"vue": ">=3.0.0"
7575
},
7676
"dependencies": {
77-
"framer-motion": "12.22.0",
77+
"framer-motion": "12.23.12",
7878
"hey-listen": "^1.0.8",
79-
"motion-dom": "12.22.0"
79+
"motion-dom": "12.23.12"
8080
}
8181
}

packages/motion/src/components/__tests__/variant.test.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
22
import { mount } from '@vue/test-utils'
33
import { Motion } from '@/components'
44
import { motionValue, stagger } from 'framer-motion/dom'
5-
import { defineComponent, h, onMounted } from 'vue'
5+
import { defineComponent, h, nextTick, onMounted } from 'vue'
66

77
describe('animate prop as variant', () => {
88
it('when: beforeChildren works correctly', async () => {
@@ -419,4 +419,63 @@ describe('animate prop as variant', () => {
419419
// First stagger should start from first element
420420
expect(firstOrder).toEqual([1, 2, 3, 4])
421421
})
422+
it('staggerChildren is calculated correctly for new children', async () => {
423+
const Component = defineComponent({
424+
props: {
425+
items: {
426+
type: Array as () => string[],
427+
required: true,
428+
},
429+
},
430+
setup(props) {
431+
return () => (
432+
<Motion
433+
animate="enter"
434+
variants={{
435+
enter: { transition: { delayChildren: stagger(0.1) } },
436+
}}
437+
>
438+
{props.items.map(item => (
439+
<Motion
440+
key={item}
441+
id={item}
442+
class="item"
443+
variants={{ enter: { opacity: 1 } }}
444+
initial={{ opacity: 0 }}
445+
/>
446+
))}
447+
</Motion>
448+
)
449+
},
450+
})
451+
452+
const wrapper = mount(Component, {
453+
props: {
454+
items: ['1', '2'],
455+
},
456+
})
457+
458+
await nextTick()
459+
await nextTick()
460+
await nextTick()
461+
await nextTick()
462+
463+
await wrapper.setProps({
464+
items: ['1', '2', '3', '4', '5'],
465+
})
466+
467+
// Wait for animations to complete
468+
await new Promise(resolve => setTimeout(resolve, 1000))
469+
470+
const elements = document.querySelectorAll('.item')
471+
472+
// Check that none of the opacities are the same
473+
const opacities = Array.from(elements).map(el =>
474+
parseFloat(window.getComputedStyle(el).opacity),
475+
)
476+
477+
// All opacities should be unique
478+
const uniqueOpacities = new Set(opacities)
479+
expect(uniqueOpacities.size).toBe(opacities.length)
480+
})
422481
})

packages/motion/src/features/animation/animation.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { VisualElement } from 'framer-motion'
1313
import { animate, noop } from 'framer-motion/dom'
1414
import { createVisualElement } from '@/state/create-visual-element'
1515
import { prefersReducedMotion } from 'framer-motion/dist/es/utils/reduced-motion/state.mjs'
16+
import { calcChildStagger } from '@/features/animation/calc-child-stagger'
1617

1718
const STATE_TYPES = ['initial', 'animate', 'whileInView', 'whileHover', 'whilePress', 'whileDrag', 'whileFocus', 'exit'] as const
1819
export type StateType = typeof STATE_TYPES[number]
@@ -43,7 +44,7 @@ export class AnimationFeature extends Feature {
4344
},
4445
reducedMotionConfig: this.state.options.motionConfig.reducedMotion,
4546
})
46-
47+
this.state.visualElement.parent?.addChild(this.state.visualElement)
4748
this.state.animateUpdates = this.animateUpdates
4849
if (this.state.isMounted())
4950
this.state.startAnimation()
@@ -61,7 +62,6 @@ export class AnimationFeature extends Feature {
6162
directAnimate,
6263
directTransition,
6364
controlDelay = 0,
64-
isFallback,
6565
isExit,
6666
} = {}) => {
6767
// check if the user has reduced motion
@@ -77,9 +77,11 @@ export class AnimationFeature extends Feature {
7777
directAnimate,
7878
directTransition,
7979
})
80+
// The final transition to be applied to the state
81+
this.state.finalTransition = animationOptions
8082

8183
const factories = this.createAnimationFactories(prevTarget, animationOptions, controlDelay)
82-
const { getChildAnimations } = this.setupChildAnimations(animationOptions, this.state.activeStates, isFallback)
84+
const { getChildAnimations } = this.setupChildAnimations(animationOptions, this.state.activeStates)
8385
return this.executeAnimations({
8486
factories,
8587
getChildAnimations,
@@ -145,7 +147,6 @@ export class AnimationFeature extends Feature {
145147
setupChildAnimations(
146148
transition: $Transition | undefined,
147149
controlActiveState: Partial<Record<string, boolean>> | undefined,
148-
isFallback: boolean,
149150
) {
150151
const visualElement = this.state.visualElement
151152
if (!visualElement.variantChildren?.size || !controlActiveState)
@@ -262,6 +263,21 @@ export class AnimationFeature extends Feature {
262263
// Add state reference to visual element
263264
(this.state.visualElement as any).state = this.state
264265
this.updateAnimationControlsSubscription()
266+
267+
const visualElement = this.state.visualElement
268+
const parentVisualElement = visualElement.parent
269+
visualElement.enteringChildren = undefined
270+
/**
271+
* when current element is new entering child and it's controlled by parent,
272+
* animate it by delayChildren
273+
*/
274+
if (this.state.parent?.isMounted() && !visualElement.isControllingVariants && parentVisualElement?.enteringChildren?.has(visualElement)) {
275+
const { delayChildren } = this.state.parent.finalTransition || {};
276+
(this.animateUpdates({
277+
controlActiveState: this.state.parent.activeStates,
278+
controlDelay: calcChildStagger(parentVisualElement.enteringChildren, visualElement, delayChildren),
279+
}) as Function) ()
280+
}
265281
}
266282

267283
update() {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { VisualElement } from 'framer-motion'
2+
import type { DynamicOption } from 'motion-dom'
3+
4+
export function calcChildStagger(
5+
children: Set<VisualElement>,
6+
child: VisualElement,
7+
delayChildren?: number | DynamicOption<number>,
8+
staggerChildren: number = 0,
9+
staggerDirection: number = 1,
10+
): number {
11+
const sortedChildren = Array.from(children).sort((a, b) => a.sortNodePosition(b))
12+
const index = sortedChildren.indexOf(child)
13+
const numChildren = children.size
14+
const maxStaggerDuration = (numChildren - 1) * staggerChildren
15+
const delayIsFunction = typeof delayChildren === 'function'
16+
/**
17+
* parent may not update, so we need to clear the enteringChildren when the child is the last one
18+
*/
19+
if (index === sortedChildren.length - 1) {
20+
child.parent.enteringChildren = undefined
21+
}
22+
return delayIsFunction
23+
? delayChildren(index, numChildren)
24+
: staggerDirection === 1
25+
? index * staggerChildren
26+
: maxStaggerDuration - index * staggerChildren
27+
}

packages/motion/src/features/animation/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export interface AnimateUpdatesOptions {
1212
controlDelay?: number
1313
directAnimate?: Options['animate']
1414
directTransition?: Options['transition']
15-
isFallback?: boolean
1615
isExit?: boolean
1716
}
1817

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,4 @@ export const domAnimation = [
1515
HoverGesture,
1616
InViewGesture,
1717
FocusGesture,
18-
// ProjectionFeature,
19-
// DragGesture,
20-
// LayoutFeature,
21-
// PanGesture,
2218
] as unknown as Feature[]

packages/motion/src/state/motion-state.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MotionStateContext, Options } from '@/types'
1+
import type { $Transition, MotionStateContext, Options } from '@/types'
22
import { invariant } from 'hey-listen'
33
import type { DOMKeyframesDefinition, VisualElement } from 'framer-motion'
44
import { cancelFrame, frame, noop } from 'framer-motion/dom'
@@ -53,14 +53,15 @@ export class MotionState {
5353
*/
5454
public currentProcess: ReturnType<typeof frame.render> | null = null
5555

56-
// Depth in component tree for lifecycle ordering
57-
public depth: number
58-
5956
// Base animation target values
6057
public baseTarget: DOMKeyframesDefinition
6158

6259
// Current animation target values
6360
public target: DOMKeyframesDefinition
61+
/**
62+
* The final transition to be applied to the state
63+
*/
64+
public finalTransition: $Transition
6465
private featureManager: FeatureManager
6566

6667
// Visual element instance from Framer Motion
@@ -72,8 +73,6 @@ export class MotionState {
7273
this.parent = parent
7374
// Add to parent's children set for lifecycle management
7475
parent?.children?.add(this)
75-
// Calculate depth in component tree
76-
this.depth = parent?.depth + 1 || 0
7776

7877
// Initialize with either initial or animate variant
7978
const initial = (options.initial === undefined && options.variants) ? this.context.initial : options.initial
@@ -173,8 +172,6 @@ export class MotionState {
173172
this.featureManager.beforeUnmount()
174173
}
175174

176-
// Unmount motion state and optionally unmount children
177-
// Handles unmounting in the correct order based on component tree
178175
unmount(unMountChildren = false) {
179176
/**
180177
* Unlike React, within the same update cycle, the execution order of unmount and mount depends on the component's order in the component tree.
@@ -228,7 +225,6 @@ export class MotionState {
228225
})
229226
if (isAnimate) {
230227
this.animateUpdates({
231-
isFallback: !isActive && name !== 'exit' && this.visualElement.isControllingVariants,
232228
isExit: name === 'exit' && this.activeStates.exit,
233229
})
234230
}

packages/motion/src/utils/use-in-view.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function useInView(
1515
const isInView = ref(false)
1616

1717
watchEffect((onCleanup) => {
18-
const realOptions = unref(options) || {}
18+
const realOptions = (unref(options) || {}) as UseInViewOptions
1919
const { once } = realOptions
2020
const el = unrefElement(domRef)
2121
if (!el || (once && isInView.value)) {
@@ -31,7 +31,7 @@ export function useInView(
3131
}
3232
const cleanup = inView(el, onEnter, {
3333
...realOptions,
34-
root: unref(realOptions.root),
34+
root: unref(realOptions.root) as Element | Document,
3535
})
3636
onCleanup(() => {
3737
cleanup()

0 commit comments

Comments
 (0)