Skip to content

Commit

Permalink
feat(Collapse): create CollapseDelayed variant (#33069)
Browse files Browse the repository at this point in the history
  • Loading branch information
robertpenner authored Oct 21, 2024
1 parent 54a28ec commit ed7c93c
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add CollapseDelayed motion component variant",
"packageName": "@fluentui/react-motion-components-preview",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ import type { PresenceMotionFn } from '@fluentui/react-motion';
// @public
export const Collapse: PresenceComponent<CollapseRuntimeParams>;

// @public (undocumented)
export const CollapseDelayed: PresenceComponent<CollapseRuntimeParams>;

// @public (undocumented)
export const CollapseExaggerated: PresenceComponent<CollapseRuntimeParams>;

// @public (undocumented)
export const CollapseSnappy: PresenceComponent<CollapseRuntimeParams>;

// @public
export const createCollapseDelayedPresence: PresenceMotionFnCreator<CollapseDelayedVariantParams, CollapseRuntimeParams>;

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,124 +1,88 @@
import { motionTokens, createPresenceComponent, AtomMotion } from '@fluentui/react-motion';
import type { PresenceMotionFnCreator } from '../../types';
import type { CollapseDelayedVariantParams, CollapseRuntimeParams, CollapseVariantParams } from './collapse-types';
import {
sizeEnterAtom,
whitespaceEnterAtom,
opacityEnterAtom,
opacityExitAtom,
sizeExitAtom,
whitespaceExitAtom,
} from './collapse-atoms';

type CollapseOrientation = 'horizontal' | 'vertical';

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;

/** The orientation of the size animation. Defaults to `'vertical'` to expand/collapse the height. */
orientation?: CollapseOrientation;
};

