@fluentui/react-motion 9.15.0 → 9.16.0

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 CHANGED
@@ -1,19 +1,29 @@
1
1
  # Change Log - @fluentui/react-motion
2
2
 
3
- This log was last generated on Thu, 23 Apr 2026 11:59:27 GMT and should not be manually modified.
3
+ This log was last generated on Tue, 26 May 2026 09:33:47 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## [9.16.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-motion_v9.16.0)
8
+
9
+ Tue, 26 May 2026 09:33:47 GMT
10
+ [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-motion_v9.15.0..@fluentui/react-motion_v9.16.0)
11
+
12
+ ### Minor changes
13
+
14
+ - feat(react-motion): add replayKey prop to replay motion without remounting ([PR #36108](https://github.com/microsoft/fluentui/pull/36108) by robertpenner@microsoft.com)
15
+ - Bump @fluentui/react-utilities to v9.26.4 ([PR #36246](https://github.com/microsoft/fluentui/pull/36246) by beachball)
16
+
7
17
  ## [9.15.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-motion_v9.15.0)
8
18
 
9
- Thu, 23 Apr 2026 11:59:27 GMT
19
+ Thu, 23 Apr 2026 14:21:02 GMT
10
20
  [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-motion_v9.14.0..@fluentui/react-motion_v9.15.0)
11
21
 
12
22
  ### Minor changes
13
23
 
14
- - fix(react-motion): apply MotionComponent type to presence definition ([PR #35952](https://github.com/microsoft/fluentui/pull/35952) by robertpenner@microsoft.com)
15
24
  - feat: expose motion params as direct props on motion slot types ([PR #36011](https://github.com/microsoft/fluentui/pull/36011) by robertpenner@microsoft.com)
16
- - Bump @fluentui/react-utilities to v9.26.3 ([PR #35996](https://github.com/microsoft/fluentui/pull/35996) by beachball)
25
+ - fix(react-motion): apply MotionComponent type to presence definition ([PR #35952](https://github.com/microsoft/fluentui/pull/35952) by robertpenner@microsoft.com)
26
+ - Bump @fluentui/react-utilities to v9.26.3 ([PR #36035](https://github.com/microsoft/fluentui/pull/36035) by beachball)
17
27
 
18
28
  ## [9.14.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-motion_v9.14.0)
19
29
 
package/README.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # @fluentui/react-motion
2
2
 
3
- **React Motions components for [Fluent UI React](https://react.fluentui.dev/)**
3
+ **React Motion components for [Fluent UI React](https://react.fluentui.dev/)**
4
4
 
5
- These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release.
5
+ A lightweight, performant animation library for React that brings Fluent UI experiences to life using the Web Animations API (WAAPI).
6
+
7
+ ## Features
8
+
9
+ - ⚡ **Performance** — Animations run on the compositor thread for smooth 60fps motion
10
+ - 📦 **Lightweight** — ~3KB gzipped, leverages native browser capabilities
11
+ - 🎯 **Simple by default** — Common UI animations with minimal code
12
+ - 🔧 **Powerful on demand** — Full customization with keyframes, timing, and callbacks
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @fluentui/react-motion
18
+ # or
19
+ yarn add @fluentui/react-motion
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```tsx
25
+ import { createPresenceComponent, motionTokens } from '@fluentui/react-motion';
26
+
27
+ // Create a custom fade presence component
28
+ const Fade = createPresenceComponent({
29
+ enter: {
30
+ keyframes: [{ opacity: 0 }, { opacity: 1 }],
31
+ duration: motionTokens.durationNormal,
32
+ },
33
+ exit: {
34
+ keyframes: [{ opacity: 1 }, { opacity: 0 }],
35
+ duration: motionTokens.durationFast,
36
+ },
37
+ });
38
+
39
+ // Use it in your app
40
+ function App() {
41
+ const [visible, setVisible] = useState(true);
42
+
43
+ return (
44
+ <Fade visible={visible}>
45
+ <div>Animated content</div>
46
+ </Fade>
47
+ );
48
+ }
49
+ ```
50
+
51
+ ## Documentation
52
+
53
+ 📚 **[Full documentation](https://react.fluentui.dev/?path=/docs/motion-introduction--docs)**
54
+
55
+ - [Introduction](https://react.fluentui.dev/?path=/docs/motion-introduction--docs) — Overview and key concepts
56
+ - [createPresenceComponent](https://react.fluentui.dev/?path=/docs/motion-apis-createpresencecomponent--docs) — Two-way enter/exit animations
57
+ - [createMotionComponent](https://react.fluentui.dev/?path=/docs/motion-apis-createmotioncomponent--docs) — One-way animations
58
+ - [Motion Tokens](https://react.fluentui.dev/?path=/docs/motion-tokens--docs) — Duration and easing values
59
+ - [Migration Guide](https://react.fluentui.dev/?path=/docs/motion-migration--docs) — Coming from Framer Motion, GSAP, etc.
60
+
61
+ ## Pre-built Components
62
+
63
+ For ready-to-use motion components (Fade, Scale, Slide, Collapse, etc.), see **[@fluentui/react-motion-components-preview](https://www.npmjs.com/package/@fluentui/react-motion-components-preview)**.
package/dist/index.d.ts CHANGED
@@ -123,6 +123,30 @@ export declare type MotionComponentProps = {
123
123
  * so the callback is triggered with "null".
124
124
  */
125
125
  onMotionStart?: (ev: null) => void;
126
+ /**
127
+ * When this value changes, the animation replays from the start on the same DOM element,
128
+ * cancelling any in-progress animation, without remounting the component or its children.
129
+ *
130
+ * **Why not just use a React `key`?** Changing a React `key` forces a full unmount and
131
+ * remount of the subtree: DOM nodes are destroyed and recreated, focus is lost, and any
132
+ * child state is reset. `replayKey` avoids all of that — only the animation effect reruns
133
+ * while the DOM and component state remain intact.
134
+ *
135
+ * Use this when you want to retrigger a motion in response to a state change (e.g. a user
136
+ * action or a data update) while preserving DOM continuity. It is the declarative equivalent
137
+ * of calling `imperativeRef.current.play()` but driven by a prop rather than a ref call.
138
+ *
139
+ * @example
140
+ * ```tsx
141
+ * // Replay a Fade.In each time the user clicks "Refresh"
142
+ * const [replayKey, setReplayKey] = React.useState(0);
143
+ * <Fade.In replayKey={replayKey}>
144
+ * <div>Content</div>
145
+ * </Fade.In>
146
+ * <button onClick={() => setReplayKey(k => k + 1)}>Refresh</button>
147
+ * ```
148
+ */
149
+ replayKey?: string | number;
126
150
  };
127
151
 
128
152
  export declare type MotionImperativeRef = {
@@ -18,10 +18,11 @@ import { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';
18
18
  */ export function createMotionComponent(value) {
19
19
  const Atom = (props)=>{
20
20
  'use no memo';
21
- const { children, imperativeRef, onMotionFinish: onMotionFinishProp, onMotionStart: onMotionStartProp, onMotionCancel: onMotionCancelProp, ..._rest } = props;
21
+ const { children, imperativeRef, onMotionFinish: onMotionFinishProp, onMotionStart: onMotionStartProp, onMotionCancel: onMotionCancelProp, replayKey, ..._rest } = props;
22
22
  const params = _rest;
23
23
  const [child, childRef] = useChildElement(children);
24
24
  const handleRef = useMotionImperativeRef(imperativeRef);
25
+ const isInitialRender = React.useRef(true);
25
26
  const skipMotions = useMotionBehaviourContext() === 'skip';
26
27
  const optionsRef = React.useRef({
27
28
  skipMotions,
@@ -38,6 +39,21 @@ import { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';
38
39
  const onMotionCancel = useEventCallback(()=>{
39
40
  onMotionCancelProp === null || onMotionCancelProp === void 0 ? void 0 : onMotionCancelProp(null);
40
41
  });
42
+ // Stable callback (all deps are refs or useEventCallback) that activates a handle for a new playback cycle.
43
+ //
44
+ // TODO: consider moving the cancel+play+rewire sequence into a handle.replay() method on AnimationHandle,
45
+ // keeping pure animation sequencing on the handle and React callbacks here in the component.
46
+ const activateAnimationHandle = React.useCallback((handle)=>{
47
+ onMotionStart();
48
+ handle.setMotionEndCallbacks(onMotionFinish, onMotionCancel);
49
+ if (optionsRef.current.skipMotions) {
50
+ handle.finish();
51
+ }
52
+ }, [
53
+ onMotionStart,
54
+ onMotionFinish,
55
+ onMotionCancel
56
+ ]);
41
57
  useIsomorphicLayoutEffect(()=>{
42
58
  // Heads up!
43
59
  // We store the params in a ref to avoid re-rendering the component when the params change.
@@ -53,15 +69,11 @@ import { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';
53
69
  element,
54
70
  ...optionsRef.current.params
55
71
  }) : value;
56
- onMotionStart();
57
72
  const handle = animateAtoms(element, atoms, {
58
73
  isReducedMotion: isReducedMotion()
59
74
  });
60
75
  handleRef.current = handle;
61
- handle.setMotionEndCallbacks(onMotionFinish, onMotionCancel);
62
- if (optionsRef.current.skipMotions) {
63
- handle.finish();
64
- }
76
+ activateAnimationHandle(handle);
65
77
  return ()=>{
66
78
  handle.cancel();
67
79
  };
@@ -71,10 +83,30 @@ import { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';
71
83
  childRef,
72
84
  handleRef,
73
85
  isReducedMotion,
74
- onMotionFinish,
75
- onMotionStart,
76
- onMotionCancel
86
+ activateAnimationHandle
87
+ ]);
88
+ // Skips initial mount; on replayKey changes, reuses existing Animation objects via cancel+play
89
+ // rather than recreating them, preserving DOM continuity.
90
+ useIsomorphicLayoutEffect(()=>{
91
+ if (isInitialRender.current) {
92
+ return;
93
+ }
94
+ const handle = handleRef.current;
95
+ if (handle) {
96
+ handle.cancel();
97
+ handle.play();
98
+ activateAnimationHandle(handle);
99
+ }
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- replayKey is intentionally the only trigger; other deps are stable refs/callbacks
101
+ }, [
102
+ replayKey
77
103
  ]);
104
+ useIsomorphicLayoutEffect(()=>{
105
+ isInitialRender.current = false;
106
+ return ()=>{
107
+ isInitialRender.current = true;
108
+ };
109
+ }, []);
78
110
  return child;
79
111
  };
80
112
  return Object.assign(Atom, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/factories/createMotionComponent.ts"],"sourcesContent":["'use client';\n\nimport type { JSXElement } from '@fluentui/react-utilities';\nimport { useEventCallback, useIsomorphicLayoutEffect } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport { useAnimateAtoms } from '../hooks/useAnimateAtoms';\nimport { useMotionImperativeRef } from '../hooks/useMotionImperativeRef';\nimport { useIsReducedMotion } from '../hooks/useIsReducedMotion';\nimport { useChildElement } from '../utils/useChildElement';\nimport type { AtomMotion, AtomMotionFn, MotionParam, MotionImperativeRef } from '../types';\nimport { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';\n\n/**\n * A private symbol to store the motion definition on the component for variants.\n *\n * @internal\n */\nexport const MOTION_DEFINITION = Symbol('MOTION_DEFINITION');\n\nexport type MotionComponentProps = {\n children: JSXElement;\n\n /** Provides imperative controls for the animation. */\n imperativeRef?: React.Ref<MotionImperativeRef | undefined>;\n\n /**\n * Callback that is called when the whole motion finishes.\n *\n * A motion definition can contain multiple animations and therefore multiple \"finish\" events. The callback is\n * triggered once all animations have finished with \"null\" instead of an event object to avoid ambiguity.\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionFinish?: (ev: null) => void;\n\n /**\n * Callback that is called when the whole motion is cancelled.\n *\n * A motion definition can contain multiple animations and therefore multiple \"cancel\" events. The callback is\n * triggered once all animations have been cancelled with \"null\" instead of an event object to avoid ambiguity.\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionCancel?: (ev: null) => void;\n\n /**\n * Callback that is called when the whole motion starts.\n *\n * A motion definition can contain multiple animations and therefore multiple \"start\" events. The callback is\n * triggered when the first animation is started. There is no official \"start\" event with the Web Animations API.\n * so the callback is triggered with \"null\".\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionStart?: (ev: null) => void;\n};\n\nexport type MotionComponent<MotionParams extends Record<string, MotionParam> = {}> = React.FC<\n MotionComponentProps & MotionParams\n> & {\n [MOTION_DEFINITION]: AtomMotionFn<MotionParams>;\n};\n\n/**\n * Creates a component that will animate the children using the provided motion.\n *\n * @param value - A motion definition.\n */\nexport function createMotionComponent<MotionParams extends Record<string, MotionParam> = {}>(\n value: AtomMotion | AtomMotion[] | AtomMotionFn<MotionParams>,\n): MotionComponent<MotionParams> {\n const Atom: React.FC<MotionComponentProps & MotionParams> = props => {\n 'use no memo';\n\n const {\n children,\n imperativeRef,\n onMotionFinish: onMotionFinishProp,\n onMotionStart: onMotionStartProp,\n onMotionCancel: onMotionCancelProp,\n ..._rest\n } = props;\n const params = _rest as Exclude<typeof props, MotionComponentProps>;\n const [child, childRef] = useChildElement(children);\n\n const handleRef = useMotionImperativeRef(imperativeRef);\n const skipMotions = useMotionBehaviourContext() === 'skip';\n const optionsRef = React.useRef<{ skipMotions: boolean; params: MotionParams }>({\n skipMotions,\n params,\n });\n\n const animateAtoms = useAnimateAtoms();\n const isReducedMotion = useIsReducedMotion();\n\n const onMotionStart = useEventCallback(() => {\n onMotionStartProp?.(null);\n });\n\n const onMotionFinish = useEventCallback(() => {\n onMotionFinishProp?.(null);\n });\n\n const onMotionCancel = useEventCallback(() => {\n onMotionCancelProp?.(null);\n });\n\n useIsomorphicLayoutEffect(() => {\n // Heads up!\n // We store the params in a ref to avoid re-rendering the component when the params change.\n optionsRef.current = { skipMotions, params };\n });\n\n useIsomorphicLayoutEffect(() => {\n const element = childRef.current;\n\n if (element) {\n const atoms = typeof value === 'function' ? value({ element, ...optionsRef.current.params }) : value;\n\n onMotionStart();\n const handle = animateAtoms(element, atoms, { isReducedMotion: isReducedMotion() });\n handleRef.current = handle;\n handle.setMotionEndCallbacks(onMotionFinish, onMotionCancel);\n\n if (optionsRef.current.skipMotions) {\n handle.finish();\n }\n\n return () => {\n handle.cancel();\n };\n }\n }, [animateAtoms, childRef, handleRef, isReducedMotion, onMotionFinish, onMotionStart, onMotionCancel]);\n\n return child;\n };\n\n return Object.assign(Atom, {\n // Heads up!\n // Always normalize it to a function to simplify types\n [MOTION_DEFINITION]: typeof value === 'function' ? value : () => value,\n });\n}\n"],"names":["useEventCallback","useIsomorphicLayoutEffect","React","useAnimateAtoms","useMotionImperativeRef","useIsReducedMotion","useChildElement","useMotionBehaviourContext","MOTION_DEFINITION","Symbol","createMotionComponent","value","Atom","props","children","imperativeRef","onMotionFinish","onMotionFinishProp","onMotionStart","onMotionStartProp","onMotionCancel","onMotionCancelProp","_rest","params","child","childRef","handleRef","skipMotions","optionsRef","useRef","animateAtoms","isReducedMotion","current","element","atoms","handle","setMotionEndCallbacks","finish","cancel","Object","assign"],"mappings":"AAAA;AAGA,SAASA,gBAAgB,EAAEC,yBAAyB,QAAQ,4BAA4B;AACxF,YAAYC,WAAW,QAAQ;AAE/B,SAASC,eAAe,QAAQ,2BAA2B;AAC3D,SAASC,sBAAsB,QAAQ,kCAAkC;AACzE,SAASC,kBAAkB,QAAQ,8BAA8B;AACjE,SAASC,eAAe,QAAQ,2BAA2B;AAE3D,SAASC,yBAAyB,QAAQ,qCAAqC;AAE/E;;;;CAIC,GACD,OAAO,MAAMC,oBAAoBC,OAAO,qBAAqB;AA2C7D;;;;CAIC,GACD,OAAO,SAASC,sBACdC,KAA6D;IAE7D,MAAMC,OAAsDC,CAAAA;QAC1D;QAEA,MAAM,EACJC,QAAQ,EACRC,aAAa,EACbC,gBAAgBC,kBAAkB,EAClCC,eAAeC,iBAAiB,EAChCC,gBAAgBC,kBAAkB,EAClC,GAAGC,OACJ,GAAGT;QACJ,MAAMU,SAASD;QACf,MAAM,CAACE,OAAOC,SAAS,GAAGnB,gBAAgBQ;QAE1C,MAAMY,YAAYtB,uBAAuBW;QACzC,MAAMY,cAAcpB,gCAAgC;QACpD,MAAMqB,aAAa1B,MAAM2B,MAAM,CAAiD;YAC9EF;YACAJ;QACF;QAEA,MAAMO,eAAe3B;QACrB,MAAM4B,kBAAkB1B;QAExB,MAAMa,gBAAgBlB,iBAAiB;YACrCmB,8BAAAA,wCAAAA,kBAAoB;QACtB;QAEA,MAAMH,iBAAiBhB,iBAAiB;YACtCiB,+BAAAA,yCAAAA,mBAAqB;QACvB;QAEA,MAAMG,iBAAiBpB,iBAAiB;YACtCqB,+BAAAA,yCAAAA,mBAAqB;QACvB;QAEApB,0BAA0B;YACxB,YAAY;YACZ,2FAA2F;YAC3F2B,WAAWI,OAAO,GAAG;gBAAEL;gBAAaJ;YAAO;QAC7C;QAEAtB,0BAA0B;YACxB,MAAMgC,UAAUR,SAASO,OAAO;YAEhC,IAAIC,SAAS;gBACX,MAAMC,QAAQ,OAAOvB,UAAU,aAAaA,MAAM;oBAAEsB;oBAAS,GAAGL,WAAWI,OAAO,CAACT,MAAM;gBAAC,KAAKZ;gBAE/FO;gBACA,MAAMiB,SAASL,aAAaG,SAASC,OAAO;oBAAEH,iBAAiBA;gBAAkB;gBACjFL,UAAUM,OAAO,GAAGG;gBACpBA,OAAOC,qBAAqB,CAACpB,gBAAgBI;gBAE7C,IAAIQ,WAAWI,OAAO,CAACL,WAAW,EAAE;oBAClCQ,OAAOE,MAAM;gBACf;gBAEA,OAAO;oBACLF,OAAOG,MAAM;gBACf;YACF;QACF,GAAG;YAACR;YAAcL;YAAUC;YAAWK;YAAiBf;YAAgBE;YAAeE;SAAe;QAEtG,OAAOI;IACT;IAEA,OAAOe,OAAOC,MAAM,CAAC5B,MAAM;QACzB,YAAY;QACZ,sDAAsD;QACtD,CAACJ,kBAAkB,EAAE,OAAOG,UAAU,aAAaA,QAAQ,IAAMA;IACnE;AACF"}
1
+ {"version":3,"sources":["../src/factories/createMotionComponent.ts"],"sourcesContent":["'use client';\n\nimport type { JSXElement } from '@fluentui/react-utilities';\nimport { useEventCallback, useIsomorphicLayoutEffect } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport { useAnimateAtoms } from '../hooks/useAnimateAtoms';\nimport { useMotionImperativeRef } from '../hooks/useMotionImperativeRef';\nimport { useIsReducedMotion } from '../hooks/useIsReducedMotion';\nimport { useChildElement } from '../utils/useChildElement';\nimport type { AtomMotion, AtomMotionFn, MotionParam, MotionImperativeRef, AnimationHandle } from '../types';\nimport { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';\n\n/**\n * A private symbol to store the motion definition on the component for variants.\n *\n * @internal\n */\nexport const MOTION_DEFINITION = Symbol('MOTION_DEFINITION');\n\nexport type MotionComponentProps = {\n children: JSXElement;\n\n /** Provides imperative controls for the animation. */\n imperativeRef?: React.Ref<MotionImperativeRef | undefined>;\n\n /**\n * Callback that is called when the whole motion finishes.\n *\n * A motion definition can contain multiple animations and therefore multiple \"finish\" events. The callback is\n * triggered once all animations have finished with \"null\" instead of an event object to avoid ambiguity.\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionFinish?: (ev: null) => void;\n\n /**\n * Callback that is called when the whole motion is cancelled.\n *\n * A motion definition can contain multiple animations and therefore multiple \"cancel\" events. The callback is\n * triggered once all animations have been cancelled with \"null\" instead of an event object to avoid ambiguity.\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionCancel?: (ev: null) => void;\n\n /**\n * Callback that is called when the whole motion starts.\n *\n * A motion definition can contain multiple animations and therefore multiple \"start\" events. The callback is\n * triggered when the first animation is started. There is no official \"start\" event with the Web Animations API.\n * so the callback is triggered with \"null\".\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionStart?: (ev: null) => void;\n\n /**\n * When this value changes, the animation replays from the start on the same DOM element,\n * cancelling any in-progress animation, without remounting the component or its children.\n *\n * **Why not just use a React `key`?** Changing a React `key` forces a full unmount and\n * remount of the subtree: DOM nodes are destroyed and recreated, focus is lost, and any\n * child state is reset. `replayKey` avoids all of that — only the animation effect reruns\n * while the DOM and component state remain intact.\n *\n * Use this when you want to retrigger a motion in response to a state change (e.g. a user\n * action or a data update) while preserving DOM continuity. It is the declarative equivalent\n * of calling `imperativeRef.current.play()` but driven by a prop rather than a ref call.\n *\n * @example\n * ```tsx\n * // Replay a Fade.In each time the user clicks \"Refresh\"\n * const [replayKey, setReplayKey] = React.useState(0);\n * <Fade.In replayKey={replayKey}>\n * <div>Content</div>\n * </Fade.In>\n * <button onClick={() => setReplayKey(k => k + 1)}>Refresh</button>\n * ```\n */\n replayKey?: string | number;\n};\n\nexport type MotionComponent<MotionParams extends Record<string, MotionParam> = {}> = React.FC<\n MotionComponentProps & MotionParams\n> & {\n [MOTION_DEFINITION]: AtomMotionFn<MotionParams>;\n};\n\n/**\n * Creates a component that will animate the children using the provided motion.\n *\n * @param value - A motion definition.\n */\nexport function createMotionComponent<MotionParams extends Record<string, MotionParam> = {}>(\n value: AtomMotion | AtomMotion[] | AtomMotionFn<MotionParams>,\n): MotionComponent<MotionParams> {\n const Atom: React.FC<MotionComponentProps & MotionParams> = props => {\n 'use no memo';\n\n const {\n children,\n imperativeRef,\n onMotionFinish: onMotionFinishProp,\n onMotionStart: onMotionStartProp,\n onMotionCancel: onMotionCancelProp,\n replayKey,\n ..._rest\n } = props;\n const params = _rest as Exclude<typeof props, MotionComponentProps>;\n const [child, childRef] = useChildElement(children);\n\n const handleRef = useMotionImperativeRef(imperativeRef);\n const isInitialRender = React.useRef(true);\n const skipMotions = useMotionBehaviourContext() === 'skip';\n const optionsRef = React.useRef<{ skipMotions: boolean; params: MotionParams }>({\n skipMotions,\n params,\n });\n\n const animateAtoms = useAnimateAtoms();\n const isReducedMotion = useIsReducedMotion();\n\n const onMotionStart = useEventCallback(() => {\n onMotionStartProp?.(null);\n });\n\n const onMotionFinish = useEventCallback(() => {\n onMotionFinishProp?.(null);\n });\n\n const onMotionCancel = useEventCallback(() => {\n onMotionCancelProp?.(null);\n });\n\n // Stable callback (all deps are refs or useEventCallback) that activates a handle for a new playback cycle.\n //\n // TODO: consider moving the cancel+play+rewire sequence into a handle.replay() method on AnimationHandle,\n // keeping pure animation sequencing on the handle and React callbacks here in the component.\n const activateAnimationHandle = React.useCallback(\n (handle: AnimationHandle) => {\n onMotionStart();\n handle.setMotionEndCallbacks(onMotionFinish, onMotionCancel);\n if (optionsRef.current.skipMotions) {\n handle.finish();\n }\n },\n [onMotionStart, onMotionFinish, onMotionCancel],\n );\n\n useIsomorphicLayoutEffect(() => {\n // Heads up!\n // We store the params in a ref to avoid re-rendering the component when the params change.\n optionsRef.current = { skipMotions, params };\n });\n\n useIsomorphicLayoutEffect(() => {\n const element = childRef.current;\n\n if (element) {\n const atoms = typeof value === 'function' ? value({ element, ...optionsRef.current.params }) : value;\n\n const handle = animateAtoms(element, atoms, { isReducedMotion: isReducedMotion() });\n handleRef.current = handle;\n activateAnimationHandle(handle);\n\n return () => {\n handle.cancel();\n };\n }\n }, [animateAtoms, childRef, handleRef, isReducedMotion, activateAnimationHandle]);\n\n // Skips initial mount; on replayKey changes, reuses existing Animation objects via cancel+play\n // rather than recreating them, preserving DOM continuity.\n useIsomorphicLayoutEffect(() => {\n if (isInitialRender.current) {\n return;\n }\n\n const handle = handleRef.current;\n if (handle) {\n handle.cancel();\n handle.play();\n activateAnimationHandle(handle);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- replayKey is intentionally the only trigger; other deps are stable refs/callbacks\n }, [replayKey]);\n\n useIsomorphicLayoutEffect(() => {\n isInitialRender.current = false;\n\n return () => {\n isInitialRender.current = true;\n };\n }, []);\n\n return child;\n };\n\n return Object.assign(Atom, {\n // Heads up!\n // Always normalize it to a function to simplify types\n [MOTION_DEFINITION]: typeof value === 'function' ? value : () => value,\n });\n}\n"],"names":["useEventCallback","useIsomorphicLayoutEffect","React","useAnimateAtoms","useMotionImperativeRef","useIsReducedMotion","useChildElement","useMotionBehaviourContext","MOTION_DEFINITION","Symbol","createMotionComponent","value","Atom","props","children","imperativeRef","onMotionFinish","onMotionFinishProp","onMotionStart","onMotionStartProp","onMotionCancel","onMotionCancelProp","replayKey","_rest","params","child","childRef","handleRef","isInitialRender","useRef","skipMotions","optionsRef","animateAtoms","isReducedMotion","activateAnimationHandle","useCallback","handle","setMotionEndCallbacks","current","finish","element","atoms","cancel","play","Object","assign"],"mappings":"AAAA;AAGA,SAASA,gBAAgB,EAAEC,yBAAyB,QAAQ,4BAA4B;AACxF,YAAYC,WAAW,QAAQ;AAE/B,SAASC,eAAe,QAAQ,2BAA2B;AAC3D,SAASC,sBAAsB,QAAQ,kCAAkC;AACzE,SAASC,kBAAkB,QAAQ,8BAA8B;AACjE,SAASC,eAAe,QAAQ,2BAA2B;AAE3D,SAASC,yBAAyB,QAAQ,qCAAqC;AAE/E;;;;CAIC,GACD,OAAO,MAAMC,oBAAoBC,OAAO,qBAAqB;AAoE7D;;;;CAIC,GACD,OAAO,SAASC,sBACdC,KAA6D;IAE7D,MAAMC,OAAsDC,CAAAA;QAC1D;QAEA,MAAM,EACJC,QAAQ,EACRC,aAAa,EACbC,gBAAgBC,kBAAkB,EAClCC,eAAeC,iBAAiB,EAChCC,gBAAgBC,kBAAkB,EAClCC,SAAS,EACT,GAAGC,OACJ,GAAGV;QACJ,MAAMW,SAASD;QACf,MAAM,CAACE,OAAOC,SAAS,GAAGpB,gBAAgBQ;QAE1C,MAAMa,YAAYvB,uBAAuBW;QACzC,MAAMa,kBAAkB1B,MAAM2B,MAAM,CAAC;QACrC,MAAMC,cAAcvB,gCAAgC;QACpD,MAAMwB,aAAa7B,MAAM2B,MAAM,CAAiD;YAC9EC;YACAN;QACF;QAEA,MAAMQ,eAAe7B;QACrB,MAAM8B,kBAAkB5B;QAExB,MAAMa,gBAAgBlB,iBAAiB;YACrCmB,8BAAAA,wCAAAA,kBAAoB;QACtB;QAEA,MAAMH,iBAAiBhB,iBAAiB;YACtCiB,+BAAAA,yCAAAA,mBAAqB;QACvB;QAEA,MAAMG,iBAAiBpB,iBAAiB;YACtCqB,+BAAAA,yCAAAA,mBAAqB;QACvB;QAEA,4GAA4G;QAC5G,EAAE;QACF,0GAA0G;QAC1G,6FAA6F;QAC7F,MAAMa,0BAA0BhC,MAAMiC,WAAW,CAC/C,CAACC;YACClB;YACAkB,OAAOC,qBAAqB,CAACrB,gBAAgBI;YAC7C,IAAIW,WAAWO,OAAO,CAACR,WAAW,EAAE;gBAClCM,OAAOG,MAAM;YACf;QACF,GACA;YAACrB;YAAeF;YAAgBI;SAAe;QAGjDnB,0BAA0B;YACxB,YAAY;YACZ,2FAA2F;YAC3F8B,WAAWO,OAAO,GAAG;gBAAER;gBAAaN;YAAO;QAC7C;QAEAvB,0BAA0B;YACxB,MAAMuC,UAAUd,SAASY,OAAO;YAEhC,IAAIE,SAAS;gBACX,MAAMC,QAAQ,OAAO9B,UAAU,aAAaA,MAAM;oBAAE6B;oBAAS,GAAGT,WAAWO,OAAO,CAACd,MAAM;gBAAC,KAAKb;gBAE/F,MAAMyB,SAASJ,aAAaQ,SAASC,OAAO;oBAAER,iBAAiBA;gBAAkB;gBACjFN,UAAUW,OAAO,GAAGF;gBACpBF,wBAAwBE;gBAExB,OAAO;oBACLA,OAAOM,MAAM;gBACf;YACF;QACF,GAAG;YAACV;YAAcN;YAAUC;YAAWM;YAAiBC;SAAwB;QAEhF,+FAA+F;QAC/F,0DAA0D;QAC1DjC,0BAA0B;YACxB,IAAI2B,gBAAgBU,OAAO,EAAE;gBAC3B;YACF;YAEA,MAAMF,SAAST,UAAUW,OAAO;YAChC,IAAIF,QAAQ;gBACVA,OAAOM,MAAM;gBACbN,OAAOO,IAAI;gBACXT,wBAAwBE;YAC1B;QACA,4IAA4I;QAC9I,GAAG;YAACd;SAAU;QAEdrB,0BAA0B;YACxB2B,gBAAgBU,OAAO,GAAG;YAE1B,OAAO;gBACLV,gBAAgBU,OAAO,GAAG;YAC5B;QACF,GAAG,EAAE;QAEL,OAAOb;IACT;IAEA,OAAOmB,OAAOC,MAAM,CAACjC,MAAM;QACzB,YAAY;QACZ,sDAAsD;QACtD,CAACJ,kBAAkB,EAAE,OAAOG,UAAU,aAAaA,QAAQ,IAAMA;IACnE;AACF"}
@@ -20,6 +20,7 @@ import * as React from 'react';
20
20
  mountedRef.current = visible;
21
21
  }
22
22
  });
23
+ // eslint-disable-next-line react-hooks/refs
23
24
  return [
24
25
  visible || mountedRef.current,
25
26
  setMounted
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks/useMountedState.ts"],"sourcesContent":["'use client';\n\nimport { useForceUpdate } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\n/**\n * This hook manages the mounted state of a component, based on the \"visible\" and \"unmountOnExit\" props.\n * It simulates the behavior of getDerivedStateFromProps(), which is not available in functional components.\n */\nexport function useMountedState(\n visible: boolean = false,\n unmountOnExit: boolean = false,\n): [boolean, (value: boolean) => void] {\n const mountedRef = React.useRef<boolean>(unmountOnExit ? visible : true);\n const forceUpdate = useForceUpdate();\n\n const setMounted = React.useCallback(\n (newValue: boolean) => {\n if (mountedRef.current !== newValue) {\n mountedRef.current = newValue;\n forceUpdate();\n }\n },\n [forceUpdate],\n );\n\n React.useEffect(() => {\n if (visible) {\n mountedRef.current = visible;\n }\n });\n\n return [visible || mountedRef.current, setMounted];\n}\n"],"names":["useForceUpdate","React","useMountedState","visible","unmountOnExit","mountedRef","useRef","forceUpdate","setMounted","useCallback","newValue","current","useEffect"],"mappings":"AAAA;AAEA,SAASA,cAAc,QAAQ,4BAA4B;AAC3D,YAAYC,WAAW,QAAQ;AAE/B;;;CAGC,GACD,OAAO,SAASC,gBACdC,UAAmB,KAAK,EACxBC,gBAAyB,KAAK;IAE9B,MAAMC,aAAaJ,MAAMK,MAAM,CAAUF,gBAAgBD,UAAU;IACnE,MAAMI,cAAcP;IAEpB,MAAMQ,aAAaP,MAAMQ,WAAW,CAClC,CAACC;QACC,IAAIL,WAAWM,OAAO,KAAKD,UAAU;YACnCL,WAAWM,OAAO,GAAGD;YACrBH;QACF;IACF,GACA;QAACA;KAAY;IAGfN,MAAMW,SAAS,CAAC;QACd,IAAIT,SAAS;YACXE,WAAWM,OAAO,GAAGR;QACvB;IACF;IAEA,OAAO;QAACA,WAAWE,WAAWM,OAAO;QAAEH;KAAW;AACpD"}
1
+ {"version":3,"sources":["../src/hooks/useMountedState.ts"],"sourcesContent":["'use client';\n\nimport { useForceUpdate } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\n/**\n * This hook manages the mounted state of a component, based on the \"visible\" and \"unmountOnExit\" props.\n * It simulates the behavior of getDerivedStateFromProps(), which is not available in functional components.\n */\nexport function useMountedState(\n visible: boolean = false,\n unmountOnExit: boolean = false,\n): [boolean, (value: boolean) => void] {\n const mountedRef = React.useRef<boolean>(unmountOnExit ? visible : true);\n const forceUpdate = useForceUpdate();\n\n const setMounted = React.useCallback(\n (newValue: boolean) => {\n if (mountedRef.current !== newValue) {\n mountedRef.current = newValue;\n forceUpdate();\n }\n },\n [forceUpdate],\n );\n\n React.useEffect(() => {\n if (visible) {\n mountedRef.current = visible;\n }\n });\n\n // eslint-disable-next-line react-hooks/refs\n return [visible || mountedRef.current, setMounted];\n}\n"],"names":["useForceUpdate","React","useMountedState","visible","unmountOnExit","mountedRef","useRef","forceUpdate","setMounted","useCallback","newValue","current","useEffect"],"mappings":"AAAA;AAEA,SAASA,cAAc,QAAQ,4BAA4B;AAC3D,YAAYC,WAAW,QAAQ;AAE/B;;;CAGC,GACD,OAAO,SAASC,gBACdC,UAAmB,KAAK,EACxBC,gBAAyB,KAAK;IAE9B,MAAMC,aAAaJ,MAAMK,MAAM,CAAUF,gBAAgBD,UAAU;IACnE,MAAMI,cAAcP;IAEpB,MAAMQ,aAAaP,MAAMQ,WAAW,CAClC,CAACC;QACC,IAAIL,WAAWM,OAAO,KAAKD,UAAU;YACnCL,WAAWM,OAAO,GAAGD;YACrBH;QACF;IACF,GACA;QAACA;KAAY;IAGfN,MAAMW,SAAS,CAAC;QACd,IAAIT,SAAS;YACXE,WAAWM,OAAO,GAAGR;QACvB;IACF;IAEA,4CAA4C;IAC5C,OAAO;QAACA,WAAWE,WAAWM,OAAO;QAAEH;KAAW;AACpD"}
@@ -29,10 +29,11 @@ const MOTION_DEFINITION = Symbol('MOTION_DEFINITION');
29
29
  function createMotionComponent(value) {
30
30
  const Atom = (props)=>{
31
31
  'use no memo';
32
- const { children, imperativeRef, onMotionFinish: onMotionFinishProp, onMotionStart: onMotionStartProp, onMotionCancel: onMotionCancelProp, ..._rest } = props;
32
+ const { children, imperativeRef, onMotionFinish: onMotionFinishProp, onMotionStart: onMotionStartProp, onMotionCancel: onMotionCancelProp, replayKey, ..._rest } = props;
33
33
  const params = _rest;
34
34
  const [child, childRef] = (0, _useChildElement.useChildElement)(children);
35
35
  const handleRef = (0, _useMotionImperativeRef.useMotionImperativeRef)(imperativeRef);
36
+ const isInitialRender = _react.useRef(true);
36
37
  const skipMotions = (0, _MotionBehaviourContext.useMotionBehaviourContext)() === 'skip';
37
38
  const optionsRef = _react.useRef({
38
39
  skipMotions,
@@ -49,6 +50,21 @@ function createMotionComponent(value) {
49
50
  const onMotionCancel = (0, _reactutilities.useEventCallback)(()=>{
50
51
  onMotionCancelProp === null || onMotionCancelProp === void 0 ? void 0 : onMotionCancelProp(null);
51
52
  });
53
+ // Stable callback (all deps are refs or useEventCallback) that activates a handle for a new playback cycle.
54
+ //
55
+ // TODO: consider moving the cancel+play+rewire sequence into a handle.replay() method on AnimationHandle,
56
+ // keeping pure animation sequencing on the handle and React callbacks here in the component.
57
+ const activateAnimationHandle = _react.useCallback((handle)=>{
58
+ onMotionStart();
59
+ handle.setMotionEndCallbacks(onMotionFinish, onMotionCancel);
60
+ if (optionsRef.current.skipMotions) {
61
+ handle.finish();
62
+ }
63
+ }, [
64
+ onMotionStart,
65
+ onMotionFinish,
66
+ onMotionCancel
67
+ ]);
52
68
  (0, _reactutilities.useIsomorphicLayoutEffect)(()=>{
53
69
  // Heads up!
54
70
  // We store the params in a ref to avoid re-rendering the component when the params change.
@@ -64,15 +80,11 @@ function createMotionComponent(value) {
64
80
  element,
65
81
  ...optionsRef.current.params
66
82
  }) : value;
67
- onMotionStart();
68
83
  const handle = animateAtoms(element, atoms, {
69
84
  isReducedMotion: isReducedMotion()
70
85
  });
71
86
  handleRef.current = handle;
72
- handle.setMotionEndCallbacks(onMotionFinish, onMotionCancel);
73
- if (optionsRef.current.skipMotions) {
74
- handle.finish();
75
- }
87
+ activateAnimationHandle(handle);
76
88
  return ()=>{
77
89
  handle.cancel();
78
90
  };
@@ -82,10 +94,30 @@ function createMotionComponent(value) {
82
94
  childRef,
83
95
  handleRef,
84
96
  isReducedMotion,
85
- onMotionFinish,
86
- onMotionStart,
87
- onMotionCancel
97
+ activateAnimationHandle
98
+ ]);
99
+ // Skips initial mount; on replayKey changes, reuses existing Animation objects via cancel+play
100
+ // rather than recreating them, preserving DOM continuity.
101
+ (0, _reactutilities.useIsomorphicLayoutEffect)(()=>{
102
+ if (isInitialRender.current) {
103
+ return;
104
+ }
105
+ const handle = handleRef.current;
106
+ if (handle) {
107
+ handle.cancel();
108
+ handle.play();
109
+ activateAnimationHandle(handle);
110
+ }
111
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- replayKey is intentionally the only trigger; other deps are stable refs/callbacks
112
+ }, [
113
+ replayKey
88
114
  ]);
115
+ (0, _reactutilities.useIsomorphicLayoutEffect)(()=>{
116
+ isInitialRender.current = false;
117
+ return ()=>{
118
+ isInitialRender.current = true;
119
+ };
120
+ }, []);
89
121
  return child;
90
122
  };
91
123
  return Object.assign(Atom, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/factories/createMotionComponent.ts"],"sourcesContent":["'use client';\n\nimport type { JSXElement } from '@fluentui/react-utilities';\nimport { useEventCallback, useIsomorphicLayoutEffect } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport { useAnimateAtoms } from '../hooks/useAnimateAtoms';\nimport { useMotionImperativeRef } from '../hooks/useMotionImperativeRef';\nimport { useIsReducedMotion } from '../hooks/useIsReducedMotion';\nimport { useChildElement } from '../utils/useChildElement';\nimport type { AtomMotion, AtomMotionFn, MotionParam, MotionImperativeRef } from '../types';\nimport { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';\n\n/**\n * A private symbol to store the motion definition on the component for variants.\n *\n * @internal\n */\nexport const MOTION_DEFINITION = Symbol('MOTION_DEFINITION');\n\nexport type MotionComponentProps = {\n children: JSXElement;\n\n /** Provides imperative controls for the animation. */\n imperativeRef?: React.Ref<MotionImperativeRef | undefined>;\n\n /**\n * Callback that is called when the whole motion finishes.\n *\n * A motion definition can contain multiple animations and therefore multiple \"finish\" events. The callback is\n * triggered once all animations have finished with \"null\" instead of an event object to avoid ambiguity.\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionFinish?: (ev: null) => void;\n\n /**\n * Callback that is called when the whole motion is cancelled.\n *\n * A motion definition can contain multiple animations and therefore multiple \"cancel\" events. The callback is\n * triggered once all animations have been cancelled with \"null\" instead of an event object to avoid ambiguity.\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionCancel?: (ev: null) => void;\n\n /**\n * Callback that is called when the whole motion starts.\n *\n * A motion definition can contain multiple animations and therefore multiple \"start\" events. The callback is\n * triggered when the first animation is started. There is no official \"start\" event with the Web Animations API.\n * so the callback is triggered with \"null\".\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionStart?: (ev: null) => void;\n};\n\nexport type MotionComponent<MotionParams extends Record<string, MotionParam> = {}> = React.FC<\n MotionComponentProps & MotionParams\n> & {\n [MOTION_DEFINITION]: AtomMotionFn<MotionParams>;\n};\n\n/**\n * Creates a component that will animate the children using the provided motion.\n *\n * @param value - A motion definition.\n */\nexport function createMotionComponent<MotionParams extends Record<string, MotionParam> = {}>(\n value: AtomMotion | AtomMotion[] | AtomMotionFn<MotionParams>,\n): MotionComponent<MotionParams> {\n const Atom: React.FC<MotionComponentProps & MotionParams> = props => {\n 'use no memo';\n\n const {\n children,\n imperativeRef,\n onMotionFinish: onMotionFinishProp,\n onMotionStart: onMotionStartProp,\n onMotionCancel: onMotionCancelProp,\n ..._rest\n } = props;\n const params = _rest as Exclude<typeof props, MotionComponentProps>;\n const [child, childRef] = useChildElement(children);\n\n const handleRef = useMotionImperativeRef(imperativeRef);\n const skipMotions = useMotionBehaviourContext() === 'skip';\n const optionsRef = React.useRef<{ skipMotions: boolean; params: MotionParams }>({\n skipMotions,\n params,\n });\n\n const animateAtoms = useAnimateAtoms();\n const isReducedMotion = useIsReducedMotion();\n\n const onMotionStart = useEventCallback(() => {\n onMotionStartProp?.(null);\n });\n\n const onMotionFinish = useEventCallback(() => {\n onMotionFinishProp?.(null);\n });\n\n const onMotionCancel = useEventCallback(() => {\n onMotionCancelProp?.(null);\n });\n\n useIsomorphicLayoutEffect(() => {\n // Heads up!\n // We store the params in a ref to avoid re-rendering the component when the params change.\n optionsRef.current = { skipMotions, params };\n });\n\n useIsomorphicLayoutEffect(() => {\n const element = childRef.current;\n\n if (element) {\n const atoms = typeof value === 'function' ? value({ element, ...optionsRef.current.params }) : value;\n\n onMotionStart();\n const handle = animateAtoms(element, atoms, { isReducedMotion: isReducedMotion() });\n handleRef.current = handle;\n handle.setMotionEndCallbacks(onMotionFinish, onMotionCancel);\n\n if (optionsRef.current.skipMotions) {\n handle.finish();\n }\n\n return () => {\n handle.cancel();\n };\n }\n }, [animateAtoms, childRef, handleRef, isReducedMotion, onMotionFinish, onMotionStart, onMotionCancel]);\n\n return child;\n };\n\n return Object.assign(Atom, {\n // Heads up!\n // Always normalize it to a function to simplify types\n [MOTION_DEFINITION]: typeof value === 'function' ? value : () => value,\n });\n}\n"],"names":["MOTION_DEFINITION","createMotionComponent","Symbol","value","Atom","props","children","imperativeRef","onMotionFinish","onMotionFinishProp","onMotionStart","onMotionStartProp","onMotionCancel","onMotionCancelProp","_rest","params","child","childRef","useChildElement","handleRef","useMotionImperativeRef","skipMotions","useMotionBehaviourContext","optionsRef","React","useRef","animateAtoms","useAnimateAtoms","isReducedMotion","useIsReducedMotion","useEventCallback","useIsomorphicLayoutEffect","current","element","atoms","handle","setMotionEndCallbacks","finish","cancel","Object","assign"],"mappings":"AAAA;;;;;;;;;;;;IAkBaA,iBAAiB;eAAjBA;;IAgDGC,qBAAqB;eAArBA;;;;gCA/D4C;iEACrC;iCAES;wCACO;oCACJ;iCACH;wCAEU;AAOnC,MAAMD,oBAAoBE,OAAO;AAgDjC,SAASD,sBACdE,KAA6D;IAE7D,MAAMC,OAAsDC,CAAAA;QAC1D;QAEA,MAAM,EACJC,QAAQ,EACRC,aAAa,EACbC,gBAAgBC,kBAAkB,EAClCC,eAAeC,iBAAiB,EAChCC,gBAAgBC,kBAAkB,EAClC,GAAGC,OACJ,GAAGT;QACJ,MAAMU,SAASD;QACf,MAAM,CAACE,OAAOC,SAAS,GAAGC,IAAAA,gCAAe,EAACZ;QAE1C,MAAMa,YAAYC,IAAAA,8CAAsB,EAACb;QACzC,MAAMc,cAAcC,IAAAA,iDAAyB,QAAO;QACpD,MAAMC,aAAaC,OAAMC,MAAM,CAAiD;YAC9EJ;YACAN;QACF;QAEA,MAAMW,eAAeC,IAAAA,gCAAe;QACpC,MAAMC,kBAAkBC,IAAAA,sCAAkB;QAE1C,MAAMnB,gBAAgBoB,IAAAA,gCAAgB,EAAC;YACrCnB,8BAAAA,wCAAAA,kBAAoB;QACtB;QAEA,MAAMH,iBAAiBsB,IAAAA,gCAAgB,EAAC;YACtCrB,+BAAAA,yCAAAA,mBAAqB;QACvB;QAEA,MAAMG,iBAAiBkB,IAAAA,gCAAgB,EAAC;YACtCjB,+BAAAA,yCAAAA,mBAAqB;QACvB;QAEAkB,IAAAA,yCAAyB,EAAC;YACxB,YAAY;YACZ,2FAA2F;YAC3FR,WAAWS,OAAO,GAAG;gBAAEX;gBAAaN;YAAO;QAC7C;QAEAgB,IAAAA,yCAAyB,EAAC;YACxB,MAAME,UAAUhB,SAASe,OAAO;YAEhC,IAAIC,SAAS;gBACX,MAAMC,QAAQ,OAAO/B,UAAU,aAAaA,MAAM;oBAAE8B;oBAAS,GAAGV,WAAWS,OAAO,CAACjB,MAAM;gBAAC,KAAKZ;gBAE/FO;gBACA,MAAMyB,SAAST,aAAaO,SAASC,OAAO;oBAAEN,iBAAiBA;gBAAkB;gBACjFT,UAAUa,OAAO,GAAGG;gBACpBA,OAAOC,qBAAqB,CAAC5B,gBAAgBI;gBAE7C,IAAIW,WAAWS,OAAO,CAACX,WAAW,EAAE;oBAClCc,OAAOE,MAAM;gBACf;gBAEA,OAAO;oBACLF,OAAOG,MAAM;gBACf;YACF;QACF,GAAG;YAACZ;YAAcT;YAAUE;YAAWS;YAAiBpB;YAAgBE;YAAeE;SAAe;QAEtG,OAAOI;IACT;IAEA,OAAOuB,OAAOC,MAAM,CAACpC,MAAM;QACzB,YAAY;QACZ,sDAAsD;QACtD,CAACJ,kBAAkB,EAAE,OAAOG,UAAU,aAAaA,QAAQ,IAAMA;IACnE;AACF"}
1
+ {"version":3,"sources":["../src/factories/createMotionComponent.ts"],"sourcesContent":["'use client';\n\nimport type { JSXElement } from '@fluentui/react-utilities';\nimport { useEventCallback, useIsomorphicLayoutEffect } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport { useAnimateAtoms } from '../hooks/useAnimateAtoms';\nimport { useMotionImperativeRef } from '../hooks/useMotionImperativeRef';\nimport { useIsReducedMotion } from '../hooks/useIsReducedMotion';\nimport { useChildElement } from '../utils/useChildElement';\nimport type { AtomMotion, AtomMotionFn, MotionParam, MotionImperativeRef, AnimationHandle } from '../types';\nimport { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';\n\n/**\n * A private symbol to store the motion definition on the component for variants.\n *\n * @internal\n */\nexport const MOTION_DEFINITION = Symbol('MOTION_DEFINITION');\n\nexport type MotionComponentProps = {\n children: JSXElement;\n\n /** Provides imperative controls for the animation. */\n imperativeRef?: React.Ref<MotionImperativeRef | undefined>;\n\n /**\n * Callback that is called when the whole motion finishes.\n *\n * A motion definition can contain multiple animations and therefore multiple \"finish\" events. The callback is\n * triggered once all animations have finished with \"null\" instead of an event object to avoid ambiguity.\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionFinish?: (ev: null) => void;\n\n /**\n * Callback that is called when the whole motion is cancelled.\n *\n * A motion definition can contain multiple animations and therefore multiple \"cancel\" events. The callback is\n * triggered once all animations have been cancelled with \"null\" instead of an event object to avoid ambiguity.\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionCancel?: (ev: null) => void;\n\n /**\n * Callback that is called when the whole motion starts.\n *\n * A motion definition can contain multiple animations and therefore multiple \"start\" events. The callback is\n * triggered when the first animation is started. There is no official \"start\" event with the Web Animations API.\n * so the callback is triggered with \"null\".\n */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler<T> does not support \"null\"\n onMotionStart?: (ev: null) => void;\n\n /**\n * When this value changes, the animation replays from the start on the same DOM element,\n * cancelling any in-progress animation, without remounting the component or its children.\n *\n * **Why not just use a React `key`?** Changing a React `key` forces a full unmount and\n * remount of the subtree: DOM nodes are destroyed and recreated, focus is lost, and any\n * child state is reset. `replayKey` avoids all of that — only the animation effect reruns\n * while the DOM and component state remain intact.\n *\n * Use this when you want to retrigger a motion in response to a state change (e.g. a user\n * action or a data update) while preserving DOM continuity. It is the declarative equivalent\n * of calling `imperativeRef.current.play()` but driven by a prop rather than a ref call.\n *\n * @example\n * ```tsx\n * // Replay a Fade.In each time the user clicks \"Refresh\"\n * const [replayKey, setReplayKey] = React.useState(0);\n * <Fade.In replayKey={replayKey}>\n * <div>Content</div>\n * </Fade.In>\n * <button onClick={() => setReplayKey(k => k + 1)}>Refresh</button>\n * ```\n */\n replayKey?: string | number;\n};\n\nexport type MotionComponent<MotionParams extends Record<string, MotionParam> = {}> = React.FC<\n MotionComponentProps & MotionParams\n> & {\n [MOTION_DEFINITION]: AtomMotionFn<MotionParams>;\n};\n\n/**\n * Creates a component that will animate the children using the provided motion.\n *\n * @param value - A motion definition.\n */\nexport function createMotionComponent<MotionParams extends Record<string, MotionParam> = {}>(\n value: AtomMotion | AtomMotion[] | AtomMotionFn<MotionParams>,\n): MotionComponent<MotionParams> {\n const Atom: React.FC<MotionComponentProps & MotionParams> = props => {\n 'use no memo';\n\n const {\n children,\n imperativeRef,\n onMotionFinish: onMotionFinishProp,\n onMotionStart: onMotionStartProp,\n onMotionCancel: onMotionCancelProp,\n replayKey,\n ..._rest\n } = props;\n const params = _rest as Exclude<typeof props, MotionComponentProps>;\n const [child, childRef] = useChildElement(children);\n\n const handleRef = useMotionImperativeRef(imperativeRef);\n const isInitialRender = React.useRef(true);\n const skipMotions = useMotionBehaviourContext() === 'skip';\n const optionsRef = React.useRef<{ skipMotions: boolean; params: MotionParams }>({\n skipMotions,\n params,\n });\n\n const animateAtoms = useAnimateAtoms();\n const isReducedMotion = useIsReducedMotion();\n\n const onMotionStart = useEventCallback(() => {\n onMotionStartProp?.(null);\n });\n\n const onMotionFinish = useEventCallback(() => {\n onMotionFinishProp?.(null);\n });\n\n const onMotionCancel = useEventCallback(() => {\n onMotionCancelProp?.(null);\n });\n\n // Stable callback (all deps are refs or useEventCallback) that activates a handle for a new playback cycle.\n //\n // TODO: consider moving the cancel+play+rewire sequence into a handle.replay() method on AnimationHandle,\n // keeping pure animation sequencing on the handle and React callbacks here in the component.\n const activateAnimationHandle = React.useCallback(\n (handle: AnimationHandle) => {\n onMotionStart();\n handle.setMotionEndCallbacks(onMotionFinish, onMotionCancel);\n if (optionsRef.current.skipMotions) {\n handle.finish();\n }\n },\n [onMotionStart, onMotionFinish, onMotionCancel],\n );\n\n useIsomorphicLayoutEffect(() => {\n // Heads up!\n // We store the params in a ref to avoid re-rendering the component when the params change.\n optionsRef.current = { skipMotions, params };\n });\n\n useIsomorphicLayoutEffect(() => {\n const element = childRef.current;\n\n if (element) {\n const atoms = typeof value === 'function' ? value({ element, ...optionsRef.current.params }) : value;\n\n const handle = animateAtoms(element, atoms, { isReducedMotion: isReducedMotion() });\n handleRef.current = handle;\n activateAnimationHandle(handle);\n\n return () => {\n handle.cancel();\n };\n }\n }, [animateAtoms, childRef, handleRef, isReducedMotion, activateAnimationHandle]);\n\n // Skips initial mount; on replayKey changes, reuses existing Animation objects via cancel+play\n // rather than recreating them, preserving DOM continuity.\n useIsomorphicLayoutEffect(() => {\n if (isInitialRender.current) {\n return;\n }\n\n const handle = handleRef.current;\n if (handle) {\n handle.cancel();\n handle.play();\n activateAnimationHandle(handle);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- replayKey is intentionally the only trigger; other deps are stable refs/callbacks\n }, [replayKey]);\n\n useIsomorphicLayoutEffect(() => {\n isInitialRender.current = false;\n\n return () => {\n isInitialRender.current = true;\n };\n }, []);\n\n return child;\n };\n\n return Object.assign(Atom, {\n // Heads up!\n // Always normalize it to a function to simplify types\n [MOTION_DEFINITION]: typeof value === 'function' ? value : () => value,\n });\n}\n"],"names":["MOTION_DEFINITION","createMotionComponent","Symbol","value","Atom","props","children","imperativeRef","onMotionFinish","onMotionFinishProp","onMotionStart","onMotionStartProp","onMotionCancel","onMotionCancelProp","replayKey","_rest","params","child","childRef","useChildElement","handleRef","useMotionImperativeRef","isInitialRender","React","useRef","skipMotions","useMotionBehaviourContext","optionsRef","animateAtoms","useAnimateAtoms","isReducedMotion","useIsReducedMotion","useEventCallback","activateAnimationHandle","useCallback","handle","setMotionEndCallbacks","current","finish","useIsomorphicLayoutEffect","element","atoms","cancel","play","Object","assign"],"mappings":"AAAA;;;;;;;;;;;;IAkBaA,iBAAiB;eAAjBA;;IAyEGC,qBAAqB;eAArBA;;;;gCAxF4C;iEACrC;iCAES;wCACO;oCACJ;iCACH;wCAEU;AAOnC,MAAMD,oBAAoBE,OAAO;AAyEjC,SAASD,sBACdE,KAA6D;IAE7D,MAAMC,OAAsDC,CAAAA;QAC1D;QAEA,MAAM,EACJC,QAAQ,EACRC,aAAa,EACbC,gBAAgBC,kBAAkB,EAClCC,eAAeC,iBAAiB,EAChCC,gBAAgBC,kBAAkB,EAClCC,SAAS,EACT,GAAGC,OACJ,GAAGV;QACJ,MAAMW,SAASD;QACf,MAAM,CAACE,OAAOC,SAAS,GAAGC,IAAAA,gCAAe,EAACb;QAE1C,MAAMc,YAAYC,IAAAA,8CAAsB,EAACd;QACzC,MAAMe,kBAAkBC,OAAMC,MAAM,CAAC;QACrC,MAAMC,cAAcC,IAAAA,iDAAyB,QAAO;QACpD,MAAMC,aAAaJ,OAAMC,MAAM,CAAiD;YAC9EC;YACAT;QACF;QAEA,MAAMY,eAAeC,IAAAA,gCAAe;QACpC,MAAMC,kBAAkBC,IAAAA,sCAAkB;QAE1C,MAAMrB,gBAAgBsB,IAAAA,gCAAgB,EAAC;YACrCrB,8BAAAA,wCAAAA,kBAAoB;QACtB;QAEA,MAAMH,iBAAiBwB,IAAAA,gCAAgB,EAAC;YACtCvB,+BAAAA,yCAAAA,mBAAqB;QACvB;QAEA,MAAMG,iBAAiBoB,IAAAA,gCAAgB,EAAC;YACtCnB,+BAAAA,yCAAAA,mBAAqB;QACvB;QAEA,4GAA4G;QAC5G,EAAE;QACF,0GAA0G;QAC1G,6FAA6F;QAC7F,MAAMoB,0BAA0BV,OAAMW,WAAW,CAC/C,CAACC;YACCzB;YACAyB,OAAOC,qBAAqB,CAAC5B,gBAAgBI;YAC7C,IAAIe,WAAWU,OAAO,CAACZ,WAAW,EAAE;gBAClCU,OAAOG,MAAM;YACf;QACF,GACA;YAAC5B;YAAeF;YAAgBI;SAAe;QAGjD2B,IAAAA,yCAAyB,EAAC;YACxB,YAAY;YACZ,2FAA2F;YAC3FZ,WAAWU,OAAO,GAAG;gBAAEZ;gBAAaT;YAAO;QAC7C;QAEAuB,IAAAA,yCAAyB,EAAC;YACxB,MAAMC,UAAUtB,SAASmB,OAAO;YAEhC,IAAIG,SAAS;gBACX,MAAMC,QAAQ,OAAOtC,UAAU,aAAaA,MAAM;oBAAEqC;oBAAS,GAAGb,WAAWU,OAAO,CAACrB,MAAM;gBAAC,KAAKb;gBAE/F,MAAMgC,SAASP,aAAaY,SAASC,OAAO;oBAAEX,iBAAiBA;gBAAkB;gBACjFV,UAAUiB,OAAO,GAAGF;gBACpBF,wBAAwBE;gBAExB,OAAO;oBACLA,OAAOO,MAAM;gBACf;YACF;QACF,GAAG;YAACd;YAAcV;YAAUE;YAAWU;YAAiBG;SAAwB;QAEhF,+FAA+F;QAC/F,0DAA0D;QAC1DM,IAAAA,yCAAyB,EAAC;YACxB,IAAIjB,gBAAgBe,OAAO,EAAE;gBAC3B;YACF;YAEA,MAAMF,SAASf,UAAUiB,OAAO;YAChC,IAAIF,QAAQ;gBACVA,OAAOO,MAAM;gBACbP,OAAOQ,IAAI;gBACXV,wBAAwBE;YAC1B;QACA,4IAA4I;QAC9I,GAAG;YAACrB;SAAU;QAEdyB,IAAAA,yCAAyB,EAAC;YACxBjB,gBAAgBe,OAAO,GAAG;YAE1B,OAAO;gBACLf,gBAAgBe,OAAO,GAAG;YAC5B;QACF,GAAG,EAAE;QAEL,OAAOpB;IACT;IAEA,OAAO2B,OAAOC,MAAM,CAACzC,MAAM;QACzB,YAAY;QACZ,sDAAsD;QACtD,CAACJ,kBAAkB,EAAE,OAAOG,UAAU,aAAaA,QAAQ,IAAMA;IACnE;AACF"}
@@ -28,6 +28,7 @@ function useMountedState(visible = false, unmountOnExit = false) {
28
28
  mountedRef.current = visible;
29
29
  }
30
30
  });
31
+ // eslint-disable-next-line react-hooks/refs
31
32
  return [
32
33
  visible || mountedRef.current,
33
34
  setMounted
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks/useMountedState.ts"],"sourcesContent":["'use client';\n\nimport { useForceUpdate } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\n/**\n * This hook manages the mounted state of a component, based on the \"visible\" and \"unmountOnExit\" props.\n * It simulates the behavior of getDerivedStateFromProps(), which is not available in functional components.\n */\nexport function useMountedState(\n visible: boolean = false,\n unmountOnExit: boolean = false,\n): [boolean, (value: boolean) => void] {\n const mountedRef = React.useRef<boolean>(unmountOnExit ? visible : true);\n const forceUpdate = useForceUpdate();\n\n const setMounted = React.useCallback(\n (newValue: boolean) => {\n if (mountedRef.current !== newValue) {\n mountedRef.current = newValue;\n forceUpdate();\n }\n },\n [forceUpdate],\n );\n\n React.useEffect(() => {\n if (visible) {\n mountedRef.current = visible;\n }\n });\n\n return [visible || mountedRef.current, setMounted];\n}\n"],"names":["useMountedState","visible","unmountOnExit","mountedRef","React","useRef","forceUpdate","useForceUpdate","setMounted","useCallback","newValue","current","useEffect"],"mappings":"AAAA;;;;;+BASgBA;;;eAAAA;;;;gCAPe;iEACR;AAMhB,SAASA,gBACdC,UAAmB,KAAK,EACxBC,gBAAyB,KAAK;IAE9B,MAAMC,aAAaC,OAAMC,MAAM,CAAUH,gBAAgBD,UAAU;IACnE,MAAMK,cAAcC,IAAAA,8BAAc;IAElC,MAAMC,aAAaJ,OAAMK,WAAW,CAClC,CAACC;QACC,IAAIP,WAAWQ,OAAO,KAAKD,UAAU;YACnCP,WAAWQ,OAAO,GAAGD;YACrBJ;QACF;IACF,GACA;QAACA;KAAY;IAGfF,OAAMQ,SAAS,CAAC;QACd,IAAIX,SAAS;YACXE,WAAWQ,OAAO,GAAGV;QACvB;IACF;IAEA,OAAO;QAACA,WAAWE,WAAWQ,OAAO;QAAEH;KAAW;AACpD"}
1
+ {"version":3,"sources":["../src/hooks/useMountedState.ts"],"sourcesContent":["'use client';\n\nimport { useForceUpdate } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\n/**\n * This hook manages the mounted state of a component, based on the \"visible\" and \"unmountOnExit\" props.\n * It simulates the behavior of getDerivedStateFromProps(), which is not available in functional components.\n */\nexport function useMountedState(\n visible: boolean = false,\n unmountOnExit: boolean = false,\n): [boolean, (value: boolean) => void] {\n const mountedRef = React.useRef<boolean>(unmountOnExit ? visible : true);\n const forceUpdate = useForceUpdate();\n\n const setMounted = React.useCallback(\n (newValue: boolean) => {\n if (mountedRef.current !== newValue) {\n mountedRef.current = newValue;\n forceUpdate();\n }\n },\n [forceUpdate],\n );\n\n React.useEffect(() => {\n if (visible) {\n mountedRef.current = visible;\n }\n });\n\n // eslint-disable-next-line react-hooks/refs\n return [visible || mountedRef.current, setMounted];\n}\n"],"names":["useMountedState","visible","unmountOnExit","mountedRef","React","useRef","forceUpdate","useForceUpdate","setMounted","useCallback","newValue","current","useEffect"],"mappings":"AAAA;;;;;+BASgBA;;;eAAAA;;;;gCAPe;iEACR;AAMhB,SAASA,gBACdC,UAAmB,KAAK,EACxBC,gBAAyB,KAAK;IAE9B,MAAMC,aAAaC,OAAMC,MAAM,CAAUH,gBAAgBD,UAAU;IACnE,MAAMK,cAAcC,IAAAA,8BAAc;IAElC,MAAMC,aAAaJ,OAAMK,WAAW,CAClC,CAACC;QACC,IAAIP,WAAWQ,OAAO,KAAKD,UAAU;YACnCP,WAAWQ,OAAO,GAAGD;YACrBJ;QACF;IACF,GACA;QAACA;KAAY;IAGfF,OAAMQ,SAAS,CAAC;QACd,IAAIX,SAAS;YACXE,WAAWQ,OAAO,GAAGV;QACvB;IACF;IAEA,4CAA4C;IAC5C,OAAO;QAACA,WAAWE,WAAWQ,OAAO;QAAEH;KAAW;AACpD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluentui/react-motion",
3
- "version": "9.15.0",
3
+ "version": "9.16.0",
4
4
  "description": "A package with utilities & motion definitions using Web Animations API",
5
5
  "main": "lib-commonjs/index.js",
6
6
  "module": "lib/index.js",
@@ -19,7 +19,7 @@
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
21
  "@fluentui/react-shared-contexts": "^9.26.2",
22
- "@fluentui/react-utilities": "^9.26.3",
22
+ "@fluentui/react-utilities": "^9.26.4",
23
23
  "@swc/helpers": "^0.5.1"
24
24
  },
25
25
  "peerDependencies": {