Skip to content

Commit 61c6eaf

Browse files
feat(motion): add Blur presence motion component (#34839)
Co-authored-by: Oleksandr Fediashov <alexander.mcgarret@gmail.com>
1 parent a8368df commit 61c6eaf

29 files changed

+827
-77
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "feat(motion): add Fade presence motion component",
4+
"packageName": "@fluentui/react-motion-components-preview",
5+
"email": "robertpenner@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-motion-components-preview/library/etc/react-motion-components-preview.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
import { PresenceComponent } from '@fluentui/react-motion';
88

9+
// @public
10+
export const Blur: PresenceComponent<BlurParams>;
11+
912
// @public
1013
export const Collapse: PresenceComponent<CollapseParams>;
1114

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { motionTokens } from '@fluentui/react-motion';
2+
import { blurAtom } from './blur-atom';
3+
4+
describe('blurAtom', () => {
5+
it('creates enter keyframes with blur from radius to 0', () => {
6+
const atom = blurAtom({
7+
direction: 'enter',
8+
duration: 300,
9+
easing: motionTokens.curveEasyEase,
10+
fromRadius: '20px',
11+
});
12+
13+
expect(atom).toMatchObject({
14+
duration: 300,
15+
easing: motionTokens.curveEasyEase,
16+
keyframes: [{ filter: 'blur(20px)' }, { filter: 'blur(0px)' }],
17+
});
18+
});
19+
20+
it('creates exit keyframes with blur from 0 to radius', () => {
21+
const atom = blurAtom({
22+
direction: 'exit',
23+
duration: 250,
24+
easing: motionTokens.curveAccelerateMin,
25+
fromRadius: '15px',
26+
});
27+
28+
expect(atom).toMatchObject({
29+
duration: 250,
30+
easing: motionTokens.curveAccelerateMin,
31+
keyframes: [{ filter: 'blur(0px)' }, { filter: 'blur(15px)' }],
32+
});
33+
});
34+
35+
it('uses default fromRadius when not provided', () => {
36+
const atom = blurAtom({
37+
direction: 'enter',
38+
duration: 300,
39+
});
40+
41+
expect(atom.keyframes).toEqual([{ filter: 'blur(10px)' }, { filter: 'blur(0px)' }]);
42+
});
43+
44+
it('uses default easing when not provided', () => {
45+
const atom = blurAtom({
46+
direction: 'enter',
47+
duration: 300,
48+
fromRadius: '5px',
49+
});
50+
51+
expect(atom.easing).toBe(motionTokens.curveLinear);
52+
});
53+
54+
it('handles different CSS units for fromRadius', () => {
55+
const atomPx = blurAtom({
56+
direction: 'enter',
57+
duration: 300,
58+
fromRadius: '8px',
59+
});
60+
61+
const atomRem = blurAtom({
62+
direction: 'enter',
63+
duration: 300,
64+
fromRadius: '1rem',
65+
});
66+
67+
expect(atomPx.keyframes[0]).toEqual({ filter: 'blur(8px)' });
68+
expect(atomRem.keyframes[0]).toEqual({ filter: 'blur(1rem)' });
69+
});
70+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { AtomMotion, PresenceDirection, motionTokens } from '@fluentui/react-motion';
2+
3+
interface BlurAtomParams {
4+
direction: PresenceDirection;
5+
duration: number;
6+
easing?: string;
7+
fromRadius?: string;
8+
}
9+
10+
/**
11+
* Generates a motion atom object for a blur-in or blur-out.
12+
* @param direction - The functional direction of the motion: 'enter' or 'exit'.
13+
* @param duration - The duration of the motion in milliseconds.
14+
* @param easing - The easing curve for the motion. Defaults to `motionTokens.curveLinear`.
15+
* @param fromRadius - The blur radius value with units (e.g., '20px', '1rem'). Defaults to '20px'.
16+
* @returns A motion atom object with filter blur keyframes and the supplied duration and easing.
17+
*/
18+
export const blurAtom = ({
19+
direction,
20+
duration,
21+
easing = motionTokens.curveLinear,
22+
fromRadius = '10px',
23+
}: BlurAtomParams): AtomMotion => {
24+
const keyframes = [{ filter: `blur(${fromRadius})` }, { filter: 'blur(0px)' }];
25+
if (direction === 'exit') {
26+
keyframes.reverse();
27+
}
28+
return {
29+
keyframes,
30+
duration,
31+
easing,
32+
};
33+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expectPresenceMotionFunction, expectPresenceMotionArray } from '../../testing/testUtils';
2+
import { Blur } from './Blur';
3+
4+
describe('Blur', () => {
5+
it('stores its motion definition as a static function', () => {
6+
expectPresenceMotionFunction(Blur);
7+
});
8+
9+
it('generates a motion definition from the static function', () => {
10+
expectPresenceMotionArray(Blur);
11+
});
12+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { motionTokens, createPresenceComponent, PresenceMotionFn } from '@fluentui/react-motion';
2+
import { fadeAtom } from '../../atoms/fade-atom';
3+
import { blurAtom } from '../../atoms/blur-atom';
4+
import { BlurParams } from './blur-types';
5+
6+
/**
7+
* Define a presence motion for blur in/out
8+
*
9+
* @param fromRadius - The radius of pixels to blend into the blur. A length string, defaulting to '10px'.
10+
* @param duration - Time (ms) for the enter transition (blur-in). Defaults to the `durationSlow` value (300 ms).
11+
* @param easing - Easing curve for the enter transition (blur-in). Defaults to the `curveDecelerateMin` value.
12+
* @param exitDuration - Time (ms) for the exit transition (blur-out). Defaults to the enter duration.
13+
* @param exitEasing - Easing curve for the exit transition (blur-out). Defaults to the `curveAccelerateMin` value.
14+
* @param animateOpacity - Whether to animate the opacity. Defaults to `true`.
15+
*/
16+
const blurPresenceFn: PresenceMotionFn<BlurParams> = ({
17+
fromRadius = '10px',
18+
duration = motionTokens.durationSlow,
19+
easing = motionTokens.curveDecelerateMin,
20+
exitDuration = duration,
21+
exitEasing = motionTokens.curveAccelerateMin,
22+
animateOpacity = true,
23+
}) => {
24+
const enterAtoms = [blurAtom({ direction: 'enter', duration, easing, fromRadius })];
25+
const exitAtoms = [
26+
blurAtom({
27+
direction: 'exit',
28+
duration: exitDuration,
29+
easing: exitEasing,
30+
fromRadius,
31+
}),
32+
];
33+
34+
// Only add fade atoms if animateOpacity is true.
35+
if (animateOpacity) {
36+
enterAtoms.push(fadeAtom({ direction: 'enter', duration, easing }));
37+
exitAtoms.push(fadeAtom({ direction: 'exit', duration: exitDuration, easing: exitEasing }));
38+
}
39+
40+
return {
41+
enter: enterAtoms,
42+
exit: exitAtoms,
43+
};
44+
};
45+
46+
/** A React component that applies blur in/out transitions to its children. */
47+
export const Blur = createPresenceComponent(blurPresenceFn);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { PresenceDuration, PresenceEasing, AnimateOpacity } from '../../types';
2+
3+
export type BlurParams = PresenceDuration &
4+
PresenceEasing &
5+
AnimateOpacity & {
6+
/** The radius of pixels to blend into the blur. A length string, defaulting to '20px'. */
7+
fromRadius?: string;
8+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Blur } from './Blur';
2+
export type { BlurParams } from './blur-types';

packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,17 @@ function createCollapseAtoms({
6969
};
7070
}
7171

72-
/** Define a presence motion for collapse/expand */
72+
/**
73+
* Define a presence motion for collapse/expand
74+
*
75+
* @param element - The element to apply the collapse motion to
76+
* @param duration - Time (ms) for the enter transition (expand). Defaults to the `durationNormal` value (200 ms)
77+
* @param easing - Easing curve for the enter transition. Defaults to the `curveEasyEaseMax` value
78+
* @param exitDuration - Time (ms) for the exit transition (collapse). Defaults to the `duration` param for symmetry
79+
* @param exitEasing - Easing curve for the exit transition. Defaults to the `easing` param for symmetry
80+
* @param animateOpacity - Whether to animate the opacity. Defaults to `true`
81+
* @param orientation - The orientation of the size animation. Defaults to `'vertical'` to expand/collapse the height
82+
*/
7383
const collapsePresenceFn: PresenceMotionFn<CollapseParams> = ({
7484
element,
7585
duration = motionTokens.durationNormal,
@@ -94,7 +104,21 @@ const collapsePresenceFn: PresenceMotionFn<CollapseParams> = ({
94104
});
95105
};
96106

97-
/** Define a presence motion for collapse/expand that can stagger the size and opacity motions by a given delay */
107+
/**
108+
* Define a presence motion for collapse/expand that can stagger the size and opacity motions by a given delay
109+
*
110+
* @param element - The element to apply the collapse motion to
111+
* @param sizeDuration - Time (ms) for the size expand. Defaults to the `durationNormal` value (200 ms)
112+
* @param opacityDuration - Time (ms) for the fade-in. Defaults to the `durationSlower` value (400 ms)
113+
* @param easing - Easing curve for the enter transition. Defaults to the `curveEasyEase` value
114+
* @param delay - Time (ms) between the size expand start and the fade-in start. Defaults to the `durationNormal` value (200 ms)
115+
* @param exitSizeDuration - Time (ms) for the size collapse. Defaults to the `sizeDuration` param for temporal symmetry
116+
* @param exitOpacityDuration - Time (ms) for the fade-out. Defaults to the `opacityDuration` param for temporal symmetry
117+
* @param exitEasing - Easing curve for the exit transition. Defaults to the `easing` param for symmetry
118+
* @param exitDelay - Time (ms) between the fade-out start and the size collapse start. Defaults to the `durationSlower` value (400 ms)
119+
* @param animateOpacity - Whether to animate the opacity. Defaults to `true`
120+
* @param orientation - The orientation of the size animation. Defaults to `'vertical'` to expand/collapse the height
121+
*/
98122
const collapseDelayedPresenceFn: PresenceMotionFn<CollapseDelayedParams> = ({
99123
element,
100124
sizeDuration = motionTokens.durationNormal,

packages/react-components/react-motion-components-preview/library/src/components/Collapse/collapse-types.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,15 @@
1+
import type { PresenceDuration, PresenceEasing, AnimateOpacity } from '../../types';
2+
13
export type CollapseOrientation = 'horizontal' | 'vertical';
24

35
/** Common properties shared by all collapse components */
4-
type CollapseBaseParams = {
5-
/** Easing curve for the enter transition. Defaults to the `curveEasyEaseMax` value. */
6-
easing?: string;
7-
8-
/** Easing curve for the exit transition. Defaults to the `easing` param for symmetry. */
9-
exitEasing?: string;
6+
type CollapseBaseParams = PresenceEasing &
7+
AnimateOpacity & {
8+
/** The orientation of the size animation. Defaults to `'vertical'` to expand/collapse the height. */
9+
orientation?: CollapseOrientation;
10+
};
1011

11-
/** The orientation of the size animation. Defaults to `'vertical'` to expand/collapse the height. */
12-
orientation?: CollapseOrientation;
13-
14-
/** Whether to animate the opacity. Defaults to `true`. */
15-
animateOpacity?: boolean;
16-
};
17-
18-
export type CollapseParams = CollapseBaseParams & {
19-
/** Time (ms) for the enter transition (expand). Defaults to the `durationNormal` value (200 ms). */
20-
duration?: number;
21-
22-
/** Time (ms) for the exit transition (collapse). Defaults to the `duration` param for symmetry. */
23-
exitDuration?: number;
24-
};
12+
export type CollapseParams = CollapseBaseParams & PresenceDuration;
2513

2614
export type CollapseDelayedParams = CollapseBaseParams & {
2715
/** Time (ms) for the size expand. Defaults to the `durationNormal` value (200 ms). */

0 commit comments

Comments
 (0)