Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(motion): add Slide motion component #33878

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual regressions to review in the fluentuiv9 Visual Regression Report

Drawer 1 screenshots
Image Name Diff(in Pixels) Image Type
Drawer.overlay drawer full - Dark Mode.chromium.png 975 Changed

"type": "minor",
"comment": "Add Slide motion component",
"packageName": "@fluentui/react-motion-components-preview",
"email": "robertpenner@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -39,6 +39,9 @@ export const createFadePresence: PresenceMotionCreator<FadeVariantParams>;
// @public
export const createScalePresence: PresenceMotionFnCreator<ScaleVariantParams_unstable, ScaleRuntimeParams_unstable>;

// @public
export const createSlidePresence: PresenceMotionFnCreator<SlideVariantParams_unstable, SlideRuntimeParams_unstable>;

// @public
export const Fade: PresenceComponent< {}>;

@@ -57,6 +60,15 @@ export const ScaleRelaxed: PresenceComponent<ScaleRuntimeParams_unstable>;
// @public (undocumented)
export const ScaleSnappy: PresenceComponent<ScaleRuntimeParams_unstable>;

// @public
export const Slide: PresenceComponent<SlideRuntimeParams_unstable>;

// @public (undocumented)
export const SlideRelaxed: PresenceComponent<SlideRuntimeParams_unstable>;

// @public (undocumented)
export const SlideSnappy: PresenceComponent<SlideRuntimeParams_unstable>;

// (No @packageDocumentation comment for this package)

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PresenceDirection, AtomMotion } from '@fluentui/react-motion/src/types';
import { SlideOrientation } from '../components/Slide/Slide.types';