/** Define a presence motion for collapse/expand */
export const createCollapsePresence: PresenceMotionFnCreator<CollapseVariantParams, CollapseRuntimeParams> =
/** Define a presence motion for collapse/expand that can stagger the size and opacity motions by a given delay. */
export const createCollapseDelayedPresence: PresenceMotionFnCreator<
CollapseDelayedVariantParams,
CollapseRuntimeParams
> =
({
enterDuration = motionTokens.durationNormal,
// enter
enterSizeDuration = motionTokens.durationNormal,
enterOpacityDuration = enterSizeDuration, // in sync with size duration by default
enterEasing = motionTokens.curveEasyEaseMax,
exitDuration = enterDuration,
enterDelay = 0,

// exit: durations and easing default to enter values for symmetry
exitSizeDuration = enterSizeDuration,
exitOpacityDuration = enterOpacityDuration,
exitEasing = enterEasing,
exitDelay = 0,
} = {}) =>
({ element, animateOpacity = true, orientation = 'vertical' }) => {
const fromOpacity = 0;
const toOpacity = 1;
const fromSize = '0'; // Could be a custom param in the future to start with partially expanded width or height
const measuredSize = orientation === 'horizontal' ? element.scrollWidth : element.scrollHeight;
const toSize = `${measuredSize}px`;
// use generic names for size and overflow, handling vertical or horizontal orientation
const sizeName = orientation === 'horizontal' ? 'maxWidth' : 'maxHeight';
const overflowName = orientation === 'horizontal' ? 'overflowX' : 'overflowY';

// Because a height of zero does not eliminate padding,
// we will create keyframes to animate it to zero.
// TODO: consider collapsing margin, perhaps as an option.
const collapsedWhiteSpace = {} as { [key: string]: string };
if (orientation === 'horizontal') {
collapsedWhiteSpace.paddingLeft = '0';
collapsedWhiteSpace.paddingRight = '0';
} else {
collapsedWhiteSpace.paddingTop = '0';
collapsedWhiteSpace.paddingBottom = '0';
}

// ----- ENTER -----
// The enter transition is an array of up to 3 motion atoms: size, whitespace and opacity.
const enterAtoms: AtomMotion[] = [
// Expand size (height or width)
{
keyframes: [
{
[sizeName]: fromSize,
[overflowName]: 'hidden',
},
{ [sizeName]: toSize, offset: 0.9999, [overflowName]: 'hidden' },
{ [sizeName]: 'unset', [overflowName]: 'unset' },
],
duration: enterDuration,
sizeEnterAtom({
orientation,
duration: enterSizeDuration,
easing: enterEasing,
},
// Expand whitespace (padding currently).
{
// Animate from zero values to the element's natural values (i.e. the missing other keyframe).
keyframes: [{ ...collapsedWhiteSpace, offset: 0 }],
duration: enterDuration,
element,
}),
whitespaceEnterAtom({
orientation,
duration: enterSizeDuration,
easing: enterEasing,
},
}),
];
// Fade in only if animateOpacity is true. Otherwise, leave opacity unaffected.
if (animateOpacity) {
enterAtoms.push({
keyframes: [{ opacity: fromOpacity }, { opacity: toOpacity }],
duration: enterDuration,
easing: enterEasing,
fill: 'both',
});
enterAtoms.push(
opacityEnterAtom({
duration: enterOpacityDuration,
easing: enterEasing,
delay: enterDelay,
}),
);
}

// ----- EXIT -----
// The exit transition is an array of up to 3 motion atoms: opacity, size and whitespace.
const exitAtoms: AtomMotion[] = [];
// Fade out only if animateOpacity is false. Otherwise, leave opacity unaffected.
// Fade out only if animateOpacity is true. Otherwise, leave opacity unaffected.
if (animateOpacity) {
exitAtoms.push({
keyframes: [{ opacity: toOpacity }, { opacity: fromOpacity }],
duration: exitDuration,
easing: exitEasing,
});
exitAtoms.push(
opacityExitAtom({
duration: exitOpacityDuration,
easing: exitEasing,
}),
);
}
exitAtoms.push(
// Collapse size (height or width)
{
keyframes: [
{ [sizeName]: toSize, [overflowName]: 'hidden' },
{ [sizeName]: fromSize, [overflowName]: 'hidden' },
],
duration: exitDuration,
sizeExitAtom({
orientation,
duration: exitSizeDuration,
easing: exitEasing,
fill: 'both',
},
element,
delay: exitDelay,
}),
);
exitAtoms.push(
// Collapse whitespace (padding currently).
{
// Animate from the element's natural values (i.e. the missing other keyframe) to zero values.
keyframes: [{ ...collapsedWhiteSpace, offset: 1 }],
duration: exitDuration,
whitespaceExitAtom({
orientation,
duration: exitSizeDuration,
easing: exitEasing,
fill: 'forwards',
},
delay: exitDelay,
}),
);

return {
Expand All @@ -127,6 +91,22 @@ export const createCollapsePresence: PresenceMotionFnCreator<CollapseVariantPara
};
};

/** Defines a presence motion for collapse/expand. */
export const createCollapsePresence: PresenceMotionFnCreator<CollapseVariantParams, CollapseRuntimeParams> = ({
enterDuration = motionTokens.durationNormal,
enterEasing = motionTokens.curveEasyEaseMax,
exitDuration = enterDuration,
exitEasing = enterEasing,
} = {}) =>
// Implement a regular collapse as a special case of the delayed collapse,
// where the delays are zero, and the size and opacity durations are equal.
createCollapseDelayedPresence({
enterSizeDuration: enterDuration,
enterEasing,
exitSizeDuration: exitDuration,
exitEasing,
});

/** A React component that applies collapse/expand transitions to its children. */
export const Collapse = createPresenceComponent(createCollapsePresence());

Expand All @@ -137,3 +117,13 @@ export const CollapseSnappy = createPresenceComponent(
export const CollapseExaggerated = createPresenceComponent(
createCollapsePresence({ enterDuration: motionTokens.durationSlower }),
);

export const CollapseDelayed = createPresenceComponent(
createCollapseDelayedPresence({
enterSizeDuration: motionTokens.durationNormal,
enterOpacityDuration: motionTokens.durationSlower,
enterDelay: motionTokens.durationNormal,
exitDelay: motionTokens.durationSlower,
enterEasing: motionTokens.curveEasyEase,
}),
);
Loading

0 comments on commit ed7c93c

Please sign in to comment.