diff --git a/change/@fluentui-react-message-bar-72abc821-ab32-4cac-8dd2-4c8dce4c810e.json b/change/@fluentui-react-message-bar-72abc821-ab32-4cac-8dd2-4c8dce4c810e.json new file mode 100644 index 0000000000000..45370fc286bc2 --- /dev/null +++ b/change/@fluentui-react-message-bar-72abc821-ab32-4cac-8dd2-4c8dce4c810e.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "refactor(MessageBar): migrate slide & fade to motion components", + "packageName": "@fluentui/react-message-bar", + "email": "robertpenner@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-message-bar/library/package.json b/packages/react-components/react-message-bar/library/package.json index f290113045fe5..f4091aa19bfce 100644 --- a/packages/react-components/react-message-bar/library/package.json +++ b/packages/react-components/react-message-bar/library/package.json @@ -21,13 +21,14 @@ "@fluentui/react-button": "^9.3.98", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.0.48", + "@fluentui/react-motion": "^9.6.4", + "@fluentui/react-motion-components-preview": "^0.4.0", "@fluentui/react-shared-contexts": "^9.21.2", "@fluentui/react-link": "^9.3.5", "@fluentui/react-theme": "^9.1.24", "@fluentui/react-utilities": "^9.18.19", "@griffel/react": "^1.5.22", - "@swc/helpers": "^0.5.1", - "react-transition-group": "^4.4.1" + "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.8.0 <19.0.0", diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.types.ts b/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.types.ts index ac8dbb62040ee..95b0ee35ccf3d 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.types.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.types.ts @@ -48,5 +48,8 @@ export type MessageBarProps = ComponentProps & export type MessageBarState = ComponentState & Required> & Pick & { + /** + * @deprecated Code is unused, replaced by motion components + */ transitionClassName: string; }; diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBar.ts b/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBar.ts index cb2e0e7f1bc25..1c6145749da4f 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBar.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBar.ts @@ -21,6 +21,7 @@ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref(null); const bodyRef = React.useRef(null); diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBarStyles.styles.ts b/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBarStyles.styles.ts index 1a9f754e95e4f..7a27836250873 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBarStyles.styles.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBarStyles.styles.ts @@ -114,7 +114,6 @@ export const useMessageBarStyles_unstable = (state: MessageBarState): MessageBar state.layout === 'multiline' && styles.rootMultiline, state.shape === 'square' && styles.square, rootIntentStyles[state.intent], - state.transitionClassName, state.root.className, ); diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarGroup.motions.tsx b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarGroup.motions.tsx new file mode 100644 index 0000000000000..3df0dab15d137 --- /dev/null +++ b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarGroup.motions.tsx @@ -0,0 +1,94 @@ +import { motionTokens, createPresenceComponent, PresenceDirection, AtomMotion } from '@fluentui/react-motion'; +import { MessageBarGroupProps } from './MessageBarGroup.types'; + +// TODO: import these atoms from react-motion-components-preview once they're available there + +interface FadeAtomParams { + direction: PresenceDirection; + duration: number; + easing?: string; + fromValue?: number; +} + +/** + * Generates a motion atom object for a fade in or fade out. + * @param direction - The functional direction of the motion: 'enter' or 'exit'. + * @param duration - The duration of the motion in milliseconds. + * @param easing - The easing curve for the motion. Defaults to `motionTokens.curveLinear`. + * @param fromValue - The starting opacity value. Defaults to 0. + * @returns A motion atom object with opacity keyframes and the supplied duration and easing. + */ +const fadeAtom = ({ + direction, + duration, + easing = motionTokens.curveLinear, + fromValue = 0, +}: FadeAtomParams): AtomMotion => { + const keyframes = [{ opacity: fromValue }, { opacity: 1 }]; + if (direction === 'exit') { + keyframes.reverse(); + } + return { + keyframes, + duration, + easing, + }; +}; + +/** + * Generates a motion atom object for an X or Y translation, from a specified distance to zero. + * @param direction - The functional direction of the motion: 'enter' or 'exit'. + * @param axis - The axis of the translation: 'X' or 'Y'. + * @param fromValue - The starting position of the slide; it can be a percentage or pixels. + * @param duration - The duration of the motion in milliseconds. + * @param easing - The easing curve for the motion. Defaults to `motionTokens.curveDecelerateMid`. + */ +const slideAtom = ({ + direction, + axis, + fromValue, + duration, + easing = motionTokens.curveDecelerateMid, +}: { + direction: PresenceDirection; + axis: 'X' | 'Y'; + fromValue: string; + duration: number; + easing?: string; +}): AtomMotion => { + const keyframes = [{ transform: `translate${axis}(${fromValue})` }, { transform: `translate${axis}(0)` }]; + if (direction === 'exit') { + keyframes.reverse(); + } + return { + keyframes, + duration, + easing, + }; +}; + +/** + * A presence component for a MessageBar to enter and exit from a MessageBarGroup. + * It has an optional enter transition of a slide-in and fade-in, + * when the `animate` prop is set to `'both'`. + * It always has an exit transition of a fade-out. + */ +export const MessageBarMotion = createPresenceComponent<{ animate?: MessageBarGroupProps['animate'] }>( + ({ animate }) => { + const duration = motionTokens.durationGentle; + + return { + enter: + animate === 'both' + ? // enter with slide and fade + [ + fadeAtom({ direction: 'enter', duration }), + slideAtom({ direction: 'enter', axis: 'Y', fromValue: '-100%', duration }), + ] + : [], // no enter motion + + // Always exit with a fade + exit: fadeAtom({ direction: 'exit', duration }), + }; + }, +); diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarGroup.types.ts b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarGroup.types.ts index dc0cbc00b1a75..3892ffb59ac5c 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarGroup.types.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarGroup.types.ts @@ -18,7 +18,9 @@ export type MessageBarGroupProps = ComponentProps & { */ export type MessageBarGroupState = ComponentState & Pick & { + /** @deprecated property is unused; these CSS animations were replaced by motion components */ enterStyles: string; + /** @deprecated property is unused; these CSS animations were replaced by motion components */ exitStyles: string; children: React.ReactElement[]; }; diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarTransition.tsx b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarTransition.tsx deleted file mode 100644 index a6e5c4ab95794..0000000000000 --- a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/MessageBarTransition.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from 'react'; -import { Transition, TransitionStatus } from 'react-transition-group'; -import { MessageBarTransitionContextProvider } from '../../contexts/messageBarTransitionContext'; -import { MessageBarGroupProps } from './MessageBarGroup.types'; - -const getClassName = ( - status: TransitionStatus, - enterClassName: string, - exitClassName: string, - animate: MessageBarGroupProps['animate'], -) => { - switch (status) { - case 'entering': - case 'entered': - return animate === 'both' ? enterClassName : ''; - case 'exiting': - case 'exited': - return exitClassName; - default: - return ''; - } -}; - -/** - * Internal component that controls the animation transition for MessageBar components - * @internal - */ -export const MessageBarTransition: React.FC<{ - children: React.ReactElement; - enterClassName: string; - exitClassName: string; - animate: MessageBarGroupProps['animate']; -}> = ({ children, enterClassName, exitClassName, animate, ...rest }) => { - const nodeRef = React.useRef(null); - - return ( - - {state => ( - - {children} - - )} - - ); -}; - -const MessageBarTransitionInner: React.FC<{ - children: React.ReactElement; - enterClassName: string; - exitClassName: string; - animate: MessageBarGroupProps['animate']; - nodeRef: React.Ref; - state: TransitionStatus; -}> = ({ children, state, enterClassName, exitClassName, animate, nodeRef }) => { - const className = getClassName(state, enterClassName, exitClassName, animate); - const context = React.useMemo( - () => ({ - className, - nodeRef, - }), - [className, nodeRef], - ); - - return {children}; -}; diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/renderMessageBarGroup.tsx b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/renderMessageBarGroup.tsx index ddd4c6d29760d..70e62632523c6 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/renderMessageBarGroup.tsx +++ b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/renderMessageBarGroup.tsx @@ -3,8 +3,8 @@ import { assertSlots } from '@fluentui/react-utilities'; import type { MessageBarGroupState, MessageBarGroupSlots } from './MessageBarGroup.types'; -import { TransitionGroup } from 'react-transition-group'; -import { MessageBarTransition } from './MessageBarTransition'; +import { PresenceGroup } from '@fluentui/react-motion'; +import { MessageBarMotion } from './MessageBarGroup.motions'; /** * Render the final JSX of MessageBarGroup @@ -14,18 +14,13 @@ export const renderMessageBarGroup_unstable = (state: MessageBarGroupState) => { return ( - + {state.children.map(child => ( - + {child} - + ))} - + ); }; diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/useMessageBarGroupStyles.styles.ts b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/useMessageBarGroupStyles.styles.ts index a7054fe773448..bdc96b5d9e631 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/useMessageBarGroupStyles.styles.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBarGroup/useMessageBarGroupStyles.styles.ts @@ -1,5 +1,4 @@ -import { makeStyles, mergeClasses } from '@griffel/react'; -import { tokens } from '@fluentui/react-theme'; +import { mergeClasses } from '@griffel/react'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { MessageBarGroupSlots, MessageBarGroupState } from './MessageBarGroup.types'; @@ -7,49 +6,12 @@ export const messageBarGroupClassNames: SlotClassNames = { root: 'fui-MessageBarGroup', }; -/** - * Styles for the root slot - */ -const useStyles = makeStyles({ - base: { - animationFillMode: 'forwards', - animationDuration: tokens.durationNormal, - }, - - enter: { - animationName: { - from: { - opacity: 0, - transform: 'translateY(-100%)', - }, - to: { - opacity: 1, - transform: 'translateY(0)', - }, - }, - }, - - exit: { - animationName: { - from: { - opacity: 1, - }, - to: { - opacity: 0, - }, - }, - }, -}); - /** * Apply styling to the MessageBarGroup slots based on the state */ export const useMessageBarGroupStyles_unstable = (state: MessageBarGroupState): MessageBarGroupState => { 'use no memo'; - const styles = useStyles(); state.root.className = mergeClasses(messageBarGroupClassNames.root, state.root.className); - state.enterStyles = mergeClasses(styles.base, styles.enter); - state.exitStyles = mergeClasses(styles.base, styles.exit); return state; }; diff --git a/packages/react-components/react-message-bar/library/src/contexts/messageBarTransitionContext.ts b/packages/react-components/react-message-bar/library/src/contexts/messageBarTransitionContext.ts index 0a023b4b0e46c..5c1dea8f74f58 100644 --- a/packages/react-components/react-message-bar/library/src/contexts/messageBarTransitionContext.ts +++ b/packages/react-components/react-message-bar/library/src/contexts/messageBarTransitionContext.ts @@ -1,6 +1,9 @@ import * as React from 'react'; export type MessageBarTransitionContextValue = { + /** + * @deprecated CSS className is no longer used for this transition, replaced by motion components + */ className: string; nodeRef: React.Ref; }; @@ -16,7 +19,7 @@ export const messageBarTransitionContextDefaultValue: MessageBarTransitionContex }; /** - * Context to pass animation className to MessageBar components + * Context to pass nodeRef for animation to MessageBar components * @internal */ export const MessageBarTransitionContextProvider = messageBarTransitionContext.Provider;