@fluentui/react-motion-components-preview 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -2
- package/dist/index.d.ts +117 -0
- package/lib/choreography/Stagger/Stagger.js +189 -0
- package/lib/choreography/Stagger/Stagger.js.map +1 -0
- package/lib/choreography/Stagger/index.js +1 -0
- package/lib/choreography/Stagger/index.js.map +1 -0
- package/lib/choreography/Stagger/stagger-types.js +1 -0
- package/lib/choreography/Stagger/stagger-types.js.map +1 -0
- package/lib/choreography/Stagger/useStaggerItemsVisibility.js +176 -0
- package/lib/choreography/Stagger/useStaggerItemsVisibility.js.map +1 -0
- package/lib/choreography/Stagger/utils/constants.js +5 -0
- package/lib/choreography/Stagger/utils/constants.js.map +1 -0
- package/lib/choreography/Stagger/utils/getStaggerChildMapping.js +29 -0
- package/lib/choreography/Stagger/utils/getStaggerChildMapping.js.map +1 -0
- package/lib/choreography/Stagger/utils/index.js +4 -0
- package/lib/choreography/Stagger/utils/index.js.map +1 -0
- package/lib/choreography/Stagger/utils/motionComponentDetection.js +83 -0
- package/lib/choreography/Stagger/utils/motionComponentDetection.js.map +1 -0
- package/lib/choreography/Stagger/utils/stagger-calculations.js +71 -0
- package/lib/choreography/Stagger/utils/stagger-calculations.js.map +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib-commonjs/choreography/Stagger/Stagger.js +137 -0
- package/lib-commonjs/choreography/Stagger/Stagger.js.map +1 -0
- package/lib-commonjs/choreography/Stagger/index.js +11 -0
- package/lib-commonjs/choreography/Stagger/index.js.map +1 -0
- package/lib-commonjs/choreography/Stagger/stagger-types.js +6 -0
- package/lib-commonjs/choreography/Stagger/stagger-types.js.map +1 -0
- package/lib-commonjs/choreography/Stagger/useStaggerItemsVisibility.js +161 -0
- package/lib-commonjs/choreography/Stagger/useStaggerItemsVisibility.js.map +1 -0
- package/lib-commonjs/choreography/Stagger/utils/constants.js +23 -0
- package/lib-commonjs/choreography/Stagger/utils/constants.js.map +1 -0
- package/lib-commonjs/choreography/Stagger/utils/getStaggerChildMapping.js +27 -0
- package/lib-commonjs/choreography/Stagger/utils/getStaggerChildMapping.js.map +1 -0
- package/lib-commonjs/choreography/Stagger/utils/index.js +37 -0
- package/lib-commonjs/choreography/Stagger/utils/index.js.map +1 -0
- package/lib-commonjs/choreography/Stagger/utils/motionComponentDetection.js +48 -0
- package/lib-commonjs/choreography/Stagger/utils/motionComponentDetection.js.map +1 -0
- package/lib-commonjs/choreography/Stagger/utils/stagger-calculations.js +76 -0
- package/lib-commonjs/choreography/Stagger/utils/stagger-calculations.js.map +1 -0
- package/lib-commonjs/index.js +4 -0
- package/lib-commonjs/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# Change Log - @fluentui/react-motion-components-preview
|
|
2
2
|
|
|
3
|
-
This log was last generated on Thu, 06 Nov 2025
|
|
3
|
+
This log was last generated on Thu, 06 Nov 2025 17:24:17 GMT and should not be manually modified.
|
|
4
4
|
|
|
5
5
|
<!-- Start content -->
|
|
6
6
|
|
|
7
|
+
## [0.14.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-motion-components-preview_v0.14.0)
|
|
8
|
+
|
|
9
|
+
Thu, 06 Nov 2025 17:24:17 GMT
|
|
10
|
+
[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-motion-components-preview_v0.13.0..@fluentui/react-motion-components-preview_v0.14.0)
|
|
11
|
+
|
|
12
|
+
### Minor changes
|
|
13
|
+
|
|
14
|
+
- feat(motion): Add Stagger choreography component ([PR #34705](https://github.com/microsoft/fluentui/pull/34705) by robertpenner@microsoft.com)
|
|
15
|
+
|
|
7
16
|
## [0.13.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-motion-components-preview_v0.13.0)
|
|
8
17
|
|
|
9
|
-
Thu, 06 Nov 2025
|
|
18
|
+
Thu, 06 Nov 2025 15:01:23 GMT
|
|
10
19
|
[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-motion-components-preview_v0.12.0..@fluentui/react-motion-components-preview_v0.13.0)
|
|
11
20
|
|
|
12
21
|
### Minor changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AtomMotion } from '@fluentui/react-motion';
|
|
2
2
|
import { PresenceComponent } from '@fluentui/react-motion';
|
|
3
|
+
import { PresenceComponentProps } from '@fluentui/react-motion';
|
|
3
4
|
import type { PresenceDirection } from '@fluentui/react-motion';
|
|
5
|
+
import * as React_2 from 'react';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Common opacity animation parameter for motion components.
|
|
@@ -277,4 +279,119 @@ export declare const SlideRelaxed: PresenceComponent<SlideParams>;
|
|
|
277
279
|
|
|
278
280
|
export declare const SlideSnappy: PresenceComponent<SlideParams>;
|
|
279
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Stagger is a component that manages the staggered entrance and exit of its children.
|
|
284
|
+
* Children are animated in sequence with configurable timing between each item.
|
|
285
|
+
* Stagger can be interactively toggled between entrance and exit animations using the `visible` prop.
|
|
286
|
+
*
|
|
287
|
+
* @param children - React elements to animate. Elements are cloned with animation props.
|
|
288
|
+
* @param visible - Controls animation direction. When `true`, the group is animating "enter" (items shown);
|
|
289
|
+
* when `false`, the group is animating "exit" (items hidden). Defaults to `false`.
|
|
290
|
+
* @param itemDelay - Milliseconds between each item's animation start.
|
|
291
|
+
* Defaults to the package's default delay (see `DEFAULT_ITEM_DELAY`).
|
|
292
|
+
* @param itemDuration - Milliseconds each item's animation lasts. Only used with `delayMode="timing"`.
|
|
293
|
+
* Defaults to the package's default item duration (see `DEFAULT_ITEM_DURATION`).
|
|
294
|
+
* @param reversed - Whether to reverse the stagger sequence (last item animates first). Defaults to `false`.
|
|
295
|
+
* @param hideMode - How children's visibility/mounting is managed. Auto-detects if not specified.
|
|
296
|
+
* @param delayMode - How staggering timing is implemented. Auto-detects if not specified.
|
|
297
|
+
* @param onMotionFinish - Callback invoked when the staggered animation sequence completes.
|
|
298
|
+
*
|
|
299
|
+
* **Auto-detection behavior:**
|
|
300
|
+
* - **hideMode**: Presence components use `'visibleProp'`, DOM elements use `'visibilityStyle'`
|
|
301
|
+
* - **delayMode**: Components with delay support use `'delayProp'` (most performant), others use `'timing'`
|
|
302
|
+
*
|
|
303
|
+
* **hideMode options:**
|
|
304
|
+
* - `'visibleProp'`: Children are presence components with `visible` prop (always rendered, visibility controlled via prop)
|
|
305
|
+
* - `'visibilityStyle'`: Children remain in DOM with inline style visibility: hidden/visible (preserves layout space)
|
|
306
|
+
* - `'unmount'`: Children are mounted/unmounted from DOM based on visibility
|
|
307
|
+
*
|
|
308
|
+
* **delayMode options:**
|
|
309
|
+
* - `'timing'`: Manages visibility over time using JavaScript timing
|
|
310
|
+
* - `'delayProp'`: Passes delay props to motion components to use native Web Animations API delays (most performant)
|
|
311
|
+
*
|
|
312
|
+
* **Static variants:**
|
|
313
|
+
* - `<Stagger.In>` - One-way stagger for entrance animations only (auto-detects optimal modes)
|
|
314
|
+
* - `<Stagger.Out>` - One-way stagger for exit animations only (auto-detects optimal modes)
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```tsx
|
|
318
|
+
* import { Stagger, Fade, Scale, Rotate } from '@fluentui/react-motion-components-preview';
|
|
319
|
+
*
|
|
320
|
+
* // Auto-detects optimal modes for presence components (delayProp + visibleProp)
|
|
321
|
+
* <Stagger visible={isVisible} itemDelay={150}>
|
|
322
|
+
* <Fade><div>Item 2</div></Fade>
|
|
323
|
+
* <Scale><div>Item 1</div></Scale>
|
|
324
|
+
* <Rotate><div>Item 3</div></Rotate>
|
|
325
|
+
* </Stagger>
|
|
326
|
+
*
|
|
327
|
+
* // Auto-detects optimal modes for motion components (delayProp + unmount)
|
|
328
|
+
* <Stagger.In itemDelay={100}>
|
|
329
|
+
* <Scale.In><div>Item 1</div></Scale.In>
|
|
330
|
+
* <Fade.In><div>Item 2</div></Fade.In>
|
|
331
|
+
* </Stagger.In>
|
|
332
|
+
*
|
|
333
|
+
* // Auto-detects timing mode for DOM elements (timing + visibilityStyle)
|
|
334
|
+
* <Stagger visible={isVisible} itemDelay={150} onMotionFinish={handleComplete}>
|
|
335
|
+
* <div>Item 1</div>
|
|
336
|
+
* <div>Item 2</div>
|
|
337
|
+
* <div>Item 3</div>
|
|
338
|
+
* </Stagger>
|
|
339
|
+
*
|
|
340
|
+
* // Override auto-detection when needed
|
|
341
|
+
* <Stagger visible={isVisible} delayMode="timing" hideMode="unmount">
|
|
342
|
+
* <CustomComponent>Item 1</CustomComponent>
|
|
343
|
+
* </Stagger>
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
export declare const Stagger: React_2.FC<StaggerProps> & {
|
|
347
|
+
In: React_2.FC<Omit<StaggerProps, "visible">>;
|
|
348
|
+
Out: React_2.FC<Omit<StaggerProps, "visible">>;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Defines how Stagger implements the timing of staggered animations.
|
|
353
|
+
* - 'timing': Manages visibility over time using JavaScript timing (current behavior)
|
|
354
|
+
* - 'delayProp': Passes delay props to motion components to use native Web Animations API delays
|
|
355
|
+
*/
|
|
356
|
+
declare type StaggerDelayMode = 'timing' | 'delayProp';
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Defines how Stagger manages its children's visibility/mounting.
|
|
360
|
+
* - 'visibleProp': Children are components with a `visible` prop (e.g. motion components)
|
|
361
|
+
* - 'visibilityStyle': Children remain in DOM with inline style `visibility: hidden | visible`
|
|
362
|
+
* - 'unmount': Children are mounted/unmounted from DOM based on visibility
|
|
363
|
+
*/
|
|
364
|
+
declare type StaggerHideMode = 'visibleProp' | 'visibilityStyle' | 'unmount';
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Props for the Stagger component that manages staggered entrance and exit animations.
|
|
368
|
+
*/
|
|
369
|
+
export declare interface StaggerProps {
|
|
370
|
+
/** React elements to animate. Elements are cloned with animation props. */
|
|
371
|
+
children: React_2.ReactNode;
|
|
372
|
+
/**
|
|
373
|
+
* Controls children animation direction. When `true`, the group is animating "enter" (items shown).
|
|
374
|
+
* When `false`, the group is animating "exit" (items hidden).
|
|
375
|
+
*/
|
|
376
|
+
visible?: PresenceComponentProps['visible'];
|
|
377
|
+
/** Whether to reverse the stagger sequence (last item animates first). Defaults to `false`. */
|
|
378
|
+
reversed?: boolean;
|
|
379
|
+
/**
|
|
380
|
+
* Milliseconds between each child's animation start.
|
|
381
|
+
* Defaults to the package's default delay (see `DEFAULT_ITEM_DELAY`).
|
|
382
|
+
*/
|
|
383
|
+
itemDelay?: number;
|
|
384
|
+
/**
|
|
385
|
+
* Milliseconds each child's animation lasts. Only used with `delayMode="timing"`.
|
|
386
|
+
* Defaults to the package's default duration (see `DEFAULT_ITEM_DURATION`).
|
|
387
|
+
*/
|
|
388
|
+
itemDuration?: number;
|
|
389
|
+
/** How children's visibility/mounting is managed. Auto-detects if not specified. */
|
|
390
|
+
hideMode?: StaggerHideMode;
|
|
391
|
+
/** How staggering timing is implemented. Defaults to 'timing'. */
|
|
392
|
+
delayMode?: StaggerDelayMode;
|
|
393
|
+
/** Callback invoked when the staggered animation sequence completes. */
|
|
394
|
+
onMotionFinish?: () => void;
|
|
395
|
+
}
|
|
396
|
+
|
|
280
397
|
export { }
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useStaggerItemsVisibility } from './useStaggerItemsVisibility';
|
|
4
|
+
import { DEFAULT_ITEM_DURATION, DEFAULT_ITEM_DELAY, acceptsVisibleProp, acceptsDelayProps, getStaggerChildMapping } from './utils';
|
|
5
|
+
/**
|
|
6
|
+
* Shared utility to detect optimal stagger modes based on children components.
|
|
7
|
+
* Consolidates the auto-detection logic used by both StaggerMain and createStaggerDirection.
|
|
8
|
+
*/ const detectStaggerModes = (children, options)=>{
|
|
9
|
+
const { hideMode, delayMode, fallbackHideMode = 'visibilityStyle' } = options;
|
|
10
|
+
const childMapping = getStaggerChildMapping(children);
|
|
11
|
+
const elements = Object.values(childMapping).map((item)=>item.element);
|
|
12
|
+
const hasVisiblePropSupport = elements.every((child)=>acceptsVisibleProp(child));
|
|
13
|
+
const hasDelayPropSupport = elements.every((child)=>acceptsDelayProps(child));
|
|
14
|
+
return {
|
|
15
|
+
hideMode: hideMode !== null && hideMode !== void 0 ? hideMode : hasVisiblePropSupport ? 'visibleProp' : fallbackHideMode,
|
|
16
|
+
delayMode: delayMode !== null && delayMode !== void 0 ? delayMode : hasDelayPropSupport ? 'delayProp' : 'timing'
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const StaggerOneWay = ({ children, direction, itemDelay = DEFAULT_ITEM_DELAY, itemDuration = DEFAULT_ITEM_DURATION, reversed = false, hideMode, delayMode = 'timing', onMotionFinish })=>{
|
|
20
|
+
const childMapping = React.useMemo(()=>getStaggerChildMapping(children), [
|
|
21
|
+
children
|
|
22
|
+
]);
|
|
23
|
+
// Always call hooks at the top level, regardless of delayMode
|
|
24
|
+
const { itemsVisibility } = useStaggerItemsVisibility({
|
|
25
|
+
childMapping,
|
|
26
|
+
itemDelay,
|
|
27
|
+
itemDuration,
|
|
28
|
+
direction,
|
|
29
|
+
reversed,
|
|
30
|
+
onMotionFinish,
|
|
31
|
+
hideMode
|
|
32
|
+
});
|
|
33
|
+
// For delayProp mode, pass delay props directly to motion components
|
|
34
|
+
if (delayMode === 'delayProp') {
|
|
35
|
+
return /*#__PURE__*/ React.createElement(React.Fragment, null, Object.entries(childMapping).map(([key, { element, index }])=>{
|
|
36
|
+
const staggerIndex = reversed ? Object.keys(childMapping).length - 1 - index : index;
|
|
37
|
+
const delay = staggerIndex * itemDelay;
|
|
38
|
+
// Clone element with delay prop (for enter direction) or exitDelay prop (for exit direction)
|
|
39
|
+
const delayProp = direction === 'enter' ? {
|
|
40
|
+
delay
|
|
41
|
+
} : {
|
|
42
|
+
exitDelay: delay
|
|
43
|
+
};
|
|
44
|
+
// Only set visible prop if the component supports it
|
|
45
|
+
// Set visible based on direction: true for enter, false for exit
|
|
46
|
+
const visibleProp = acceptsVisibleProp(element) ? {
|
|
47
|
+
visible: direction === 'enter'
|
|
48
|
+
} : {};
|
|
49
|
+
return /*#__PURE__*/ React.cloneElement(element, {
|
|
50
|
+
key,
|
|
51
|
+
...visibleProp,
|
|
52
|
+
...delayProp
|
|
53
|
+
});
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
// For timing mode, use the existing timing-based implementation
|
|
57
|
+
return /*#__PURE__*/ React.createElement(React.Fragment, null, Object.entries(childMapping).map(([key, { element }])=>{
|
|
58
|
+
if (hideMode === 'visibleProp') {
|
|
59
|
+
// Use a generic record type for props to avoid `any` while still allowing unknown prop shapes.
|
|
60
|
+
return /*#__PURE__*/ React.cloneElement(element, {
|
|
61
|
+
key,
|
|
62
|
+
visible: itemsVisibility[key]
|
|
63
|
+
});
|
|
64
|
+
} else if (hideMode === 'visibilityStyle') {
|
|
65
|
+
const childProps = element.props;
|
|
66
|
+
const style = {
|
|
67
|
+
...childProps === null || childProps === void 0 ? void 0 : childProps.style,
|
|
68
|
+
visibility: itemsVisibility[key] ? 'visible' : 'hidden'
|
|
69
|
+
};
|
|
70
|
+
return /*#__PURE__*/ React.cloneElement(element, {
|
|
71
|
+
key,
|
|
72
|
+
style
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
// unmount mode
|
|
76
|
+
return itemsVisibility[key] ? /*#__PURE__*/ React.cloneElement(element, {
|
|
77
|
+
key
|
|
78
|
+
}) : null;
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
};
|
|
82
|
+
// Shared helper for StaggerIn and StaggerOut
|
|
83
|
+
const createStaggerDirection = (direction)=>{
|
|
84
|
+
const StaggerDirection = ({ hideMode, delayMode, children, ...props })=>{
|
|
85
|
+
// Auto-detect modes for better performance with motion components
|
|
86
|
+
const { hideMode: resolvedHideMode, delayMode: resolvedDelayMode } = detectStaggerModes(children, {
|
|
87
|
+
hideMode,
|
|
88
|
+
delayMode,
|
|
89
|
+
// One-way stagger falls back to visibilityStyle if it doesn't detect visibleProp support
|
|
90
|
+
fallbackHideMode: 'visibilityStyle'
|
|
91
|
+
});
|
|
92
|
+
return /*#__PURE__*/ React.createElement(StaggerOneWay, {
|
|
93
|
+
...props,
|
|
94
|
+
children: children,
|
|
95
|
+
direction: direction,
|
|
96
|
+
hideMode: resolvedHideMode,
|
|
97
|
+
delayMode: resolvedDelayMode
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
return StaggerDirection;
|
|
101
|
+
};
|
|
102
|
+
const StaggerIn = createStaggerDirection('enter');
|
|
103
|
+
const StaggerOut = createStaggerDirection('exit');
|
|
104
|
+
// Main Stagger component with auto-detection or explicit modes
|
|
105
|
+
const StaggerMain = (props)=>{
|
|
106
|
+
const { children, visible = false, hideMode, delayMode, ...rest } = props;
|
|
107
|
+
// Auto-detect modes for bidirectional stagger
|
|
108
|
+
const { hideMode: resolvedHideMode, delayMode: resolvedDelayMode } = detectStaggerModes(children, {
|
|
109
|
+
hideMode,
|
|
110
|
+
delayMode,
|
|
111
|
+
// Bidirectional stagger falls back to visibilityStyle if it doesn't detect visibleProp support
|
|
112
|
+
fallbackHideMode: 'visibilityStyle'
|
|
113
|
+
});
|
|
114
|
+
const direction = visible ? 'enter' : 'exit';
|
|
115
|
+
return /*#__PURE__*/ React.createElement(StaggerOneWay, {
|
|
116
|
+
...rest,
|
|
117
|
+
children: children,
|
|
118
|
+
hideMode: resolvedHideMode,
|
|
119
|
+
delayMode: resolvedDelayMode,
|
|
120
|
+
direction: direction
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Stagger is a component that manages the staggered entrance and exit of its children.
|
|
125
|
+
* Children are animated in sequence with configurable timing between each item.
|
|
126
|
+
* Stagger can be interactively toggled between entrance and exit animations using the `visible` prop.
|
|
127
|
+
*
|
|
128
|
+
* @param children - React elements to animate. Elements are cloned with animation props.
|
|
129
|
+
* @param visible - Controls animation direction. When `true`, the group is animating "enter" (items shown);
|
|
130
|
+
* when `false`, the group is animating "exit" (items hidden). Defaults to `false`.
|
|
131
|
+
* @param itemDelay - Milliseconds between each item's animation start.
|
|
132
|
+
* Defaults to the package's default delay (see `DEFAULT_ITEM_DELAY`).
|
|
133
|
+
* @param itemDuration - Milliseconds each item's animation lasts. Only used with `delayMode="timing"`.
|
|
134
|
+
* Defaults to the package's default item duration (see `DEFAULT_ITEM_DURATION`).
|
|
135
|
+
* @param reversed - Whether to reverse the stagger sequence (last item animates first). Defaults to `false`.
|
|
136
|
+
* @param hideMode - How children's visibility/mounting is managed. Auto-detects if not specified.
|
|
137
|
+
* @param delayMode - How staggering timing is implemented. Auto-detects if not specified.
|
|
138
|
+
* @param onMotionFinish - Callback invoked when the staggered animation sequence completes.
|
|
139
|
+
*
|
|
140
|
+
* **Auto-detection behavior:**
|
|
141
|
+
* - **hideMode**: Presence components use `'visibleProp'`, DOM elements use `'visibilityStyle'`
|
|
142
|
+
* - **delayMode**: Components with delay support use `'delayProp'` (most performant), others use `'timing'`
|
|
143
|
+
*
|
|
144
|
+
* **hideMode options:**
|
|
145
|
+
* - `'visibleProp'`: Children are presence components with `visible` prop (always rendered, visibility controlled via prop)
|
|
146
|
+
* - `'visibilityStyle'`: Children remain in DOM with inline style visibility: hidden/visible (preserves layout space)
|
|
147
|
+
* - `'unmount'`: Children are mounted/unmounted from DOM based on visibility
|
|
148
|
+
*
|
|
149
|
+
* **delayMode options:**
|
|
150
|
+
* - `'timing'`: Manages visibility over time using JavaScript timing
|
|
151
|
+
* - `'delayProp'`: Passes delay props to motion components to use native Web Animations API delays (most performant)
|
|
152
|
+
*
|
|
153
|
+
* **Static variants:**
|
|
154
|
+
* - `<Stagger.In>` - One-way stagger for entrance animations only (auto-detects optimal modes)
|
|
155
|
+
* - `<Stagger.Out>` - One-way stagger for exit animations only (auto-detects optimal modes)
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```tsx
|
|
159
|
+
* import { Stagger, Fade, Scale, Rotate } from '@fluentui/react-motion-components-preview';
|
|
160
|
+
*
|
|
161
|
+
* // Auto-detects optimal modes for presence components (delayProp + visibleProp)
|
|
162
|
+
* <Stagger visible={isVisible} itemDelay={150}>
|
|
163
|
+
* <Fade><div>Item 2</div></Fade>
|
|
164
|
+
* <Scale><div>Item 1</div></Scale>
|
|
165
|
+
* <Rotate><div>Item 3</div></Rotate>
|
|
166
|
+
* </Stagger>
|
|
167
|
+
*
|
|
168
|
+
* // Auto-detects optimal modes for motion components (delayProp + unmount)
|
|
169
|
+
* <Stagger.In itemDelay={100}>
|
|
170
|
+
* <Scale.In><div>Item 1</div></Scale.In>
|
|
171
|
+
* <Fade.In><div>Item 2</div></Fade.In>
|
|
172
|
+
* </Stagger.In>
|
|
173
|
+
*
|
|
174
|
+
* // Auto-detects timing mode for DOM elements (timing + visibilityStyle)
|
|
175
|
+
* <Stagger visible={isVisible} itemDelay={150} onMotionFinish={handleComplete}>
|
|
176
|
+
* <div>Item 1</div>
|
|
177
|
+
* <div>Item 2</div>
|
|
178
|
+
* <div>Item 3</div>
|
|
179
|
+
* </Stagger>
|
|
180
|
+
*
|
|
181
|
+
* // Override auto-detection when needed
|
|
182
|
+
* <Stagger visible={isVisible} delayMode="timing" hideMode="unmount">
|
|
183
|
+
* <CustomComponent>Item 1</CustomComponent>
|
|
184
|
+
* </Stagger>
|
|
185
|
+
* ```
|
|
186
|
+
*/ export const Stagger = Object.assign(StaggerMain, {
|
|
187
|
+
In: StaggerIn,
|
|
188
|
+
Out: StaggerOut
|
|
189
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/choreography/Stagger/Stagger.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useStaggerItemsVisibility } from './useStaggerItemsVisibility';\nimport {\n DEFAULT_ITEM_DURATION,\n DEFAULT_ITEM_DELAY,\n acceptsVisibleProp,\n acceptsDelayProps,\n getStaggerChildMapping,\n} from './utils';\nimport { StaggerOneWayProps, StaggerProps, type StaggerHideMode, type StaggerDelayMode } from './stagger-types';\n\n/**\n * Shared utility to detect optimal stagger modes based on children components.\n * Consolidates the auto-detection logic used by both StaggerMain and createStaggerDirection.\n */\nconst detectStaggerModes = (\n children: React.ReactNode,\n options: {\n hideMode?: StaggerHideMode;\n delayMode?: StaggerDelayMode;\n fallbackHideMode?: StaggerHideMode;\n },\n): { hideMode: StaggerHideMode; delayMode: StaggerDelayMode } => {\n const { hideMode, delayMode, fallbackHideMode = 'visibilityStyle' } = options;\n\n const childMapping = getStaggerChildMapping(children);\n const elements = Object.values(childMapping).map(item => item.element);\n const hasVisiblePropSupport = elements.every(child => acceptsVisibleProp(child));\n const hasDelayPropSupport = elements.every(child => acceptsDelayProps(child));\n\n return {\n hideMode: hideMode ?? (hasVisiblePropSupport ? 'visibleProp' : fallbackHideMode),\n delayMode: delayMode ?? (hasDelayPropSupport ? 'delayProp' : 'timing'),\n };\n};\n\nconst StaggerOneWay: React.FC<StaggerOneWayProps> = ({\n children,\n direction,\n itemDelay = DEFAULT_ITEM_DELAY,\n itemDuration = DEFAULT_ITEM_DURATION,\n reversed = false,\n hideMode,\n delayMode = 'timing',\n onMotionFinish,\n}) => {\n const childMapping = React.useMemo(() => getStaggerChildMapping(children), [children]);\n\n // Always call hooks at the top level, regardless of delayMode\n const { itemsVisibility } = useStaggerItemsVisibility({\n childMapping,\n itemDelay,\n itemDuration,\n direction,\n reversed,\n onMotionFinish,\n hideMode,\n });\n\n // For delayProp mode, pass delay props directly to motion components\n if (delayMode === 'delayProp') {\n return (\n <>\n {Object.entries(childMapping).map(([key, { element, index }]) => {\n const staggerIndex = reversed ? Object.keys(childMapping).length - 1 - index : index;\n const delay = staggerIndex * itemDelay;\n\n // Clone element with delay prop (for enter direction) or exitDelay prop (for exit direction)\n const delayProp = direction === 'enter' ? { delay } : { exitDelay: delay };\n\n // Only set visible prop if the component supports it\n // Set visible based on direction: true for enter, false for exit\n const visibleProp = acceptsVisibleProp(element) ? { visible: direction === 'enter' } : {};\n\n return React.cloneElement(element, {\n key,\n ...visibleProp,\n ...delayProp,\n });\n })}\n </>\n );\n }\n\n // For timing mode, use the existing timing-based implementation\n\n return (\n <>\n {Object.entries(childMapping).map(([key, { element }]) => {\n if (hideMode === 'visibleProp') {\n // Use a generic record type for props to avoid `any` while still allowing unknown prop shapes.\n return React.cloneElement(element, {\n key,\n visible: itemsVisibility[key],\n } as Partial<Record<string, unknown>>);\n } else if (hideMode === 'visibilityStyle') {\n const childProps = element.props as Record<string, unknown> | undefined;\n const style = {\n ...(childProps?.style as Record<string, unknown> | undefined),\n visibility: itemsVisibility[key] ? 'visible' : 'hidden',\n };\n return React.cloneElement(element, { key, style } as Partial<Record<string, unknown>>);\n } else {\n // unmount mode\n return itemsVisibility[key] ? React.cloneElement(element, { key }) : null;\n }\n })}\n </>\n );\n};\n\n// Shared helper for StaggerIn and StaggerOut\nconst createStaggerDirection = (direction: 'enter' | 'exit') => {\n const StaggerDirection: React.FC<Omit<StaggerProps, 'visible'>> = ({ hideMode, delayMode, children, ...props }) => {\n // Auto-detect modes for better performance with motion components\n const { hideMode: resolvedHideMode, delayMode: resolvedDelayMode } = detectStaggerModes(children, {\n hideMode,\n delayMode,\n // One-way stagger falls back to visibilityStyle if it doesn't detect visibleProp support\n fallbackHideMode: 'visibilityStyle',\n });\n\n return (\n <StaggerOneWay\n {...props}\n children={children}\n direction={direction}\n hideMode={resolvedHideMode}\n delayMode={resolvedDelayMode}\n />\n );\n };\n\n return StaggerDirection;\n};\n\nconst StaggerIn = createStaggerDirection('enter');\nconst StaggerOut = createStaggerDirection('exit');\n\n// Main Stagger component with auto-detection or explicit modes\nconst StaggerMain: React.FC<StaggerProps> = props => {\n const { children, visible = false, hideMode, delayMode, ...rest } = props;\n\n // Auto-detect modes for bidirectional stagger\n const { hideMode: resolvedHideMode, delayMode: resolvedDelayMode } = detectStaggerModes(children, {\n hideMode,\n delayMode,\n // Bidirectional stagger falls back to visibilityStyle if it doesn't detect visibleProp support\n fallbackHideMode: 'visibilityStyle',\n });\n\n const direction = visible ? 'enter' : 'exit';\n\n return (\n <StaggerOneWay\n {...rest}\n children={children}\n hideMode={resolvedHideMode}\n delayMode={resolvedDelayMode}\n direction={direction}\n />\n );\n};\n\n/**\n * Stagger is a component that manages the staggered entrance and exit of its children.\n * Children are animated in sequence with configurable timing between each item.\n * Stagger can be interactively toggled between entrance and exit animations using the `visible` prop.\n *\n * @param children - React elements to animate. Elements are cloned with animation props.\n * @param visible - Controls animation direction. When `true`, the group is animating \"enter\" (items shown);\n * when `false`, the group is animating \"exit\" (items hidden). Defaults to `false`.\n * @param itemDelay - Milliseconds between each item's animation start.\n * Defaults to the package's default delay (see `DEFAULT_ITEM_DELAY`).\n * @param itemDuration - Milliseconds each item's animation lasts. Only used with `delayMode=\"timing\"`.\n * Defaults to the package's default item duration (see `DEFAULT_ITEM_DURATION`).\n * @param reversed - Whether to reverse the stagger sequence (last item animates first). Defaults to `false`.\n * @param hideMode - How children's visibility/mounting is managed. Auto-detects if not specified.\n * @param delayMode - How staggering timing is implemented. Auto-detects if not specified.\n * @param onMotionFinish - Callback invoked when the staggered animation sequence completes.\n *\n * **Auto-detection behavior:**\n * - **hideMode**: Presence components use `'visibleProp'`, DOM elements use `'visibilityStyle'`\n * - **delayMode**: Components with delay support use `'delayProp'` (most performant), others use `'timing'`\n *\n * **hideMode options:**\n * - `'visibleProp'`: Children are presence components with `visible` prop (always rendered, visibility controlled via prop)\n * - `'visibilityStyle'`: Children remain in DOM with inline style visibility: hidden/visible (preserves layout space)\n * - `'unmount'`: Children are mounted/unmounted from DOM based on visibility\n *\n * **delayMode options:**\n * - `'timing'`: Manages visibility over time using JavaScript timing\n * - `'delayProp'`: Passes delay props to motion components to use native Web Animations API delays (most performant)\n *\n * **Static variants:**\n * - `<Stagger.In>` - One-way stagger for entrance animations only (auto-detects optimal modes)\n * - `<Stagger.Out>` - One-way stagger for exit animations only (auto-detects optimal modes)\n *\n * @example\n * ```tsx\n * import { Stagger, Fade, Scale, Rotate } from '@fluentui/react-motion-components-preview';\n *\n * // Auto-detects optimal modes for presence components (delayProp + visibleProp)\n * <Stagger visible={isVisible} itemDelay={150}>\n * <Fade><div>Item 2</div></Fade>\n * <Scale><div>Item 1</div></Scale>\n * <Rotate><div>Item 3</div></Rotate>\n * </Stagger>\n *\n * // Auto-detects optimal modes for motion components (delayProp + unmount)\n * <Stagger.In itemDelay={100}>\n * <Scale.In><div>Item 1</div></Scale.In>\n * <Fade.In><div>Item 2</div></Fade.In>\n * </Stagger.In>\n *\n * // Auto-detects timing mode for DOM elements (timing + visibilityStyle)\n * <Stagger visible={isVisible} itemDelay={150} onMotionFinish={handleComplete}>\n * <div>Item 1</div>\n * <div>Item 2</div>\n * <div>Item 3</div>\n * </Stagger>\n *\n * // Override auto-detection when needed\n * <Stagger visible={isVisible} delayMode=\"timing\" hideMode=\"unmount\">\n * <CustomComponent>Item 1</CustomComponent>\n * </Stagger>\n * ```\n */\nexport const Stagger = Object.assign(StaggerMain, {\n In: StaggerIn,\n Out: StaggerOut,\n});\n"],"names":["React","useStaggerItemsVisibility","DEFAULT_ITEM_DURATION","DEFAULT_ITEM_DELAY","acceptsVisibleProp","acceptsDelayProps","getStaggerChildMapping","detectStaggerModes","children","options","hideMode","delayMode","fallbackHideMode","childMapping","elements","Object","values","map","item","element","hasVisiblePropSupport","every","child","hasDelayPropSupport","StaggerOneWay","direction","itemDelay","itemDuration","reversed","onMotionFinish","useMemo","itemsVisibility","entries","key","index","staggerIndex","keys","length","delay","delayProp","exitDelay","visibleProp","visible","cloneElement","childProps","props","style","visibility","createStaggerDirection","StaggerDirection","resolvedHideMode","resolvedDelayMode","StaggerIn","StaggerOut","StaggerMain","rest","Stagger","assign","In","Out"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Stagger } from './Stagger';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/choreography/Stagger/index.ts"],"sourcesContent":["export { Stagger } from './Stagger';\nexport type { StaggerProps } from './stagger-types';\n"],"names":["Stagger"],"mappings":"AAAA,SAASA,OAAO,QAAQ,YAAY"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/choreography/Stagger/stagger-types.ts"],"sourcesContent":["import { PresenceComponentProps, PresenceDirection } from '@fluentui/react-motion';\nimport * as React from 'react';\n\n/**\n * Defines how Stagger manages its children's visibility/mounting.\n * - 'visibleProp': Children are components with a `visible` prop (e.g. motion components)\n * - 'visibilityStyle': Children remain in DOM with inline style `visibility: hidden | visible`\n * - 'unmount': Children are mounted/unmounted from DOM based on visibility\n */\nexport type StaggerHideMode = 'visibleProp' | 'visibilityStyle' | 'unmount';\n\n/**\n * Defines how Stagger implements the timing of staggered animations.\n * - 'timing': Manages visibility over time using JavaScript timing (current behavior)\n * - 'delayProp': Passes delay props to motion components to use native Web Animations API delays\n */\nexport type StaggerDelayMode = 'timing' | 'delayProp';\n\n/**\n * Props for the Stagger component that manages staggered entrance and exit animations.\n */\nexport interface StaggerProps {\n /** React elements to animate. Elements are cloned with animation props. */\n children: React.ReactNode;\n\n /**\n * Controls children animation direction. When `true`, the group is animating \"enter\" (items shown).\n * When `false`, the group is animating \"exit\" (items hidden).\n */\n visible?: PresenceComponentProps['visible'];\n\n /** Whether to reverse the stagger sequence (last item animates first). Defaults to `false`. */\n reversed?: boolean;\n\n /**\n * Milliseconds between each child's animation start.\n * Defaults to the package's default delay (see `DEFAULT_ITEM_DELAY`).\n */\n itemDelay?: number;\n\n /**\n * Milliseconds each child's animation lasts. Only used with `delayMode=\"timing\"`.\n * Defaults to the package's default duration (see `DEFAULT_ITEM_DURATION`).\n */\n itemDuration?: number;\n\n /** How children's visibility/mounting is managed. Auto-detects if not specified. */\n hideMode?: StaggerHideMode;\n\n /** How staggering timing is implemented. Defaults to 'timing'. */\n delayMode?: StaggerDelayMode;\n\n /** Callback invoked when the staggered animation sequence completes. */\n onMotionFinish?: () => void;\n}\n\nexport interface StaggerOneWayProps extends Omit<StaggerProps, 'visible' | 'hideMode' | 'delayMode'> {\n /** Animation direction: 'enter' or 'exit'. */\n direction: PresenceDirection;\n\n /** How children's visibility/mounting is managed. Required - provided by wrapper components. */\n hideMode: StaggerHideMode;\n\n /** How staggering timing is implemented. Required - provided by wrapper components. */\n delayMode: StaggerDelayMode;\n}\n"],"names":["React"],"mappings":"AACA,YAAYA,WAAW,QAAQ"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useAnimationFrame, useEventCallback } from '@fluentui/react-utilities';
|
|
4
|
+
import { staggerItemsVisibilityAtTime, DEFAULT_ITEM_DURATION } from './utils';
|
|
5
|
+
/**
|
|
6
|
+
* Hook that tracks the visibility of a staggered sequence of items as time progresses.
|
|
7
|
+
*
|
|
8
|
+
* Behavior summary for all hide modes:
|
|
9
|
+
* - On the first render, items are placed in their final state (enter => visible, exit => hidden)
|
|
10
|
+
* and no animation runs.
|
|
11
|
+
* - On subsequent renders when direction changes, items animate from the opposite state
|
|
12
|
+
* to the final state over the stagger timeline.
|
|
13
|
+
* - Changes to the `reversed` prop do not trigger re-animation; they only affect the order
|
|
14
|
+
* during the next direction change animation.
|
|
15
|
+
*
|
|
16
|
+
* This hook uses child key mapping instead of item count to track individual items.
|
|
17
|
+
* This allows it to correctly handle:
|
|
18
|
+
* - Items being added and removed simultaneously (when count stays the same)
|
|
19
|
+
* - Items being reordered
|
|
20
|
+
* - Individual item identity across renders
|
|
21
|
+
*
|
|
22
|
+
* @param childMapping - Mapping of child keys to elements and indices
|
|
23
|
+
* @param itemDelay - Milliseconds between the start of each item's animation
|
|
24
|
+
* @param itemDuration - Milliseconds each item's animation lasts
|
|
25
|
+
* @param direction - 'enter' (show items) or 'exit' (hide items)
|
|
26
|
+
* @param reversed - Whether to reverse the stagger order (last item first)
|
|
27
|
+
* @param onMotionFinish - Callback fired when the full stagger sequence completes
|
|
28
|
+
* @param hideMode - How children's visibility is managed: 'visibleProp', 'visibilityStyle', or 'unmount'
|
|
29
|
+
*
|
|
30
|
+
* @returns An object with `itemsVisibility: Record<string, boolean>` indicating which items are currently visible by key
|
|
31
|
+
*/ export function useStaggerItemsVisibility({ childMapping, itemDelay, itemDuration = DEFAULT_ITEM_DURATION, direction, reversed = false, onMotionFinish, hideMode = 'visibleProp' }) {
|
|
32
|
+
const [requestAnimationFrame, cancelAnimationFrame] = useAnimationFrame();
|
|
33
|
+
// Stabilize the callback reference to avoid re-triggering effects on every render
|
|
34
|
+
const handleMotionFinish = useEventCallback(onMotionFinish !== null && onMotionFinish !== void 0 ? onMotionFinish : ()=>{
|
|
35
|
+
return;
|
|
36
|
+
});
|
|
37
|
+
// Track animation state independently of child changes
|
|
38
|
+
const [animationKey, setAnimationKey] = React.useState(0);
|
|
39
|
+
const prevDirection = React.useRef(direction);
|
|
40
|
+
// Only trigger new animation when direction actually changes, not when children change
|
|
41
|
+
React.useEffect(()=>{
|
|
42
|
+
if (prevDirection.current !== direction) {
|
|
43
|
+
setAnimationKey((prev)=>prev + 1);
|
|
44
|
+
prevDirection.current = direction;
|
|
45
|
+
}
|
|
46
|
+
}, [
|
|
47
|
+
direction
|
|
48
|
+
]);
|
|
49
|
+
// State: visibility mapping for all items by key
|
|
50
|
+
const [itemsVisibility, setItemsVisibility] = React.useState(()=>{
|
|
51
|
+
const initial = {};
|
|
52
|
+
// All hide modes start in final state: visible for 'enter', hidden for 'exit'
|
|
53
|
+
const initialState = direction === 'enter';
|
|
54
|
+
Object.keys(childMapping).forEach((key)=>{
|
|
55
|
+
initial[key] = initialState;
|
|
56
|
+
});
|
|
57
|
+
return initial;
|
|
58
|
+
});
|
|
59
|
+
// Update visibility mapping when childMapping changes
|
|
60
|
+
React.useEffect(()=>{
|
|
61
|
+
setItemsVisibility((prev)=>{
|
|
62
|
+
const next = {};
|
|
63
|
+
const targetState = direction === 'enter';
|
|
64
|
+
// Add or update items from new mapping
|
|
65
|
+
Object.keys(childMapping).forEach((key)=>{
|
|
66
|
+
if (key in prev) {
|
|
67
|
+
// Existing item - preserve its visibility state
|
|
68
|
+
next[key] = prev[key];
|
|
69
|
+
} else {
|
|
70
|
+
// New item - set to target state
|
|
71
|
+
next[key] = targetState;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Note: Items that were in prev but not in childMapping are automatically removed
|
|
75
|
+
// because we only iterate over keys in childMapping
|
|
76
|
+
return next;
|
|
77
|
+
});
|
|
78
|
+
}, [
|
|
79
|
+
childMapping,
|
|
80
|
+
direction
|
|
81
|
+
]);
|
|
82
|
+
// Refs: animation timing and control
|
|
83
|
+
const startTimeRef = React.useRef(null);
|
|
84
|
+
const frameRef = React.useRef(null);
|
|
85
|
+
const finishedRef = React.useRef(false);
|
|
86
|
+
const isFirstRender = React.useRef(true);
|
|
87
|
+
// Use ref to avoid re-running the animation when child mapping changes
|
|
88
|
+
const childMappingRef = React.useRef(childMapping);
|
|
89
|
+
// Update childMapping ref whenever it changes
|
|
90
|
+
React.useEffect(()=>{
|
|
91
|
+
childMappingRef.current = childMapping;
|
|
92
|
+
}, [
|
|
93
|
+
childMapping
|
|
94
|
+
]);
|
|
95
|
+
// Use ref for reversed to avoid re-running animation when it changes
|
|
96
|
+
const reversedRef = React.useRef(reversed);
|
|
97
|
+
// Update reversed ref whenever it changes
|
|
98
|
+
React.useEffect(()=>{
|
|
99
|
+
reversedRef.current = reversed;
|
|
100
|
+
}, [
|
|
101
|
+
reversed
|
|
102
|
+
]);
|
|
103
|
+
// ====== ANIMATION EFFECT ======
|
|
104
|
+
React.useEffect(()=>{
|
|
105
|
+
let cancelled = false;
|
|
106
|
+
startTimeRef.current = null;
|
|
107
|
+
finishedRef.current = false;
|
|
108
|
+
// All hide modes skip animation on first render - items are already in their final state
|
|
109
|
+
if (isFirstRender.current) {
|
|
110
|
+
isFirstRender.current = false;
|
|
111
|
+
// Items are already in their final state from useState, no animation needed
|
|
112
|
+
handleMotionFinish();
|
|
113
|
+
return; // No cleanup needed for first render
|
|
114
|
+
}
|
|
115
|
+
// For animations after first render, start from the opposite of the final state
|
|
116
|
+
// - Enter animation: start hidden (false), animate to visible (true)
|
|
117
|
+
// - Exit animation: start visible (true), animate to hidden (false)
|
|
118
|
+
const startState = direction === 'exit';
|
|
119
|
+
// Use childMappingRef.current to avoid adding childMapping to dependencies
|
|
120
|
+
const initialVisibility = {};
|
|
121
|
+
Object.keys(childMappingRef.current).forEach((key)=>{
|
|
122
|
+
initialVisibility[key] = startState;
|
|
123
|
+
});
|
|
124
|
+
setItemsVisibility(initialVisibility);
|
|
125
|
+
// Animation loop: update visibility on each frame until complete
|
|
126
|
+
const tick = (now)=>{
|
|
127
|
+
if (cancelled) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (startTimeRef.current === null) {
|
|
131
|
+
startTimeRef.current = now;
|
|
132
|
+
}
|
|
133
|
+
const elapsed = now - startTimeRef.current;
|
|
134
|
+
const childKeys = Object.keys(childMappingRef.current);
|
|
135
|
+
const itemCount = childKeys.length;
|
|
136
|
+
const result = staggerItemsVisibilityAtTime({
|
|
137
|
+
itemCount,
|
|
138
|
+
elapsed,
|
|
139
|
+
itemDelay,
|
|
140
|
+
itemDuration,
|
|
141
|
+
direction,
|
|
142
|
+
reversed: reversedRef.current
|
|
143
|
+
});
|
|
144
|
+
// Convert boolean array to keyed object
|
|
145
|
+
const nextVisibility = {};
|
|
146
|
+
childKeys.forEach((key, idx)=>{
|
|
147
|
+
nextVisibility[key] = result.itemsVisibility[idx];
|
|
148
|
+
});
|
|
149
|
+
setItemsVisibility(nextVisibility);
|
|
150
|
+
if (elapsed < result.totalDuration) {
|
|
151
|
+
frameRef.current = requestAnimationFrame(tick);
|
|
152
|
+
} else if (!finishedRef.current) {
|
|
153
|
+
finishedRef.current = true;
|
|
154
|
+
handleMotionFinish();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
frameRef.current = requestAnimationFrame(tick);
|
|
158
|
+
return ()=>{
|
|
159
|
+
cancelled = true;
|
|
160
|
+
if (frameRef.current) {
|
|
161
|
+
cancelAnimationFrame();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}, [
|
|
165
|
+
animationKey,
|
|
166
|
+
itemDelay,
|
|
167
|
+
itemDuration,
|
|
168
|
+
direction,
|
|
169
|
+
requestAnimationFrame,
|
|
170
|
+
cancelAnimationFrame,
|
|
171
|
+
handleMotionFinish
|
|
172
|
+
]);
|
|
173
|
+
return {
|
|
174
|
+
itemsVisibility
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/choreography/Stagger/useStaggerItemsVisibility.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useAnimationFrame, useEventCallback } from '@fluentui/react-utilities';\nimport type { StaggerProps } from './stagger-types';\nimport {\n staggerItemsVisibilityAtTime,\n type StaggerItemsVisibilityAtTimeParams,\n DEFAULT_ITEM_DURATION,\n type StaggerChildMapping,\n} from './utils';\n\nexport interface UseStaggerItemsVisibilityParams\n extends Pick<StaggerProps, 'onMotionFinish'>,\n Omit<StaggerItemsVisibilityAtTimeParams, 'elapsed' | 'itemCount'> {\n hideMode: StaggerProps['hideMode'];\n childMapping: StaggerChildMapping;\n}\n\n/**\n * Hook that tracks the visibility of a staggered sequence of items as time progresses.\n *\n * Behavior summary for all hide modes:\n * - On the first render, items are placed in their final state (enter => visible, exit => hidden)\n * and no animation runs.\n * - On subsequent renders when direction changes, items animate from the opposite state\n * to the final state over the stagger timeline.\n * - Changes to the `reversed` prop do not trigger re-animation; they only affect the order\n * during the next direction change animation.\n *\n * This hook uses child key mapping instead of item count to track individual items.\n * This allows it to correctly handle:\n * - Items being added and removed simultaneously (when count stays the same)\n * - Items being reordered\n * - Individual item identity across renders\n *\n * @param childMapping - Mapping of child keys to elements and indices\n * @param itemDelay - Milliseconds between the start of each item's animation\n * @param itemDuration - Milliseconds each item's animation lasts\n * @param direction - 'enter' (show items) or 'exit' (hide items)\n * @param reversed - Whether to reverse the stagger order (last item first)\n * @param onMotionFinish - Callback fired when the full stagger sequence completes\n * @param hideMode - How children's visibility is managed: 'visibleProp', 'visibilityStyle', or 'unmount'\n *\n * @returns An object with `itemsVisibility: Record<string, boolean>` indicating which items are currently visible by key\n */\nexport function useStaggerItemsVisibility({\n childMapping,\n itemDelay,\n itemDuration = DEFAULT_ITEM_DURATION,\n direction,\n reversed = false,\n onMotionFinish,\n hideMode = 'visibleProp',\n}: UseStaggerItemsVisibilityParams): { itemsVisibility: Record<string, boolean> } {\n const [requestAnimationFrame, cancelAnimationFrame] = useAnimationFrame();\n\n // Stabilize the callback reference to avoid re-triggering effects on every render\n const handleMotionFinish = useEventCallback(\n onMotionFinish ??\n (() => {\n return;\n }),\n );\n\n // Track animation state independently of child changes\n const [animationKey, setAnimationKey] = React.useState(0);\n const prevDirection = React.useRef(direction);\n\n // Only trigger new animation when direction actually changes, not when children change\n React.useEffect(() => {\n if (prevDirection.current !== direction) {\n setAnimationKey(prev => prev + 1);\n prevDirection.current = direction;\n }\n }, [direction]);\n\n // State: visibility mapping for all items by key\n const [itemsVisibility, setItemsVisibility] = React.useState<Record<string, boolean>>(() => {\n const initial: Record<string, boolean> = {};\n // All hide modes start in final state: visible for 'enter', hidden for 'exit'\n const initialState = direction === 'enter';\n Object.keys(childMapping).forEach(key => {\n initial[key] = initialState;\n });\n return initial;\n });\n\n // Update visibility mapping when childMapping changes\n React.useEffect(() => {\n setItemsVisibility(prev => {\n const next: Record<string, boolean> = {};\n const targetState = direction === 'enter';\n\n // Add or update items from new mapping\n Object.keys(childMapping).forEach(key => {\n if (key in prev) {\n // Existing item - preserve its visibility state\n next[key] = prev[key];\n } else {\n // New item - set to target state\n next[key] = targetState;\n }\n });\n\n // Note: Items that were in prev but not in childMapping are automatically removed\n // because we only iterate over keys in childMapping\n\n return next;\n });\n }, [childMapping, direction]);\n\n // Refs: animation timing and control\n const startTimeRef = React.useRef<number | null>(null);\n const frameRef = React.useRef<number | null>(null);\n const finishedRef = React.useRef(false);\n const isFirstRender = React.useRef(true);\n\n // Use ref to avoid re-running the animation when child mapping changes\n const childMappingRef = React.useRef(childMapping);\n\n // Update childMapping ref whenever it changes\n React.useEffect(() => {\n childMappingRef.current = childMapping;\n }, [childMapping]);\n\n // Use ref for reversed to avoid re-running animation when it changes\n const reversedRef = React.useRef(reversed);\n\n // Update reversed ref whenever it changes\n React.useEffect(() => {\n reversedRef.current = reversed;\n }, [reversed]);\n\n // ====== ANIMATION EFFECT ======\n\n React.useEffect(() => {\n let cancelled = false;\n startTimeRef.current = null;\n finishedRef.current = false;\n\n // All hide modes skip animation on first render - items are already in their final state\n if (isFirstRender.current) {\n isFirstRender.current = false;\n // Items are already in their final state from useState, no animation needed\n handleMotionFinish();\n return; // No cleanup needed for first render\n }\n\n // For animations after first render, start from the opposite of the final state\n // - Enter animation: start hidden (false), animate to visible (true)\n // - Exit animation: start visible (true), animate to hidden (false)\n const startState = direction === 'exit';\n // Use childMappingRef.current to avoid adding childMapping to dependencies\n const initialVisibility: Record<string, boolean> = {};\n Object.keys(childMappingRef.current).forEach(key => {\n initialVisibility[key] = startState;\n });\n setItemsVisibility(initialVisibility);\n\n // Animation loop: update visibility on each frame until complete\n const tick = (now: number) => {\n if (cancelled) {\n return;\n }\n if (startTimeRef.current === null) {\n startTimeRef.current = now;\n }\n const elapsed = now - (startTimeRef.current as number);\n\n const childKeys = Object.keys(childMappingRef.current);\n const itemCount = childKeys.length;\n\n const result = staggerItemsVisibilityAtTime({\n itemCount,\n elapsed,\n itemDelay,\n itemDuration,\n direction,\n reversed: reversedRef.current,\n });\n\n // Convert boolean array to keyed object\n const nextVisibility: Record<string, boolean> = {};\n childKeys.forEach((key, idx) => {\n nextVisibility[key] = result.itemsVisibility[idx];\n });\n\n setItemsVisibility(nextVisibility);\n\n if (elapsed < result.totalDuration) {\n frameRef.current = requestAnimationFrame(tick);\n } else if (!finishedRef.current) {\n finishedRef.current = true;\n handleMotionFinish();\n }\n };\n\n frameRef.current = requestAnimationFrame(tick);\n return () => {\n cancelled = true;\n if (frameRef.current) {\n cancelAnimationFrame();\n }\n };\n }, [\n animationKey,\n itemDelay,\n itemDuration,\n direction,\n requestAnimationFrame,\n cancelAnimationFrame,\n handleMotionFinish,\n ]);\n\n return { itemsVisibility };\n}\n"],"names":["React","useAnimationFrame","useEventCallback","staggerItemsVisibilityAtTime","DEFAULT_ITEM_DURATION","useStaggerItemsVisibility","childMapping","itemDelay","itemDuration","direction","reversed","onMotionFinish","hideMode","requestAnimationFrame","cancelAnimationFrame","handleMotionFinish","animationKey","setAnimationKey","useState","prevDirection","useRef","useEffect","current","prev","itemsVisibility","setItemsVisibility","initial","initialState","Object","keys","forEach","key","next","targetState","startTimeRef","frameRef","finishedRef","isFirstRender","childMappingRef","reversedRef","cancelled","startState","initialVisibility","tick","now","elapsed","childKeys","itemCount","length","result","nextVisibility","idx","totalDuration"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default timing constants for stagger animations (milliseconds).
|
|
3
|
+
*/ import { motionTokens } from '@fluentui/react-motion';
|
|
4
|
+
/** Default delay in milliseconds between each item's animation start */ export const DEFAULT_ITEM_DELAY = motionTokens.durationFaster;
|
|
5
|
+
/** Default duration in milliseconds for each item's animation */ export const DEFAULT_ITEM_DURATION = motionTokens.durationNormal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/choreography/Stagger/utils/constants.ts"],"sourcesContent":["/**\n * Default timing constants for stagger animations (milliseconds).\n */\n\nimport { motionTokens } from '@fluentui/react-motion';\n\n/** Default delay in milliseconds between each item's animation start */\nexport const DEFAULT_ITEM_DELAY = motionTokens.durationFaster;\n\n/** Default duration in milliseconds for each item's animation */\nexport const DEFAULT_ITEM_DURATION = motionTokens.durationNormal;\n"],"names":["motionTokens","DEFAULT_ITEM_DELAY","durationFaster","DEFAULT_ITEM_DURATION","durationNormal"],"mappings":"AAAA;;CAEC,GAED,SAASA,YAAY,QAAQ,yBAAyB;AAEtD,sEAAsE,GACtE,OAAO,MAAMC,qBAAqBD,aAAaE,cAAc,CAAC;AAE9D,+DAA+D,GAC/D,OAAO,MAAMC,wBAAwBH,aAAaI,cAAc,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Given `children`, return an object mapping key to child element and its index.
|
|
4
|
+
* This allows tracking individual items by identity (via React keys) rather than by position.
|
|
5
|
+
*
|
|
6
|
+
* Uses React.Children.toArray() which:
|
|
7
|
+
* - Automatically provides stable indices (0, 1, 2, ...)
|
|
8
|
+
* - Handles key normalization (e.g., 'a' → '.$a') consistently
|
|
9
|
+
* - Flattens fragments automatically
|
|
10
|
+
* - Generates keys for elements without explicit keys (e.g., '.0', '.1', '.2')
|
|
11
|
+
*
|
|
12
|
+
* @param children - React children to map
|
|
13
|
+
* @returns Object mapping child keys to { element, index }
|
|
14
|
+
*/ // TODO: consider unifying with getChildMapping from react-motion package by making it generic
|
|
15
|
+
export function getStaggerChildMapping(children) {
|
|
16
|
+
const childMapping = {};
|
|
17
|
+
if (children) {
|
|
18
|
+
React.Children.toArray(children).forEach((child, index)=>{
|
|
19
|
+
if (React.isValidElement(child)) {
|
|
20
|
+
var _child_key;
|
|
21
|
+
childMapping[(_child_key = child.key) !== null && _child_key !== void 0 ? _child_key : ''] = {
|
|
22
|
+
element: child,
|
|
23
|
+
index
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return childMapping;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/choreography/Stagger/utils/getStaggerChildMapping.ts"],"sourcesContent":["import * as React from 'react';\n\nexport type StaggerChildMapping = Record<string, { element: React.ReactElement; index: number }>;\n\n/**\n * Given `children`, return an object mapping key to child element and its index.\n * This allows tracking individual items by identity (via React keys) rather than by position.\n *\n * Uses React.Children.toArray() which:\n * - Automatically provides stable indices (0, 1, 2, ...)\n * - Handles key normalization (e.g., 'a' → '.$a') consistently\n * - Flattens fragments automatically\n * - Generates keys for elements without explicit keys (e.g., '.0', '.1', '.2')\n *\n * @param children - React children to map\n * @returns Object mapping child keys to { element, index }\n */\n// TODO: consider unifying with getChildMapping from react-motion package by making it generic\nexport function getStaggerChildMapping(children: React.ReactNode | undefined): StaggerChildMapping {\n const childMapping: StaggerChildMapping = {};\n\n if (children) {\n React.Children.toArray(children).forEach((child, index) => {\n if (React.isValidElement(child)) {\n childMapping[child.key ?? ''] = {\n element: child,\n index,\n };\n }\n });\n }\n\n return childMapping;\n}\n"],"names":["React","getStaggerChildMapping","children","childMapping","Children","toArray","forEach","child","index","isValidElement","key","element"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAI/B;;;;;;;;;;;;CAYC,GACD,8FAA8F;AAC9F,OAAO,SAASC,uBAAuBC,QAAqC;IAC1E,MAAMC,eAAoC,CAAC;IAE3C,IAAID,UAAU;QACZF,MAAMI,QAAQ,CAACC,OAAO,CAACH,UAAUI,OAAO,CAAC,CAACC,OAAOC;YAC/C,IAAIR,MAAMS,cAAc,CAACF,QAAQ;oBAClBA;gBAAbJ,YAAY,CAACI,CAAAA,aAAAA,MAAMG,GAAG,cAATH,wBAAAA,aAAa,GAAG,GAAG;oBAC9BI,SAASJ;oBACTC;gBACF;YACF;QACF;IACF;IAEA,OAAOL;AACT"}
|