/**
* Generates a motion atom object for a horizontal or vertical translation, from a specified distance to zero.
* @param direction - The functional direction of the motion: 'enter' or 'exit'.
* @param orientation - The axis of the translation: 'X' or 'Y'.
* @param distance - The distance of the slide relative to the natural position. It can be pixels or other length unit.
* @param duration - The duration of the motion in milliseconds.
* @param easing - The easing curve for the motion.
*/
export const slideAtom = ({
direction,
orientation,
distance,
duration,
easing,
}: {
direction: PresenceDirection;
orientation: SlideOrientation;
distance: string;
duration: number;
easing: string;
}): AtomMotion => {
const axis: 'X' | 'Y' = orientation === 'horizontal' ? 'X' : 'Y';
const keyframes = [{ transform: `translate${axis}(${distance})` }, { transform: `translate${axis}(0)` }];
if (direction === 'exit') {
keyframes.reverse();
}
return {
keyframes,
duration,
easing,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AtomMotion, PresenceDirection } from '@fluentui/react-motion';

/**
* Generates a motion atom object for visibility, toggling between 'visible' and 'hidden'.
* This is useful for making elements disappear when there is no fade-out.
* @param direction - The functional direction of the motion: 'enter' or 'exit'.
* @param duration - The duration of the motion in milliseconds.
* @returns A motion atom object with visibility keyframes and the supplied duration.
*/
export const visibilityAtom = ({
direction,
duration,
}: {
direction: PresenceDirection;
duration: number;
}): AtomMotion => {
const visibility = direction === 'enter' ? 'visible' : 'hidden';
// For the exit animation, offset is 1 so the keyframe to set visibility to 'hidden' is at the end of the animation.
const offset = direction === 'enter' ? 0 : 1;
return {
keyframes: [{ visibility, offset }],
duration,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expectPresenceMotionFunction, expectPresenceMotionArray } from '../../testing/testUtils';
import { Slide } from './Slide';

describe('Slide', () => {
it('stores its motion definition as a static function', () => {
expectPresenceMotionFunction(Slide);
});

it('generates a motion definition from the static function', () => {
expectPresenceMotionArray(Slide);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { motionTokens, createPresenceComponent, AtomMotion } from '@fluentui/react-motion';
import { PresenceMotionFnCreator } from '../../types';
import { SlideRuntimeParams_unstable, SlideVariantParams_unstable } from './Slide.types';
import { fadeAtom } from '../../atoms/fade-atom';
import { slideAtom } from '../../atoms/slide-atom';
import { visibilityAtom } from '../../atoms/visibility-atom';

/** Define a presence motion for slide in/out */
export const createSlidePresence: PresenceMotionFnCreator<SlideVariantParams_unstable, SlideRuntimeParams_unstable> =
({
enterDuration = motionTokens.durationNormal,
enterEasing = motionTokens.curveDecelerateMid,
exitDuration = enterDuration, // defaults to the enter duration for symmetry
exitEasing = motionTokens.curveAccelerateMid,
} = {}) =>
({ animateOpacity = true, orientation = 'vertical', distance = '20px' }) => {
// ----- ENTER -----
const enterAtoms: AtomMotion[] = [
slideAtom({
direction: 'enter',
orientation,
distance,
duration: enterDuration,
easing: enterEasing,
}),
];
if (animateOpacity) {
enterAtoms.push(
fadeAtom({
direction: 'enter',
duration: enterDuration,
easing: enterEasing,
}),
);
} else {
// Since there is no fade-in, use visibility to show the element
enterAtoms.push(visibilityAtom({ direction: 'enter', duration: enterDuration }));
}

// ----- EXIT -----
const exitAtoms: AtomMotion[] = [
slideAtom({
direction: 'exit',
orientation,
distance,
duration: exitDuration,
easing: exitEasing,
}),
];
if (animateOpacity) {
exitAtoms.push(
fadeAtom({
direction: 'exit',
duration: exitDuration,
easing: exitEasing,
}),
);
} else {
// Since there is no fade-out, use visibility to hide the element
enterAtoms.push(visibilityAtom({ direction: 'exit', duration: exitDuration }));
}

return {
enter: enterAtoms,
exit: exitAtoms,
};
};

/** A React component that applies slide in/out transitions to its children. */
export const Slide = createPresenceComponent(createSlidePresence());

export const SlideSnappy = createPresenceComponent(
createSlidePresence({
enterDuration: motionTokens.durationNormal,
enterEasing: motionTokens.curveDecelerateMax,
exitDuration: motionTokens.durationNormal,
exitEasing: motionTokens.curveAccelerateMax,
}),
);

export const SlideRelaxed = createPresenceComponent(
createSlidePresence({
enterDuration: motionTokens.durationGentle,
enterEasing: motionTokens.curveDecelerateMid,
exitDuration: motionTokens.durationGentle,
exitEasing: motionTokens.curveAccelerateMid,
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type SlideOrientation = 'horizontal' | 'vertical';

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

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

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

/** 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 SlideRuntimeParams_unstable = {
/** Whether to animate the opacity. Defaults to `true`. */
animateOpacity?: boolean;

/**
* The orientation of the slide animation: 'horizontal' or 'vertical'
* @default 'vertical'
*/
orientation?: SlideOrientation;

/**
* The distance of the slide, relative to the content's natural position.
* Can be positive or negative, in pixels or other length units.
* @default '10px'
*/
distance?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Slide, SlideRelaxed, SlideSnappy, createSlidePresence } from './Slide';
Original file line number Diff line number Diff line change
@@ -9,3 +9,4 @@ export {
export type { CollapseRuntimeParams } from './components/Collapse';
export { Fade, FadeSnappy, FadeRelaxed, createFadePresence } from './components/Fade';
export { Scale, ScaleSnappy, ScaleRelaxed, createScalePresence } from './components/Scale';
export { Slide, SlideSnappy, SlideRelaxed, createSlidePresence } from './components/Slide';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import { PresenceComponentProps } from '@fluentui/react-components';
import { Slide } from '@fluentui/react-motion-components-preview';

const LoremIpsum = React.forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>((props, ref) => (
<div ref={ref} {...props}>
{'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. '.repeat(
10,
)}
</div>
));

export const DefaultSlide = (props: PresenceComponentProps) => {
return (
<Slide {...props}>
<LoremIpsum />
</Slide>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
- The predefined fade transition can be disabled by setting `animateOpacity` to `false`.
- A scale variant can be created with the factory function `createSlidePresence()`, then converting the result to a React component using `createPresenceComponent()`:

```tsx
import { motionTokens, createPresenceComponentVariant } from '@fluentui/react-components';
import { createSlidePresence } from '@fluentui/react-motion-components-preview';

const CustomSlideVariant = createPresenceComponent(
createSlidePresence({
enterDuration: motionTokens.durationSlow,
enterEasing: motionTokens.curveEasyEaseMax,
exitDuration: motionTokens.durationNormal,
exitEasing: motionTokens.curveEasyEaseMax,
}),
);

const CustomSlide = ({ visible }) => (
<CustomSlideVariant animateOpacity={false} unmountOnExit visible={visible}>
{/* Content */}
</CustomSlideVariant>
);
```
Loading
Oops, something went wrong.