Skip to content

Commit

Permalink
feat(scale): add scaleAtom and refactor Scale component for enhanced …
Browse files Browse the repository at this point in the history
…animations
  • Loading branch information
pixel-perfectionist committed Nov 12, 2024
1 parent 639871a commit 75ec58b
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,16 @@ export const opacityAtom = ({
keyframes: [{ opacity: fromOpacity }, { opacity: toOpacity }],
...props,
});

export const scaleAtom = ({
fromScale = 0.9,
toScale = 1,
...props
}: { fromScale?: number; toScale?: number } & MotionAtomProps): AtomMotion =>
motionAtom({
keyframes: [
{ transform: `scale3d(${fromScale}, ${fromScale}, 1)`, visibility: 'visible' },
{ transform: `scale3d(${toScale}, ${toScale}, 1)` },
],
...props,
});
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { opacityAtom } from './Atoms';
export { opacityAtom, scaleAtom } from './Atoms';
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import { expectPresenceMotionFunction, expectPresenceMotionArray } from '../../testing/testUtils';
import { motionTokens, PresenceComponent } from '@fluentui/react-motion';
import { expectPresenceMotionFunction, getMotionFunction } from '../../testing/testUtils';
import { Collapse } from './Collapse';

function expectPresenceMotionArray(component: PresenceComponent) {
const presenceMotionFn = getMotionFunction(component);

// eslint-disable-next-line @nx/workspace-no-restricted-globals
expect(presenceMotionFn?.({ element: document.createElement('div') })).toMatchObject({
enter: expect.arrayContaining([
expect.objectContaining({
duration: expect.any(Number),
easing: expect.any(String),
keyframes: expect.any(Array),
}),
]),
exit: expect.arrayContaining([
expect.objectContaining({
duration: expect.any(Number),
easing: expect.any(String),
keyframes: expect.any(Array),
}),
expect.objectContaining({
duration: motionTokens.durationNormal,
easing: motionTokens.curveEasyEaseMax,
keyframes: expect.arrayContaining([
expect.objectContaining({ maxHeight: '0px' }),
expect.objectContaining({ maxHeight: '0' }),
]),
}),
expect.objectContaining({
duration: motionTokens.durationNormal,
easing: motionTokens.curveEasyEaseMax,
keyframes: expect.arrayContaining([expect.objectContaining({ paddingBottom: '0', paddingTop: '0' })]),
}),
]),
});
}

