Skip to content

Commit

Permalink
refactor(motion): simplify variant creation w/functional approach, st…
Browse files Browse the repository at this point in the history
…arting with Collapse
  • Loading branch information
robertpenner committed Sep 27, 2024
1 parent 2137714 commit 915f1e2
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,49 +1,69 @@
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;

/** Time (ms) for the exit transition (collapse). Defaults to the `durationNormal` value (200 ms). */
exitDuration?: number;

/** Easing curve for the enter transition (expand). Defaults to the `easeEaseMax` value. */
enterEasing?: string;

/** Easing curve for the exit transition (expand). Defaults to the `easeEaseMax` value. */
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 = durationNormal,
exitDuration = durationNormal,
enterEasing = curveEasyEaseMax,
exitEasing = curveEasyEaseMax,
} = {}) =>
({ 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 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: { enterDuration, enterEasing, keyframes: enterKeyframes },
exit: { exitDuration, 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, exitDuration: 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, exitDuration: durationNormal }),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { MotionParam, PresenceMotionFn } from '@fluentui/react-motion';

// 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
Expand Up @@ -19,6 +19,7 @@ export type {
PresenceMotionFn,
PresenceDirection,
MotionImperativeRef,
MotionParam,
} from './types';

export { MotionBehaviourProvider } from './contexts/MotionBehaviourContext';

0 comments on commit 915f1e2

Please sign in to comment.