diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts b/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts index 498884eb718f3..160770410a3c6 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts @@ -1,49 +1,65 @@ -import { - motionTokens, - type PresenceMotionFn, - createPresenceComponent, - createPresenceComponentVariant, -} from '@fluentui/react-motion'; +import { motionTokens, createPresenceComponent } from '@fluentui/react-motion'; +import type { PresenceMotionFnCreator } from '../../types'; + +const { durationNormal, durationSlower, durationFast, curveEasyEaseMax } = motionTokens; + +type CollapseVariantParams = { + /** Time (ms) for the enter transition (expand). Defaults to the `durationNormal` value (200 ms). */ + enterDuration?: number; + + /** Easing curve for the enter transition (expand). Defaults to the `easeEaseMax` value. */ + enterEasing?: string; + + /** Time (ms) for the exit transition (collapse). Defaults to the `enterDuration` param for symmetry. */ + exitDuration?: number; + + /** Easing curve for the exit transition (expand). Defaults to the `enterEasing` param for symmetry. */ + exitEasing?: string; +}; + +type CollapseRuntimeParams = { + /** Whether to animate the opacity. Defaults to `true`. */ + animateOpacity?: boolean; +}; /** Define a presence motion for collapse/expand */ -const collapseMotion: PresenceMotionFn<{ animateOpacity?: boolean }> = ({ element, animateOpacity = true }) => { - const fromOpacity = animateOpacity ? 0 : 1; - const toOpacity = 1; - const fromHeight = '0'; // Could be a custom param in the future: start partially expanded - const toHeight = `${element.scrollHeight}px`; - const overflow = 'hidden'; - - const duration = motionTokens.durationNormal; - const easing = motionTokens.curveEasyEaseMax; - - const enterKeyframes = [ - { opacity: fromOpacity, maxHeight: fromHeight, overflow }, - // Transition to the height of the content, at 99.99% of the duration. - { opacity: toOpacity, maxHeight: toHeight, offset: 0.9999, overflow }, - // On completion, remove the maxHeight because the content might need to expand later. - // This extra keyframe is simpler than firing a callback on completion. - { opacity: toOpacity, maxHeight: 'unset', overflow }, - ]; - - const exitKeyframes = [ - { opacity: toOpacity, maxHeight: toHeight, overflow }, - { opacity: fromOpacity, maxHeight: fromHeight, overflow }, - ]; - - return { - enter: { duration, easing, keyframes: enterKeyframes }, - exit: { duration, easing, keyframes: exitKeyframes }, +export const createCollapsePresence: PresenceMotionFnCreator = + ({ + enterDuration = durationNormal, + enterEasing = curveEasyEaseMax, + exitDuration = enterDuration, + exitEasing = enterEasing, + } = {}) => + ({ element, animateOpacity = true }) => { + const fromOpacity = animateOpacity ? 0 : 1; + const toOpacity = 1; + const fromHeight = '0'; // Could be a custom param in the future to start partially expanded + const toHeight = `${element.scrollHeight}px`; + const overflow = 'hidden'; + + const enterKeyframes = [ + { opacity: fromOpacity, maxHeight: fromHeight, overflow }, + // Transition to the height of the content, at 99.99% of the duration. + { opacity: toOpacity, maxHeight: toHeight, offset: 0.9999, overflow }, + // On completion, remove the maxHeight because the content might need to expand later. + // This extra keyframe is simpler than firing a callback on completion. + { opacity: toOpacity, maxHeight: 'unset', overflow }, + ]; + + const exitKeyframes = [ + { opacity: toOpacity, maxHeight: toHeight, overflow }, + { opacity: fromOpacity, maxHeight: fromHeight, overflow }, + ]; + + return { + enter: { duration: enterDuration, easing: enterEasing, keyframes: enterKeyframes }, + exit: { duration: exitDuration, easing: exitEasing, keyframes: exitKeyframes }, + }; }; -}; /** A React component that applies collapse/expand transitions to its children. */ -export const Collapse = createPresenceComponent(collapseMotion); +export const Collapse = createPresenceComponent(createCollapsePresence()); -export const CollapseSnappy = createPresenceComponentVariant(Collapse, { - all: { duration: motionTokens.durationUltraFast }, -}); +export const CollapseSnappy = createPresenceComponent(createCollapsePresence({ enterDuration: durationFast })); -export const CollapseExaggerated = createPresenceComponentVariant(Collapse, { - enter: { duration: motionTokens.durationSlow, easing: motionTokens.curveEasyEaseMax }, - exit: { duration: motionTokens.durationNormal, easing: motionTokens.curveEasyEaseMax }, -}); +export const CollapseExaggerated = createPresenceComponent(createCollapsePresence({ enterDuration: durationSlower })); diff --git a/packages/react-components/react-motion-components-preview/library/src/index.ts b/packages/react-components/react-motion-components-preview/library/src/index.ts index 2548c42df0810..ee70098ff0399 100644 --- a/packages/react-components/react-motion-components-preview/library/src/index.ts +++ b/packages/react-components/react-motion-components-preview/library/src/index.ts @@ -1,3 +1,3 @@ -export { Collapse, CollapseSnappy, CollapseExaggerated } from './components/Collapse'; +export { Collapse, CollapseSnappy, CollapseExaggerated, createCollapsePresence } from './components/Collapse'; export { Fade, FadeSnappy, FadeExaggerated } from './components/Fade'; export { Scale, ScaleSnappy, ScaleExaggerated } from './components/Scale'; diff --git a/packages/react-components/react-motion-components-preview/library/src/types.ts b/packages/react-components/react-motion-components-preview/library/src/types.ts new file mode 100644 index 0000000000000..36ba309864c9a --- /dev/null +++ b/packages/react-components/react-motion-components-preview/library/src/types.ts @@ -0,0 +1,17 @@ +import type { MotionParam, PresenceMotionFn } from '@fluentui/react-motion'; + +/** + * This is a factory function that generates a motion function, which has variant params bound into it. + * The generated motion function accepts other runtime params that aren't locked into the variant, but supplied at runtime. + * This separation allows the variant to be defined once and reused with different runtime params which may be orthogonal to the variant params. + * For example, a variant may define the duration and easing of a transition, which are fixed for all instances of the variant, + * while the runtime params may give access to the target element, which is different for each instance. + * + * The generated motion function is also framework-independent, i.e. non-React. + * It can be turned into a React component using `createPresenceComponent`. + */ +// TODO: move to @fluentui/react-motion when stable +export type PresenceMotionFnCreator< + MotionVariantParams extends Record = {}, + MotionRuntimeParams extends Record = {}, +> = (variantParams?: MotionVariantParams) => PresenceMotionFn; diff --git a/packages/react-components/react-motion-components-preview/stories/src/Collapse/CollapseCustomization.stories.md b/packages/react-components/react-motion-components-preview/stories/src/Collapse/CollapseCustomization.stories.md index 24460334c98d8..d155181a9f6fb 100644 --- a/packages/react-components/react-motion-components-preview/stories/src/Collapse/CollapseCustomization.stories.md +++ b/packages/react-components/react-motion-components-preview/stories/src/Collapse/CollapseCustomization.stories.md @@ -1,14 +1,19 @@ -- `duration` and `easing` can be customized for each transition separately using `createPresenceComponentVariant()`. - The predefined fade transition can be disabled by setting `animateOpacity` to `false`. +- The `unmountOnExit` prop can be used to unmount the content when its `exit` transition is finished. +- A collapse variant can be created with the factory function `createCollapsePresence()`, then converting the result to a React component using `createPresenceComponent()`: ```tsx -import { motionTokens, createPresenceComponentVariant } from '@fluentui/react-components'; -import { Collapse } from '@fluentui/react-motion-components-preview'; +import { motionTokens, createPresenceComponent } from '@fluentui/react-components'; +import { createCollapsePresence } from '@fluentui/react-motion-components-preview'; -const CustomCollapseVariant = createPresenceComponentVariant(Collapse, { - enter: { duration: motionTokens.durationSlow, easing: motionTokens.curveEasyEaseMax }, - exit: { duration: motionTokens.durationNormal, easing: motionTokens.curveEasyEaseMax }, -}); +const CustomCollapseVariant = createPresenceComponent( + createCollapsePresence({ + enterDuration: motionTokens.durationSlow, + enterEasing: motionTokens.curveEasyEaseMax, + exitDuration: motionTokens.durationNormal, + exitEasing: motionTokens.curveEasyEaseMax, + }), +); const CustomCollapse = ({ visible }) => ( diff --git a/packages/react-components/react-motion-components-preview/stories/src/Collapse/CollapseCustomization.stories.tsx b/packages/react-components/react-motion-components-preview/stories/src/Collapse/CollapseCustomization.stories.tsx index 1ef84f9680079..17c6056a7e574 100644 --- a/packages/react-components/react-motion-components-preview/stories/src/Collapse/CollapseCustomization.stories.tsx +++ b/packages/react-components/react-motion-components-preview/stories/src/Collapse/CollapseCustomization.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { - createPresenceComponentVariant, + createPresenceComponent, Field, makeStyles, mergeClasses, @@ -10,7 +10,7 @@ import { Switch, tokens, } from '@fluentui/react-components'; -import { Collapse } from '@fluentui/react-motion-components-preview'; +import { createCollapsePresence } from '@fluentui/react-motion-components-preview'; import description from './CollapseCustomization.stories.md'; @@ -54,10 +54,14 @@ const useClasses = makeStyles({ }, }); -const CustomCollapseVariant = createPresenceComponentVariant(Collapse, { - enter: { duration: motionTokens.durationSlow, easing: motionTokens.curveEasyEaseMax }, - exit: { duration: motionTokens.durationNormal, easing: motionTokens.curveEasyEaseMax }, -}); +const CustomCollapseVariant = createPresenceComponent( + createCollapsePresence({ + enterDuration: motionTokens.durationSlow, + enterEasing: motionTokens.curveEasyEaseMax, + exitDuration: motionTokens.durationNormal, + exitEasing: motionTokens.curveEasyEaseMax, + }), +); const LoremIpsum = () => ( <> diff --git a/packages/react-components/react-motion/library/src/index.ts b/packages/react-components/react-motion/library/src/index.ts index fe49b7011d977..74495e48de51a 100644 --- a/packages/react-components/react-motion/library/src/index.ts +++ b/packages/react-components/react-motion/library/src/index.ts @@ -19,6 +19,7 @@ export type { PresenceMotionFn, PresenceDirection, MotionImperativeRef, + MotionParam, } from './types'; export { MotionBehaviourProvider } from './contexts/MotionBehaviourContext';