Skip to content

Commit

Permalink
refactor(motion): simplify variant creation, starting with Collapse (#…
Browse files Browse the repository at this point in the history
…32939)

Co-authored-by: Oleksandr Fediashov <[email protected]>
  • Loading branch information
robertpenner and layershifter authored Oct 9, 2024
1 parent 348f676 commit 82c0e84
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: export MotionParam type",
"packageName": "@fluentui/react-motion",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "refactor: simplify motion component variant creation",
"packageName": "@fluentui/react-motion-components-preview",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@
```ts

import type { MotionParam } from '@fluentui/react-motion';
import { PresenceComponent } from '@fluentui/react-motion';
import type { PresenceMotionFn } from '@fluentui/react-motion';

// @public
export const Collapse: PresenceComponent< {
animateOpacity?: boolean | undefined;
}>;
export const Collapse: PresenceComponent<CollapseRuntimeParams>;

// @public (undocumented)
export const CollapseExaggerated: PresenceComponent< {
animateOpacity?: boolean | undefined;
}>;
export const CollapseExaggerated: PresenceComponent<CollapseRuntimeParams>;

// @public (undocumented)
export const CollapseSnappy: PresenceComponent< {
animateOpacity?: boolean | undefined;
}>;
export const CollapseSnappy: PresenceComponent<CollapseRuntimeParams>;

// @public
export const createCollapsePresence: PresenceMotionFnCreator<CollapseVariantParams, CollapseRuntimeParams>;

// @public
export const Fade: PresenceComponent< {}>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,67 @@
import {
motionTokens,
type PresenceMotionFn,
createPresenceComponent,
createPresenceComponentVariant,
} from '@fluentui/react-motion';
import { motionTokens, createPresenceComponent } from '@fluentui/react-motion';
import type { PresenceMotionFnCreator } from '../../types';

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 (collapse). 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<CollapseVariantParams, CollapseRuntimeParams> =
({
enterDuration = motionTokens.durationNormal,
enterEasing = motionTokens.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: motionTokens.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: motionTokens.durationSlower }),
);
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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<string, MotionParam> = {},
MotionRuntimeParams extends Record<string, MotionParam> = {},
> = (variantParams?: MotionVariantParams) => PresenceMotionFn<MotionRuntimeParams>;
Original file line number Diff line number Diff line change
@@ -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 }) => (
<CustomCollapseVariant animateOpacity={false} unmountOnExit visible={visible}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import {
createPresenceComponentVariant,
createPresenceComponent,
Field,
makeStyles,
mergeClasses,
Expand All @@ -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';

Expand Down Expand Up @@ -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 = () => (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export type MotionImperativeRef = {
setPlayState: (state: 'running' | 'paused') => void;
};

// @public
export type MotionParam = boolean | number | string;

// @public (undocumented)
export const motionTokens: {
curveAccelerateMax: "cubic-bezier(0.9,0.1,1,0.2)";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type {
PresenceMotionFn,
PresenceDirection,
MotionImperativeRef,
MotionParam,
} from './types';

export { MotionBehaviourProvider } from './contexts/MotionBehaviourContext';
2 changes: 0 additions & 2 deletions packages/react-components/react-motion/library/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ export type PresenceDirection = 'enter' | 'exit';
export type PresenceMotion = Record<PresenceDirection, AtomMotion | AtomMotion[]>;

/**
* @internal
*
* A motion param should be a primitive value that can be serialized to JSON and could be potentially used a plain
* dependency for React hooks.
*/
Expand Down

0 comments on commit 82c0e84

Please sign in to comment.