From d5f5f6c6905c819764d1702bd0b032ccfe37970e Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Fri, 29 Sep 2023 15:14:54 +0200 Subject: [PATCH 1/7] feat: Implement motion for MessageBar --- .../react-message-bar-preview/package.json | 3 +- .../components/MessageBar/MessageBar.types.ts | 11 +- .../MessageBar/renderMessageBar.tsx | 4 + .../components/MessageBar/useMessageBar.ts | 16 ++- .../MessageBar/useMessageBarStyles.styles.ts | 39 ++++++- .../stories/MessageBar/Animation.stories.tsx | 108 ++++++++++++++++++ .../stories/MessageBar/Dismiss.stories.tsx | 95 +++++++++++++++ .../stories/MessageBar/index.stories.tsx | 2 + 8 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx create mode 100644 packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx diff --git a/packages/react-components/react-message-bar-preview/package.json b/packages/react-components/react-message-bar-preview/package.json index 41ae3a47b94fad..df0c666d8e9e7a 100644 --- a/packages/react-components/react-message-bar-preview/package.json +++ b/packages/react-components/react-message-bar-preview/package.json @@ -34,7 +34,8 @@ "@fluentui/react-button": "^9.3.44", "@fluentui/react-icons": "^2.0.217", "@fluentui/react-jsx-runtime": "^9.0.12", - "@fluentui/react-shared-contexts": "^9.9.2", + "@fluentui/react-motion-preview": "^0.2.8", + "@fluentui/react-shared-contexts": "^9.9.1", "@fluentui/react-theme": "^9.1.14", "@fluentui/react-utilities": "^9.13.5", "@griffel/react": "^1.5.14", diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.types.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.types.ts index 9a1689458def95..b16a5335f5bcfd 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.types.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.types.ts @@ -1,5 +1,6 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { MessageBarContextValue } from '../../contexts/messageBarContext'; +import type { MotionState } from '@fluentui/react-motion-preview'; +import type { MessageBarContextValue } from '../../contexts/messageBarContext'; export type MessageBarSlots = { root: Slot<'div'>; @@ -16,9 +17,15 @@ export type MessageBarContextValues = { export type MessageBarProps = ComponentProps & Pick & { intent?: 'info' | 'success' | 'warning' | 'error'; + visible?: boolean; + animate?: 'exit-only' | 'both'; + onDismiss?: () => void; }; /** * State used in rendering MessageBar */ -export type MessageBarState = ComponentState & Required>; +export type MessageBarState = ComponentState & + Required> & { + motionState: MotionState; + }; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBar/renderMessageBar.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBar/renderMessageBar.tsx index 83aa749e3d1eff..523941c140744f 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBar/renderMessageBar.tsx +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBar/renderMessageBar.tsx @@ -11,6 +11,10 @@ import { MessageBarContextProvider } from '../../contexts/messageBarContext'; export const renderMessageBar_unstable = (state: MessageBarState, contexts: MessageBarContextValues) => { assertSlots(state); + if (!state.motionState.canRender) { + return null; + } + return ( diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts index c65ec5e43ef920..fed37c923100f5 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { getNativeElementProps, slot, useMergedRefs } from '@fluentui/react-utilities'; +import { useMotion } from '@fluentui/react-motion-preview'; import type { MessageBarProps, MessageBarState } from './MessageBar.types'; import { getIntentIcon } from './getIntentIcon'; import { useMessageBarReflow } from './useMessageBarReflow'; @@ -14,12 +15,17 @@ import { useMessageBarReflow } from './useMessageBarReflow'; * @param ref - reference to root HTMLElement of MessageBar */ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref): MessageBarState => { - const { layout = 'auto', intent = 'info' } = props; - + const { layout = 'auto', intent = 'info', visible = true, onDismiss, animate = 'exit-only' } = props; const autoReflow = layout === 'auto'; const { ref: reflowRef, reflowing } = useMessageBarReflow(autoReflow); - const computedLayout = autoReflow ? (reflowing ? 'multiline' : 'singleline') : layout; + const motionState = useMotion(visible, { animateOnFirstMount: true }); + + React.useEffect(() => { + if (motionState.type === 'exited' && onDismiss) { + onDismiss(); + } + }, [motionState.type, onDismiss]); return { components: { @@ -28,7 +34,7 @@ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref = { root: 'fui-MessageBar', @@ -21,6 +22,8 @@ const useRootBaseStyles = makeResetStyles({ minHeight: '36px', boxSizing: 'border-box', backgroundColor: tokens.colorNeutralBackground3, + animationFillMode: 'forwards', + animationDuration: tokens.durationNormal, }); const useIconBaseStyles = makeResetStyles({ @@ -83,6 +86,32 @@ const useRootIntentStyles = makeStyles({ }, }); +const useMotionStyles = makeStyles({ + 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 MessageBar slots based on the state */ @@ -91,12 +120,18 @@ export const useMessageBarStyles_unstable = (state: MessageBarState): MessageBar const iconBaseStyles = useIconBaseStyles(); const multilineStyles = useMultilineStyles(); const iconIntentStyles = useIconIntentStyles(); - const rootIntntStyles = useRootIntentStyles(); + const rootIntentStyles = useRootIntentStyles(); + const motionStyles = useMotionStyles(); + + const shouldExit = (type: MotionType) => ['exiting', 'exited'].includes(type); + state.root.className = mergeClasses( messageBarClassNames.root, rootBaseStyles, state.layout === 'multiline' && multilineStyles.rootMultiline, - rootIntntStyles[state.intent], + rootIntentStyles[state.intent], + shouldExit(state.motionState.type) && motionStyles.exit, + state.animate === 'both' && !shouldExit(state.motionState.type) && motionStyles.enter, state.root.className, ); diff --git a/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx b/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx new file mode 100644 index 00000000000000..3315e603232b18 --- /dev/null +++ b/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import { Button, Link, makeStyles, shorthands, tokens, Field, RadioGroup, Radio } from '@fluentui/react-components'; +import { DismissRegular } from '@fluentui/react-icons'; +import { + MessageBar, + MessageBarActions, + MessageBarTitle, + MessageBarBody, + MessageBarProps, +} from '@fluentui/react-message-bar-preview'; + +const useStyles = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + marginTop: '10px', + ...shorthands.gap('10px'), + height: '300px', + ...shorthands.overflow('auto'), + ...shorthands.border('2px', 'solid', tokens.colorBrandForeground1), + }, +}); + +const intents = ['info', 'warning', 'error', 'success'] as const; + +interface Entry { + intent: (typeof intents)[number]; + id: number; + visible: boolean; +} + +export const Animation = () => { + const styles = useStyles(); + const counterRef = React.useRef(0); + const [animate, setAnimate] = React.useState('exit-only'); + const [messages, setMessages] = React.useState([]); + const prepend = () => { + const intentPos = Math.floor(Math.random() * intents.length); + const newEntry = { + intent: intents[intentPos], + visible: true, + id: counterRef.current++, + }; + + setMessages(s => [newEntry, ...s]); + }; + + const clear = () => { + setMessages(s => { + return s.map(entry => { + return { + ...entry, + visible: false, + }; + }); + }); + }; + + const onDismiss = (id: number) => () => { + setMessages(s => { + const newState = s.map(entry => ({ ...entry })); + return newState.filter(entry => entry.id !== id); + }); + }; + + const dismiss = (id: number) => () => { + setMessages(s => { + return s.map(entry => { + return { + ...entry, + visible: entry.id === id ? false : entry.visible, + }; + }); + }); + }; + + return ( + <> + + + + setAnimate(value as MessageBarProps['animate'])}> + + + + +
+ {messages.map(({ intent, visible, id }) => ( + + + Descriptive title + Message providing information to the user with actionable insights. Link + + } />} + /> + + ))} +
+ + ); +}; diff --git a/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx b/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx new file mode 100644 index 00000000000000..ca8b19ba4b74da --- /dev/null +++ b/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { Button, Link, makeStyles, shorthands, tokens } from '@fluentui/react-components'; +import { DismissRegular } from '@fluentui/react-icons'; +import { MessageBar, MessageBarActions, MessageBarTitle, MessageBarBody } from '@fluentui/react-message-bar-preview'; + +const useStyles = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + marginTop: '10px', + ...shorthands.gap('10px'), + height: '300px', + ...shorthands.overflow('auto'), + ...shorthands.border('2px', 'solid', tokens.colorBrandForeground1), + }, +}); + +const intents = ['info', 'warning', 'error', 'success'] as const; + +interface Entry { + intent: (typeof intents)[number]; + id: number; + visible: boolean; +} + +export const Dismiss = () => { + const styles = useStyles(); + const counterRef = React.useRef(0); + const [messages, setMessages] = React.useState([]); + const prepend = () => { + const intentPos = Math.floor(Math.random() * intents.length); + const newEntry = { + intent: intents[intentPos], + visible: true, + id: counterRef.current++, + }; + + setMessages(s => [newEntry, ...s]); + }; + + const clear = () => { + setMessages(s => { + return s.map(entry => { + return { + ...entry, + visible: false, + }; + }); + }); + }; + + const onDismiss = (id: number) => () => { + setMessages(s => { + const newState = s.map(entry => ({ ...entry })); + return newState.filter(entry => entry.id !== id); + }); + }; + + const dismiss = (id: number) => () => { + setMessages(s => { + return s.map(entry => { + return { + ...entry, + visible: entry.id === id ? false : entry.visible, + }; + }); + }); + }; + + return ( + <> + + +
+ {messages.map(({ intent, visible, id }) => ( + + + Descriptive title + Message providing information to the user with actionable insights. Link + + } />} + /> + + ))} +
+ + ); +}; diff --git a/packages/react-components/react-message-bar-preview/stories/MessageBar/index.stories.tsx b/packages/react-components/react-message-bar-preview/stories/MessageBar/index.stories.tsx index 24d5580dba1ebd..4791dd2c83a592 100644 --- a/packages/react-components/react-message-bar-preview/stories/MessageBar/index.stories.tsx +++ b/packages/react-components/react-message-bar-preview/stories/MessageBar/index.stories.tsx @@ -4,8 +4,10 @@ import descriptionMd from './MessageBarDescription.md'; import bestPracticesMd from './MessageBarBestPractices.md'; export { Default } from './Default.stories'; +export { Dismiss } from './Dismiss.stories'; export { Reflow } from './Reflow.stories'; export { ManualLayout } from './ManualLayout.stories'; +export { Animation } from './Animation.stories'; export default { title: 'Preview Components/MessageBar', From 18b81d20deedc22bb1323c1db110d89067983452 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Mon, 2 Oct 2023 14:55:41 +0200 Subject: [PATCH 2/7] Add MessageBarGroup --- .../etc/react-message-bar-preview.api.md | 37 ++++++++++++- .../react-message-bar-preview/package.json | 3 +- .../src/MessageBarGroup.ts | 1 + .../components/MessageBar/MessageBar.types.ts | 8 +-- .../MessageBar/renderMessageBar.tsx | 4 -- .../components/MessageBar/useMessageBar.ts | 17 ++---- .../MessageBar/useMessageBarStyles.styles.ts | 35 +----------- .../MessageBarGroup/MessageBarGroup.test.tsx | 22 ++++++++ .../MessageBarGroup/MessageBarGroup.tsx | 18 +++++++ .../MessageBarGroup/MessageBarGroup.types.ts | 24 +++++++++ .../MessageBarGroup/MessageBarTransition.tsx | 41 ++++++++++++++ .../MessageBarGroup.test.tsx.snap | 13 +++++ .../src/components/MessageBarGroup/index.ts | 5 ++ .../MessageBarGroup/renderMessageBarGroup.tsx | 31 +++++++++++ .../MessageBarGroup/useMessageBarGroup.ts | 47 ++++++++++++++++ .../useMessageBarGroupStyles.styles.ts | 53 +++++++++++++++++++ .../contexts/messageBarTransitionContext.ts | 19 +++++++ .../react-message-bar-preview/src/index.ts | 9 ++++ .../stories/MessageBar/Animation.stories.tsx | 44 ++++----------- .../stories/MessageBar/Dismiss.stories.tsx | 47 +++++----------- 20 files changed, 353 insertions(+), 125 deletions(-) create mode 100644 packages/react-components/react-message-bar-preview/src/MessageBarGroup.ts create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.test.tsx create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.tsx create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.types.ts create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarTransition.tsx create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/__snapshots__/MessageBarGroup.test.tsx.snap create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/index.ts create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/renderMessageBarGroup.tsx create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroup.ts create mode 100644 packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroupStyles.styles.ts create mode 100644 packages/react-components/react-message-bar-preview/src/contexts/messageBarTransitionContext.ts diff --git a/packages/react-components/react-message-bar-preview/etc/react-message-bar-preview.api.md b/packages/react-components/react-message-bar-preview/etc/react-message-bar-preview.api.md index 7fe7c5e7925947..4b38d3f78ee27b 100644 --- a/packages/react-components/react-message-bar-preview/etc/react-message-bar-preview.api.md +++ b/packages/react-components/react-message-bar-preview/etc/react-message-bar-preview.api.md @@ -62,6 +62,30 @@ export type MessageBarContextValue = { layout?: 'multiline' | 'singleline' | 'auto'; }; +// @public +export const MessageBarGroup: ForwardRefComponent; + +// @public (undocumented) +export const messageBarGroupClassNames: SlotClassNames; + +// @public +export type MessageBarGroupProps = ComponentProps & { + children: React_2.ReactElement[] | React_2.ReactElement; + animate?: 'exit-only' | 'both'; +}; + +// @public (undocumented) +export type MessageBarGroupSlots = { + root: Slot<'div'>; +}; + +// @public +export type MessageBarGroupState = ComponentState & Pick & { + enterStyles: string; + exitStyles: string; + children: React_2.ReactElement[]; +}; + // @public export type MessageBarProps = ComponentProps & Pick & { intent?: 'info' | 'success' | 'warning' | 'error'; @@ -74,7 +98,9 @@ export type MessageBarSlots = { }; // @public -export type MessageBarState = ComponentState & Required>; +export type MessageBarState = ComponentState & Required> & { + transitionClassName: string; +}; // @public export const MessageBarTitle: ForwardRefComponent; @@ -102,6 +128,9 @@ export const renderMessageBarActions_unstable: (state: MessageBarActionsState) = // @public export const renderMessageBarBody_unstable: (state: MessageBarBodyState) => JSX.Element; +// @public +export const renderMessageBarGroup_unstable: (state: MessageBarGroupState) => JSX.Element; + // @public export const renderMessageBarTitle_unstable: (state: MessageBarTitleState) => JSX.Element; @@ -123,6 +152,12 @@ export const useMessageBarBodyStyles_unstable: (state: MessageBarBodyState) => M // @public (undocumented) export const useMessageBarContext: () => MessageBarContextValue; +// @public +export const useMessageBarGroup_unstable: (props: MessageBarGroupProps, ref: React_2.Ref) => MessageBarGroupState; + +// @public +export const useMessageBarGroupStyles_unstable: (state: MessageBarGroupState) => MessageBarGroupState; + // @public export const useMessageBarStyles_unstable: (state: MessageBarState) => MessageBarState; diff --git a/packages/react-components/react-message-bar-preview/package.json b/packages/react-components/react-message-bar-preview/package.json index df0c666d8e9e7a..7edc135fdac205 100644 --- a/packages/react-components/react-message-bar-preview/package.json +++ b/packages/react-components/react-message-bar-preview/package.json @@ -39,7 +39,8 @@ "@fluentui/react-theme": "^9.1.14", "@fluentui/react-utilities": "^9.13.5", "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" }, "peerDependencies": { "@types/react": ">=16.8.0 <19.0.0", diff --git a/packages/react-components/react-message-bar-preview/src/MessageBarGroup.ts b/packages/react-components/react-message-bar-preview/src/MessageBarGroup.ts new file mode 100644 index 00000000000000..3515bfed8ae631 --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/MessageBarGroup.ts @@ -0,0 +1 @@ +export * from './components/MessageBarGroup/index'; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.types.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.types.ts index b16a5335f5bcfd..9de1424f0a04f4 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.types.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.types.ts @@ -1,5 +1,4 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import type { MotionState } from '@fluentui/react-motion-preview'; import type { MessageBarContextValue } from '../../contexts/messageBarContext'; export type MessageBarSlots = { @@ -17,15 +16,12 @@ export type MessageBarContextValues = { export type MessageBarProps = ComponentProps & Pick & { intent?: 'info' | 'success' | 'warning' | 'error'; - visible?: boolean; - animate?: 'exit-only' | 'both'; - onDismiss?: () => void; }; /** * State used in rendering MessageBar */ export type MessageBarState = ComponentState & - Required> & { - motionState: MotionState; + Required> & { + transitionClassName: string; }; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBar/renderMessageBar.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBar/renderMessageBar.tsx index 523941c140744f..83aa749e3d1eff 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBar/renderMessageBar.tsx +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBar/renderMessageBar.tsx @@ -11,10 +11,6 @@ import { MessageBarContextProvider } from '../../contexts/messageBarContext'; export const renderMessageBar_unstable = (state: MessageBarState, contexts: MessageBarContextValues) => { assertSlots(state); - if (!state.motionState.canRender) { - return null; - } - return ( diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts index fed37c923100f5..8c201fd698d879 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts @@ -1,9 +1,9 @@ import * as React from 'react'; import { getNativeElementProps, slot, useMergedRefs } from '@fluentui/react-utilities'; -import { useMotion } from '@fluentui/react-motion-preview'; import type { MessageBarProps, MessageBarState } from './MessageBar.types'; import { getIntentIcon } from './getIntentIcon'; import { useMessageBarReflow } from './useMessageBarReflow'; +import { useMessageBarTransitionContext } from '../../contexts/messageBarTransitionContext'; /** * Create the state required to render MessageBar. @@ -15,17 +15,11 @@ import { useMessageBarReflow } from './useMessageBarReflow'; * @param ref - reference to root HTMLElement of MessageBar */ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref): MessageBarState => { - const { layout = 'auto', intent = 'info', visible = true, onDismiss, animate = 'exit-only' } = props; + const { layout = 'auto', intent = 'info' } = props; const autoReflow = layout === 'auto'; const { ref: reflowRef, reflowing } = useMessageBarReflow(autoReflow); const computedLayout = autoReflow ? (reflowing ? 'multiline' : 'singleline') : layout; - const motionState = useMotion(visible, { animateOnFirstMount: true }); - - React.useEffect(() => { - if (motionState.type === 'exited' && onDismiss) { - onDismiss(); - } - }, [motionState.type, onDismiss]); + const transitionClassName = useMessageBarTransitionContext(); return { components: { @@ -34,7 +28,7 @@ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref = { root: 'fui-MessageBar', @@ -22,8 +21,6 @@ const useRootBaseStyles = makeResetStyles({ minHeight: '36px', boxSizing: 'border-box', backgroundColor: tokens.colorNeutralBackground3, - animationFillMode: 'forwards', - animationDuration: tokens.durationNormal, }); const useIconBaseStyles = makeResetStyles({ @@ -86,32 +83,6 @@ const useRootIntentStyles = makeStyles({ }, }); -const useMotionStyles = makeStyles({ - 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 MessageBar slots based on the state */ @@ -121,17 +92,13 @@ export const useMessageBarStyles_unstable = (state: MessageBarState): MessageBar const multilineStyles = useMultilineStyles(); const iconIntentStyles = useIconIntentStyles(); const rootIntentStyles = useRootIntentStyles(); - const motionStyles = useMotionStyles(); - - const shouldExit = (type: MotionType) => ['exiting', 'exited'].includes(type); state.root.className = mergeClasses( messageBarClassNames.root, rootBaseStyles, state.layout === 'multiline' && multilineStyles.rootMultiline, rootIntentStyles[state.intent], - shouldExit(state.motionState.type) && motionStyles.exit, - state.animate === 'both' && !shouldExit(state.motionState.type) && motionStyles.enter, + state.transitionClassName, state.root.className, ); diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.test.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.test.tsx new file mode 100644 index 00000000000000..ae58b08a5637ad --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.test.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { MessageBarGroup } from './MessageBarGroup'; + +describe('MessageBarGroup', () => { + isConformant({ + Component: MessageBarGroup, + displayName: 'MessageBarGroup', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render( + + Default MessageBarGroup + , + ); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.tsx new file mode 100644 index 00000000000000..4dcb40271bf160 --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useMessageBarGroup_unstable } from './useMessageBarGroup'; +import { renderMessageBarGroup_unstable } from './renderMessageBarGroup'; +import { useMessageBarGroupStyles_unstable } from './useMessageBarGroupStyles.styles'; +import type { MessageBarGroupProps } from './MessageBarGroup.types'; + +/** + * MessageBarGroup component - TODO: add more docs + */ +export const MessageBarGroup: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useMessageBarGroup_unstable(props, ref); + + useMessageBarGroupStyles_unstable(state); + return renderMessageBarGroup_unstable(state); +}); + +MessageBarGroup.displayName = 'MessageBarGroup'; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.types.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.types.ts new file mode 100644 index 00000000000000..dc0cbc00b1a751 --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.types.ts @@ -0,0 +1,24 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import * as React from 'react'; + +export type MessageBarGroupSlots = { + root: Slot<'div'>; +}; + +/** + * MessageBarGroup Props + */ +export type MessageBarGroupProps = ComponentProps & { + children: React.ReactElement[] | React.ReactElement; + animate?: 'exit-only' | 'both'; +}; + +/** + * State used in rendering MessageBarGroup + */ +export type MessageBarGroupState = ComponentState & + Pick & { + enterStyles: string; + exitStyles: string; + children: React.ReactElement[]; + }; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarTransition.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarTransition.tsx new file mode 100644 index 00000000000000..20db4f21d64314 --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarTransition.tsx @@ -0,0 +1,41 @@ +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; + } +}; + +/** + * Internal component that controls the animation transition for MessageBar components + * @internal + */ +export const MessageBarTransition: React.FC<{ + children: React.ReactNode; + enterClassName: string; + exitClassName: string; + animate: MessageBarGroupProps['animate']; +}> = ({ children, enterClassName, exitClassName, animate, ...rest }) => { + return ( + + {state => ( + + {children} + + )} + + ); +}; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/__snapshots__/MessageBarGroup.test.tsx.snap b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/__snapshots__/MessageBarGroup.test.tsx.snap new file mode 100644 index 00000000000000..fc1e020af85b30 --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/__snapshots__/MessageBarGroup.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageBarGroup renders a default state 1`] = ` +
+
+ + Default MessageBarGroup + +
+
+`; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/index.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/index.ts new file mode 100644 index 00000000000000..21c912d11b842c --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/index.ts @@ -0,0 +1,5 @@ +export * from './MessageBarGroup'; +export * from './MessageBarGroup.types'; +export * from './renderMessageBarGroup'; +export * from './useMessageBarGroup'; +export * from './useMessageBarGroupStyles.styles'; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/renderMessageBarGroup.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/renderMessageBarGroup.tsx new file mode 100644 index 00000000000000..ddd4c6d29760d5 --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/renderMessageBarGroup.tsx @@ -0,0 +1,31 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import { assertSlots } from '@fluentui/react-utilities'; +import type { MessageBarGroupState, MessageBarGroupSlots } from './MessageBarGroup.types'; +import { TransitionGroup } from 'react-transition-group'; +import { MessageBarTransition } from './MessageBarTransition'; + +/** + * Render the final JSX of MessageBarGroup + */ +export const renderMessageBarGroup_unstable = (state: MessageBarGroupState) => { + assertSlots(state); + + return ( + + + {state.children.map(child => ( + + {child} + + ))} + + + ); +}; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroup.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroup.ts new file mode 100644 index 00000000000000..0ef4cbed1459e9 --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroup.ts @@ -0,0 +1,47 @@ +import * as React from 'react'; +import type { MessageBarGroupProps, MessageBarGroupState } from './MessageBarGroup.types'; +import { getNativeElementProps, slot } from '@fluentui/react-utilities'; + +/** + * Create the state required to render MessageBarGroup. + * + * The returned state can be modified with hooks such as useMessageBarGroupStyles_unstable, + * before being passed to renderMessageBarGroup_unstable. + * + * @param props - props from this instance of MessageBarGroup + * @param ref - reference to root HTMLElement of MessageBarGroup + */ +export const useMessageBarGroup_unstable = ( + props: MessageBarGroupProps, + ref: React.Ref, +): MessageBarGroupState => { + if (process.env.NODE_ENV !== 'production') { + React.Children.forEach(props.children, c => { + if (!React.isValidElement(c) || c.type === React.Fragment) { + throw new Error( + "MessageBarGroup: children must be valid React elements. Please ensure you're not using fragments. ", + ); + } + }); + } + + const children = React.Children.map(props.children ?? [], c => (React.isValidElement(c) ? c : null)); + + return { + components: { + root: 'div', + }, + + root: slot.always( + getNativeElementProps('div', { + ref, + ...props, + }), + { elementType: 'div' }, + ), + children, + animate: props.animate ?? 'exit-only', + enterStyles: '', + exitStyles: '', + }; +}; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroupStyles.styles.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroupStyles.styles.ts new file mode 100644 index 00000000000000..0c1b62059a810c --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroupStyles.styles.ts @@ -0,0 +1,53 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import { tokens } from '@fluentui/react-theme'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { MessageBarGroupSlots, MessageBarGroupState } from './MessageBarGroup.types'; + +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 => { + 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-preview/src/contexts/messageBarTransitionContext.ts b/packages/react-components/react-message-bar-preview/src/contexts/messageBarTransitionContext.ts new file mode 100644 index 00000000000000..273c555eaab95e --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/contexts/messageBarTransitionContext.ts @@ -0,0 +1,19 @@ +import * as React from 'react'; + +const messageBarContext = React.createContext(undefined); + +/** + * @internal + */ +export const messageBarContextDefaultValue = ''; + +/** + * Context to pass animation className to MessageBar components + * @internal + */ +export const MessageBarTransitionContextProvider = messageBarContext.Provider; +/** + * @internal + */ +export const useMessageBarTransitionContext = () => + React.useContext(messageBarContext) ?? messageBarContextDefaultValue; diff --git a/packages/react-components/react-message-bar-preview/src/index.ts b/packages/react-components/react-message-bar-preview/src/index.ts index e4d035efe8584d..03ef9aa77eca80 100644 --- a/packages/react-components/react-message-bar-preview/src/index.ts +++ b/packages/react-components/react-message-bar-preview/src/index.ts @@ -40,3 +40,12 @@ export type { MessageBarBodyProps, MessageBarBodySlots, MessageBarBodyState } fr export { MessageBarContextProvider, useMessageBarContext } from './contexts/messageBarContext'; export type { MessageBarContextValue } from './contexts/messageBarContext'; +export { + MessageBarGroup, + useMessageBarGroupStyles_unstable, + useMessageBarGroup_unstable, + renderMessageBarGroup_unstable, + messageBarGroupClassNames, +} from './MessageBarGroup'; + +export type { MessageBarGroupProps, MessageBarGroupSlots, MessageBarGroupState } from './MessageBarGroup'; diff --git a/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx b/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx index 3315e603232b18..9e9dd7711b4c19 100644 --- a/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx +++ b/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx @@ -6,7 +6,8 @@ import { MessageBarActions, MessageBarTitle, MessageBarBody, - MessageBarProps, + MessageBarGroup, + MessageBarGroupProps, } from '@fluentui/react-message-bar-preview'; const useStyles = makeStyles({ @@ -26,13 +27,12 @@ const intents = ['info', 'warning', 'error', 'success'] as const; interface Entry { intent: (typeof intents)[number]; id: number; - visible: boolean; } export const Animation = () => { const styles = useStyles(); const counterRef = React.useRef(0); - const [animate, setAnimate] = React.useState('exit-only'); + const [animate, setAnimate] = React.useState('exit-only'); const [messages, setMessages] = React.useState([]); const prepend = () => { const intentPos = Math.floor(Math.random() * intents.length); @@ -46,53 +46,29 @@ export const Animation = () => { }; const clear = () => { - setMessages(s => { - return s.map(entry => { - return { - ...entry, - visible: false, - }; - }); - }); + setMessages([]); }; - const onDismiss = (id: number) => () => { + const dismiss = (id: number) => () => { setMessages(s => { const newState = s.map(entry => ({ ...entry })); return newState.filter(entry => entry.id !== id); }); }; - const dismiss = (id: number) => () => { - setMessages(s => { - return s.map(entry => { - return { - ...entry, - visible: entry.id === id ? false : entry.visible, - }; - }); - }); - }; - return ( <> - setAnimate(value as MessageBarProps['animate'])}> + setAnimate(value as MessageBarGroupProps['animate'])}> -
- {messages.map(({ intent, visible, id }) => ( - + + {messages.map(({ intent, id }) => ( + Descriptive title Message providing information to the user with actionable insights. Link @@ -102,7 +78,7 @@ export const Animation = () => { /> ))} -
+ ); }; diff --git a/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx b/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx index ca8b19ba4b74da..967fbd5b18847e 100644 --- a/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx +++ b/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx @@ -1,7 +1,13 @@ import * as React from 'react'; import { Button, Link, makeStyles, shorthands, tokens } from '@fluentui/react-components'; import { DismissRegular } from '@fluentui/react-icons'; -import { MessageBar, MessageBarActions, MessageBarTitle, MessageBarBody } from '@fluentui/react-message-bar-preview'; +import { + MessageBar, + MessageBarActions, + MessageBarTitle, + MessageBarBody, + MessageBarGroup, +} from '@fluentui/react-message-bar-preview'; const useStyles = makeStyles({ container: { @@ -20,7 +26,6 @@ const intents = ['info', 'warning', 'error', 'success'] as const; interface Entry { intent: (typeof intents)[number]; id: number; - visible: boolean; } export const Dismiss = () => { @@ -31,7 +36,6 @@ export const Dismiss = () => { const intentPos = Math.floor(Math.random() * intents.length); const newEntry = { intent: intents[intentPos], - visible: true, id: counterRef.current++, }; @@ -39,47 +43,24 @@ export const Dismiss = () => { }; const clear = () => { - setMessages(s => { - return s.map(entry => { - return { - ...entry, - visible: false, - }; - }); - }); + setMessages([]); }; - const onDismiss = (id: number) => () => { + const dismiss = (id: number) => () => { setMessages(s => { const newState = s.map(entry => ({ ...entry })); return newState.filter(entry => entry.id !== id); }); }; - const dismiss = (id: number) => () => { - setMessages(s => { - return s.map(entry => { - return { - ...entry, - visible: entry.id === id ? false : entry.visible, - }; - }); - }); - }; - + console.log(messages.length); return ( <> -
- {messages.map(({ intent, visible, id }) => ( - + + {messages.map(({ intent, id }) => ( + Descriptive title Message providing information to the user with actionable insights. Link @@ -89,7 +70,7 @@ export const Dismiss = () => { /> ))} -
+ ); }; From f50e4acfb454e8035d1f29bc18ecec92ad1abc2c Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Mon, 2 Oct 2023 14:59:49 +0200 Subject: [PATCH 3/7] remove TODO --- .../src/components/MessageBarGroup/MessageBarGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.tsx index 4dcb40271bf160..7eeaa3baaa364b 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.tsx +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarGroup.tsx @@ -6,7 +6,7 @@ import { useMessageBarGroupStyles_unstable } from './useMessageBarGroupStyles.st import type { MessageBarGroupProps } from './MessageBarGroup.types'; /** - * MessageBarGroup component - TODO: add more docs + * MessageBarGroup component */ export const MessageBarGroup: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useMessageBarGroup_unstable(props, ref); From f967a9d892aecf5ce7f460efde43b71207614d21 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Mon, 2 Oct 2023 15:18:56 +0200 Subject: [PATCH 4/7] update package versions --- .../react-components/react-message-bar-preview/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-components/react-message-bar-preview/package.json b/packages/react-components/react-message-bar-preview/package.json index 7edc135fdac205..1ae1df022bc72d 100644 --- a/packages/react-components/react-message-bar-preview/package.json +++ b/packages/react-components/react-message-bar-preview/package.json @@ -34,8 +34,7 @@ "@fluentui/react-button": "^9.3.44", "@fluentui/react-icons": "^2.0.217", "@fluentui/react-jsx-runtime": "^9.0.12", - "@fluentui/react-motion-preview": "^0.2.8", - "@fluentui/react-shared-contexts": "^9.9.1", + "@fluentui/react-shared-contexts": "^9.9.2", "@fluentui/react-theme": "^9.1.14", "@fluentui/react-utilities": "^9.13.5", "@griffel/react": "^1.5.14", From 132b7b1f6150c824450667590f7eb4a26daf9ecb Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Mon, 2 Oct 2023 15:51:35 +0200 Subject: [PATCH 5/7] address issues --- .../stories/MessageBar/Animation.stories.tsx | 4 +--- .../stories/MessageBar/Dismiss.stories.tsx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx b/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx index 9e9dd7711b4c19..2f0378d969af85 100644 --- a/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx +++ b/packages/react-components/react-message-bar-preview/stories/MessageBar/Animation.stories.tsx @@ -38,7 +38,6 @@ export const Animation = () => { const intentPos = Math.floor(Math.random() * intents.length); const newEntry = { intent: intents[intentPos], - visible: true, id: counterRef.current++, }; @@ -51,8 +50,7 @@ export const Animation = () => { const dismiss = (id: number) => () => { setMessages(s => { - const newState = s.map(entry => ({ ...entry })); - return newState.filter(entry => entry.id !== id); + return s.filter(entry => entry.id !== id); }); }; diff --git a/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx b/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx index 967fbd5b18847e..da6f35d2bf44b6 100644 --- a/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx +++ b/packages/react-components/react-message-bar-preview/stories/MessageBar/Dismiss.stories.tsx @@ -48,12 +48,10 @@ export const Dismiss = () => { const dismiss = (id: number) => () => { setMessages(s => { - const newState = s.map(entry => ({ ...entry })); - return newState.filter(entry => entry.id !== id); + return s.filter(entry => entry.id !== id); }); }; - console.log(messages.length); return ( <> From f0e715132de1a405b2829832c409d83b5bc5ff80 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Mon, 2 Oct 2023 16:10:11 +0200 Subject: [PATCH 6/7] avoid findDOMNode --- .../MessageBarGroup/MessageBarTransition.tsx | 14 +++++++++++--- .../MessageBarGroup/useMessageBarGroup.ts | 6 ++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarTransition.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarTransition.tsx index 20db4f21d64314..72282994c564ec 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarTransition.tsx +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/MessageBarTransition.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Transition, TransitionStatus } from 'react-transition-group'; import { MessageBarTransitionContextProvider } from '../../contexts/messageBarTransitionContext'; import { MessageBarGroupProps } from './MessageBarGroup.types'; +import { useMergedRefs } from '@fluentui/react-utilities'; const getClassName = ( status: TransitionStatus, @@ -24,16 +25,23 @@ const getClassName = ( * @internal */ export const MessageBarTransition: React.FC<{ - children: React.ReactNode; + children: React.ReactElement; enterClassName: string; exitClassName: string; animate: MessageBarGroupProps['animate']; }> = ({ children, enterClassName, exitClassName, animate, ...rest }) => { + const ref = React.useRef(null); + + // Clone element to set nodeRef on and avoid findDOMNode + // Here we are already sure the children is one ReactElement + // https://github.com/reactjs/react-transition-group/blob/2989b5b87b4b4d1001f21c8efa503049ffb4fe8d/src/Transition.js#L404-L412 + const clone = React.cloneElement(children, { ...children.props, ref: useMergedRefs(children.props.ref, ref) }); + return ( - + {state => ( - {children} + {clone} )} diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroup.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroup.ts index 0ef4cbed1459e9..6adb9280d95b10 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroup.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarGroup/useMessageBarGroup.ts @@ -19,13 +19,15 @@ export const useMessageBarGroup_unstable = ( React.Children.forEach(props.children, c => { if (!React.isValidElement(c) || c.type === React.Fragment) { throw new Error( - "MessageBarGroup: children must be valid React elements. Please ensure you're not using fragments. ", + "MessageBarGroup: children must be valid MessageBar components. Please ensure you're not using fragments. ", ); } }); } - const children = React.Children.map(props.children ?? [], c => (React.isValidElement(c) ? c : null)); + const children = React.Children.map(props.children ?? [], c => + React.isValidElement(c) && c.type !== React.Fragment ? c : null, + ).filter(Boolean); return { components: { From 7884909684813e5be7c5bb2fea9481f2ac4dadbd Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Mon, 2 Oct 2023 18:37:53 +0200 Subject: [PATCH 7/7] pass nodeRef through context --- .../components/MessageBar/useMessageBar.ts | 4 +- .../MessageBarGroup/MessageBarTransition.tsx | 44 ++++++++++++++----- .../contexts/messageBarTransitionContext.ts | 16 +++++-- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts index 8c201fd698d879..c296460ad70060 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBar/useMessageBar.ts @@ -19,7 +19,7 @@ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref = ({ children, enterClassName, exitClassName, animate, ...rest }) => { - const ref = React.useRef(null); - - // Clone element to set nodeRef on and avoid findDOMNode - // Here we are already sure the children is one ReactElement - // https://github.com/reactjs/react-transition-group/blob/2989b5b87b4b4d1001f21c8efa503049ffb4fe8d/src/Transition.js#L404-L412 - const clone = React.cloneElement(children, { ...children.props, ref: useMergedRefs(children.props.ref, ref) }); + const nodeRef = React.useRef(null); return ( - + {state => ( - - {clone} - + + {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-preview/src/contexts/messageBarTransitionContext.ts b/packages/react-components/react-message-bar-preview/src/contexts/messageBarTransitionContext.ts index 273c555eaab95e..0a023b4b0e46ca 100644 --- a/packages/react-components/react-message-bar-preview/src/contexts/messageBarTransitionContext.ts +++ b/packages/react-components/react-message-bar-preview/src/contexts/messageBarTransitionContext.ts @@ -1,19 +1,27 @@ import * as React from 'react'; -const messageBarContext = React.createContext(undefined); +export type MessageBarTransitionContextValue = { + className: string; + nodeRef: React.Ref; +}; + +const messageBarTransitionContext = React.createContext(undefined); /** * @internal */ -export const messageBarContextDefaultValue = ''; +export const messageBarTransitionContextDefaultValue: MessageBarTransitionContextValue = { + className: '', + nodeRef: React.createRef(), +}; /** * Context to pass animation className to MessageBar components * @internal */ -export const MessageBarTransitionContextProvider = messageBarContext.Provider; +export const MessageBarTransitionContextProvider = messageBarTransitionContext.Provider; /** * @internal */ export const useMessageBarTransitionContext = () => - React.useContext(messageBarContext) ?? messageBarContextDefaultValue; + React.useContext(messageBarTransitionContext) ?? messageBarTransitionContextDefaultValue;