Skip to content

Commit 7857108

Browse files
authored
Merge branch 'microsoft:master' into users/machi/focusgroup
2 parents 8ff8565 + 93d6841 commit 7857108

20 files changed

Lines changed: 699 additions & 32 deletions
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: add motionSlot() as parallel to presenceMotionSlot()",
4+
"packageName": "@fluentui/react-motion",
5+
"email": "robertpenner@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}
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: add motionSlot() as parallel to presenceMotionSlot()",
4+
"packageName": "@fluentui/react-motion",
5+
"email": "robertpenner@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "refactor: migrate ProgressBar from CSS to motion components",
4+
"packageName": "@fluentui/react-progress",
5+
"email": "robertpenner@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ export const MotionRefForwarderReset: React_2.FC<{
9393
children: React_2.ReactElement;
9494
}>;
9595

96+
// @public (undocumented)
97+
export function motionSlot<MotionParams extends Record<string, MotionParam> = {}>(motion: MotionSlotProps<MotionParams> | null | undefined, options: {
98+
elementType: React_2.FC<MotionComponentProps & MotionParams>;
99+
defaultProps: MotionSlotRenderProps & MotionParams;
100+
}): SlotComponentType<MotionSlotRenderProps & MotionParams>;
101+
102+
// @public (undocumented)
103+
export type MotionSlotProps<MotionParams extends Record<string, MotionParam> = {}> = Pick<MotionComponentProps, 'imperativeRef' | 'onMotionFinish' | 'onMotionStart' | 'onMotionCancel'> & {
104+
as?: JSXIntrinsicElementKeys;
105+
children?: SlotRenderFunction<MotionSlotRenderProps & MotionParams & {
106+
children: JSXElement;
107+
}>;
108+
};
109+
96110
// @public (undocumented)
97111
export const motionTokens: {
98112
curveAccelerateMax: "cubic-bezier(0.9,0.1,1,0.2)";

packages/react-components/react-motion/library/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export { createPresenceComponentVariant } from './factories/createPresenceCompon
1616
export { PresenceGroup } from './components/PresenceGroup';
1717
export { MotionRefForwarder, MotionRefForwarderReset, useMotionForwardedRef } from './components/MotionRefForwarder';
1818

19+
export { motionSlot, type MotionSlotProps } from './slots/motionSlot';
1920
export { presenceMotionSlot, type PresenceMotionSlotProps } from './slots/presenceMotionSlot';
2021

2122
export {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/** @jsxRuntime automatic */
2+
/** @jsxImportSource @fluentui/react-jsx-runtime */
3+
4+
import { assertSlots, type ComponentProps, type ComponentState, type Slot } from '@fluentui/react-utilities';
5+
import { render } from '@testing-library/react';
6+
import * as React from 'react';
7+
8+
import { createMotionComponent } from '../factories/createMotionComponent';
9+
import { motionSlot, type MotionSlotProps } from './motionSlot';
10+
11+
type TestComponentSlots = { motion: Slot<MotionSlotProps> };
12+
type TestComponentProps = ComponentProps<Partial<TestComponentSlots>>;
13+
type TestComponentState = ComponentState<TestComponentSlots>;
14+
15+
const TestMotion = jest.fn(
16+
createMotionComponent({
17+
keyframes: [{ opacity: 0 }, { opacity: 1 }],
18+
duration: 300,
19+
}),
20+
);
21+
const TestComponent: React.FC<TestComponentProps> = props => {
22+
const state: TestComponentState = {
23+
components: {
24+
motion: TestMotion,
25+
},
26+
motion: motionSlot(props.motion, {
27+
elementType: TestMotion,
28+
defaultProps: {},
29+
}),
30+
};
31+
32+
assertSlots<TestComponentSlots>(state);
33+
34+
return (
35+
<div data-testid="root">
36+
{
37+
// TODO: state.motion is non nullable, but assertSlots asserts it as nullable
38+
// FIXME: this should be resolved by properly splitting props and state slots declaration
39+
state.motion && (
40+
<state.motion>
41+
<div data-testid="content" />
42+
</state.motion>
43+
)
44+
}
45+
</div>
46+
);
47+
};
48+
49+
describe('motionSlot', () => {
50+
beforeEach(() => {
51+
jest.clearAllMocks();
52+
});
53+
54+
it('renders a component with a slot', () => {
55+
const { queryByTestId } = render(<TestComponent />);
56+
57+
expect(queryByTestId('root')).not.toBeNull();
58+
expect(queryByTestId('content')).not.toBeNull();
59+
60+
expect(TestMotion).toHaveBeenCalled();
61+
});
62+
63+
it('handles object as value', () => {
64+
const onMotionStart = jest.fn();
65+
const { queryByTestId } = render(<TestComponent motion={{ onMotionStart }} />);
66+
67+
expect(queryByTestId('content')).not.toBeNull();
68+
69+
expect(TestMotion).toHaveBeenCalled();
70+
const firstCall = TestMotion.mock.calls[0];
71+
expect(firstCall[0]).toEqual(expect.objectContaining({ onMotionStart }));
72+
});
73+
74+
it('handles function as value', () => {
75+
const renderFn = jest.fn((Component, props) => <Component {...props} />);
76+
const { queryByTestId } = render(<TestComponent motion={{ children: renderFn }} />);
77+
78+
expect(queryByTestId('content')).not.toBeNull();
79+
expect(renderFn).toHaveBeenCalled();
80+
expect(renderFn).toHaveBeenCalledWith(TestMotion, {
81+
children: expect.objectContaining({ type: 'div' }),
82+
});
83+
});
84+
85+
it('handles "null" as value', () => {
86+
const { queryByTestId } = render(<TestComponent motion={null} />);
87+
88+
expect(queryByTestId('content')).not.toBeNull();
89+
expect(TestMotion).not.toHaveBeenCalled();
90+
});
91+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as React from 'react';
2+
import { SLOT_ELEMENT_TYPE_SYMBOL, SLOT_RENDER_FUNCTION_SYMBOL } from '@fluentui/react-utilities';
3+
import type {
4+
JSXElement,
5+
JSXIntrinsicElementKeys,
6+
SlotComponentType,
7+
SlotRenderFunction,
8+
} from '@fluentui/react-utilities';
9+
10+
import type { MotionComponentProps } from '../factories/createMotionComponent';
11+
import type { MotionParam } from '../types';
12+
13+
/**
14+
* @internal
15+
*/
16+
type MotionSlotRenderProps = Pick<MotionComponentProps, 'onMotionFinish' | 'onMotionStart' | 'onMotionCancel'>;
17+
18+
export type MotionSlotProps<MotionParams extends Record<string, MotionParam> = {}> = Pick<
19+
MotionComponentProps,
20+
'imperativeRef' | 'onMotionFinish' | 'onMotionStart' | 'onMotionCancel'
21+
> & {
22+
// FIXME: 'as' property is required by design on the slot AP but it does not support components, only intrinsic
23+
// elements motion slots do not support intrinsic elements, only custom components.
24+
/**
25+
* @deprecated Do not use. Motion Slots do not support intrinsic elements.
26+
*
27+
* If you want to override the animation, use the children render function instead.
28+
*/
29+
as?: JSXIntrinsicElementKeys;
30+
31+
// TODO: remove once React v18 slot API is modified ComponentProps is not properly adding render function as a
32+
// possible value for children
33+
children?: SlotRenderFunction<MotionSlotRenderProps & MotionParams & { children: JSXElement }>;
34+
};
35+
36+
export function motionSlot<MotionParams extends Record<string, MotionParam> = {}>(
37+
motion: MotionSlotProps<MotionParams> | null | undefined,
38+
options: {
39+
elementType: React.FC<MotionComponentProps & MotionParams>;
40+
defaultProps: MotionSlotRenderProps & MotionParams;
41+
},
42+
): SlotComponentType<MotionSlotRenderProps & MotionParams> {
43+
// eslint-disable-next-line @typescript-eslint/no-deprecated
44+
const { as, children, ...rest } = motion ?? {};
45+
46+
if (process.env.NODE_ENV !== 'production') {
47+
if (typeof as !== 'undefined') {
48+
throw new Error(`@fluentui/react-motion: "as" property is not supported on motion slots.`);
49+
}
50+
}
51+
52+
if (motion === null) {
53+
// Heads up!
54+
// Render function is used there to avoid rendering a motion component and render children directly
55+
const renderFn: SlotRenderFunction<MotionSlotRenderProps & MotionParams & { children: JSXElement }> = (
56+
_,
57+
props,
58+
) => <>{props.children}</>;
59+
60+
/**
61+
* Casting is required here as SlotComponentType is a function, not an object.
62+
* Although SlotComponentType has a function signature, it is still just an object.
63+
* This is required to make a slot callable (JSX compatible), this is the exact same approach
64+
* that is used on `@types/react` components
65+
*/
66+
return {
67+
[SLOT_RENDER_FUNCTION_SYMBOL]: renderFn,
68+
[SLOT_ELEMENT_TYPE_SYMBOL]: options.elementType,
69+
} as SlotComponentType<MotionSlotRenderProps & MotionParams>;
70+
}
71+
72+
/**
73+
* Casting is required here as SlotComponentType is a function, not an object.
74+
* Although SlotComponentType has a function signature, it is still just an object.
75+
* This is required to make a slot callable (JSX compatible), this is the exact same approach
76+
* that is used on `@types/react` components
77+
*/
78+
const propsWithMetadata = {
79+
...options.defaultProps,
80+
...rest,
81+
[SLOT_ELEMENT_TYPE_SYMBOL]: options.elementType,
82+
} as SlotComponentType<MotionSlotRenderProps & MotionParams>;
83+
84+
if (typeof children === 'function') {
85+
propsWithMetadata[SLOT_RENDER_FUNCTION_SYMBOL] = children as SlotRenderFunction<
86+
MotionSlotRenderProps & MotionParams
87+
>;
88+
}
89+
90+
return propsWithMetadata;
91+
}

packages/react-components/react-progress/library/etc/react-progress.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { ComponentProps } from '@fluentui/react-utilities';
88
import type { ComponentState } from '@fluentui/react-utilities';
99
import type { ForwardRefComponent } from '@fluentui/react-utilities';
1010
import type { JSXElement } from '@fluentui/react-utilities';
11+
import type { MotionSlotProps } from '@fluentui/react-motion';
1112
import * as React_2 from 'react';
1213
import type { Slot } from '@fluentui/react-utilities';
1314
import type { SlotClassNames } from '@fluentui/react-utilities';
@@ -16,7 +17,7 @@ import type { SlotClassNames } from '@fluentui/react-utilities';
1617
export const ProgressBar: ForwardRefComponent<ProgressBarProps>;
1718

1819
// @public (undocumented)
19-
export const progressBarClassNames: SlotClassNames<ProgressBarSlots>;
20+
export const progressBarClassNames: SlotClassNames<Omit<ProgressBarSlots, 'indeterminateMotion'>>;
2021

2122
// @public
2223
export type ProgressBarProps = Omit<ComponentProps<ProgressBarSlots>, 'size'> & {
@@ -31,6 +32,7 @@ export type ProgressBarProps = Omit<ComponentProps<ProgressBarSlots>, 'size'> &
3132
export type ProgressBarSlots = {
3233
root: NonNullable<Slot<'div'>>;
3334
bar?: NonNullable<Slot<'div'>>;
35+
indeterminateMotion?: Slot<MotionSlotProps>;
3436
};
3537

3638
// @public

packages/react-components/react-progress/library/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"license": "MIT",
1414
"dependencies": {
15+
"@fluentui/react-motion": "^9.11.5",
1516
"@fluentui/react-field": "^9.4.16",
1617
"@fluentui/react-jsx-runtime": "^9.4.1",
1718
"@fluentui/react-shared-contexts": "^9.26.2",

packages/react-components/react-progress/library/src/components/ProgressBar/ProgressBar.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { MotionSlotProps } from '@fluentui/react-motion';
12
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
23

34
export type ProgressBarSlots = {
@@ -9,6 +10,10 @@ export type ProgressBarSlots = {
910
* The filled portion of the ProgressBar bar. Animated in the indeterminate state, when no value is provided.
1011
*/
1112
bar?: NonNullable<Slot<'div'>>;
13+
/**
14+
* Motion slot for the indeterminate animation. Pass `null` to disable the animation.
15+
*/
16+
indeterminateMotion?: Slot<MotionSlotProps>;
1217
};
1318

1419
/**

0 commit comments

Comments
 (0)