describe('Collapse', () => {
it('stores its motion definition as a static function', () => {
expectPresenceMotionFunction(Collapse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ describe('Fade motion component', () => {
// Testing fade out motion
rerender(<Fade visible={false}>{testElement}</Fade>);
expect(animateSpy).toHaveBeenCalledWith(
[{ opacity: 1 }, { opacity: 0 }],
expect.objectContaining({ duration: motionTokens.durationNormal, easing: motionTokens.curveEasyEase }),
[{ opacity: 0 }, { opacity: 1 }],
expect.objectContaining({
direction: 'reverse',
duration: motionTokens.durationNormal,
easing: motionTokens.curveEasyEase,
}),
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
import { expectPresenceMotionFunction, expectPresenceMotionObject } from '../../testing/testUtils';
import { PresenceComponent } from '@fluentui/react-motion';
import { expectPresenceMotionFunction, getMotionFunction } from '../../testing/testUtils';
import { Scale } from './Scale';

export function expectPresenceMotionObject(component: PresenceComponent) {
const presenceMotionFn = getMotionFunction(component);

// eslint-disable-next-line @nx/workspace-no-restricted-globals
expect(presenceMotionFn?.({ element: document.createElement('div') })).toMatchObject({
enter: expect.arrayContaining([
expect.objectContaining({
duration: expect.any(Number),
easing: expect.any(String),
keyframes: expect.any(Array),
}),
]),
exit: expect.arrayContaining([
expect.objectContaining({
duration: expect.any(Number),
easing: expect.any(String),
keyframes: expect.any(Array),
}),
]),
});
}

describe('Scale', () => {
it('stores its motion definition as a static function', () => {
expectPresenceMotionFunction(Scale);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,66 @@
import {
motionTokens,
PresenceMotionFn,
createPresenceComponent,
createPresenceComponentVariant,
} from '@fluentui/react-motion';
import { motionTokens, createPresenceComponent } from '@fluentui/react-motion';
import { PresenceMotionFnCreator } from '../../types';
import { opacityAtom, scaleAtom } from '../Atoms';
import { ScaleRuntimeParams_unstable, ScaleVariantParams_unstable } from './Scale.types';

/** Define a presence motion for scale in/out */
const scaleMotion: PresenceMotionFn<{ animateOpacity?: boolean }> = ({ animateOpacity = true }) => {
const fromOpacity = animateOpacity ? 0 : 1;
const toOpacity = 1;
const fromScale = 0.9; // Could be a custom param in the future
const toScale = 1;

const enterKeyframes = [
{ opacity: fromOpacity, transform: `scale3d(${fromScale}, ${fromScale}, 1)`, visibility: 'visible' },
{ opacity: toOpacity, transform: `scale3d(${toScale}, ${toScale}, 1)` },
];

const exitKeyframes = [
{ opacity: toOpacity, transform: `scale3d(${toScale}, ${toScale}, 1)` },
{ opacity: fromOpacity, transform: `scale3d(${fromScale}, ${fromScale}, 1)`, visibility: 'hidden' },
];

return {
enter: {
duration: motionTokens.durationGentle,
easing: motionTokens.curveDecelerateMax,
keyframes: enterKeyframes,
},
exit: { duration: motionTokens.durationNormal, easing: motionTokens.curveAccelerateMax, keyframes: exitKeyframes },
};
};
export const createScalePresence: PresenceMotionFnCreator<ScaleVariantParams_unstable, ScaleRuntimeParams_unstable> =
({
enterDuration = motionTokens.durationGentle,
enterEasing = motionTokens.curveEasyEaseMax,
exitDuration = motionTokens.durationNormal,
exitEasing = motionTokens.curveAccelerateMax,
} = {}) =>
({ animateOpacity = true }) => ({
enter: [
scaleAtom({
duration: enterDuration,
easing: enterEasing,
}),
...(animateOpacity
? [
opacityAtom({
duration: enterDuration,
easing: enterEasing,
}),
]
: []),
],
exit: [
scaleAtom({
duration: exitDuration,
easing: exitEasing,
direction: 'reverse',
}),
...(animateOpacity
? [
opacityAtom({
duration: exitDuration,
easing: exitEasing,
direction: 'reverse',
}),
]
: []),
],
});

/** A React component that applies scale in/out transitions to its children. */
export const Scale = createPresenceComponent(scaleMotion);
export const Scale = createPresenceComponent(createScalePresence());

export const ScaleSnappy = createPresenceComponentVariant(Scale, {
enter: { duration: motionTokens.durationNormal, easing: motionTokens.curveDecelerateMax },
exit: { duration: motionTokens.durationFast, easing: motionTokens.curveAccelerateMax },
});
export const ScaleSnappy = createPresenceComponent(
createScalePresence({
enterDuration: motionTokens.durationNormal,
enterEasing: motionTokens.curveDecelerateMax,
exitDuration: motionTokens.durationFast,
exitEasing: motionTokens.curveAccelerateMax,
}),
);

export const ScaleRelaxed = createPresenceComponentVariant(Scale, {
enter: { duration: motionTokens.durationSlow, easing: motionTokens.curveDecelerateMax },
exit: { duration: motionTokens.durationGentle, easing: motionTokens.curveAccelerateMax },
});
export const ScaleRelaxed = createPresenceComponent(
createScalePresence({
enterDuration: motionTokens.durationSlow,
enterEasing: motionTokens.curveDecelerateMax,
exitDuration: motionTokens.durationGentle,
exitEasing: motionTokens.curveAccelerateMax,
}),
);
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
export type ScaleRuntimeParams_unstable = {
/** Whether to animate the opacity. Defaults to `true`. */
animateOpacity?: boolean;
};

// eslint-disable-next-line @typescript-eslint/naming-convention
export type ScaleVariantParams_unstable = {
/** Time (ms) for the enter transition (expand). Defaults to the `durationNormal` value (200 ms). */
enterScaleDuration?: number;
/** Time (ms) for the enter transition. Defaults to the `durationNormal` value (200 ms). */
enterDuration?: number;

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

/** Time (ms) for the exit transition (collapse). Defaults to the `enterDuration` param for symmetry. */
exitScaleDuration?: number;
/** Time (ms) for the exit transition. Defaults to the `enterDuration` param for symmetry. */
exitDuration?: number;

/** Easing curve for the exit transition (collapse). Defaults to the `enterEasing` param for symmetry. */
exitScaleEasing?: string;
/** Easing curve for the exit transition. Defaults to the `enterEasing` param for symmetry. */
exitEasing?: string;
};

// eslint-disable-next-line @typescript-eslint/naming-convention
export type ScaleRuntimeParams_unstable = {
/** Whether to animate the opacity. Defaults to `true`. */
animateOpacity?: boolean;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { PresenceComponent, PresenceMotionFn } from '@fluentui/react-motion';

function getMotionFunction(component: PresenceComponent): PresenceMotionFn | null {
export function getMotionFunction(component: PresenceComponent): PresenceMotionFn | null {
const symbols = Object.getOwnPropertySymbols(component);

for (const symbol of symbols) {
Expand Down

0 comments on commit 75ec58b

Please sign in to comment.