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 4b38d3f78ee27b..beb5a9bb592f52 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 @@ -6,6 +6,7 @@ /// +import type { ButtonContextValue } from '@fluentui/react-button'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; @@ -59,7 +60,9 @@ export const MessageBarContextProvider: React_2.Provider; + bodyRef: React_2.MutableRefObject; }; // @public @@ -87,8 +90,9 @@ export type MessageBarGroupState = ComponentState & Pick & Pick & { +export type MessageBarProps = ComponentProps & Pick, 'layout'> & { intent?: 'info' | 'success' | 'warning' | 'error'; + politeness?: 'assertive' | 'polite'; }; // @public (undocumented) @@ -100,6 +104,8 @@ export type MessageBarSlots = { // @public export type MessageBarState = ComponentState & Required> & { transitionClassName: string; + actionsRef: React_2.MutableRefObject; + bodyRef: React_2.MutableRefObject; }; // @public @@ -123,7 +129,7 @@ export type MessageBarTitleState = ComponentState; export const renderMessageBar_unstable: (state: MessageBarState, contexts: MessageBarContextValues) => JSX.Element; // @public -export const renderMessageBarActions_unstable: (state: MessageBarActionsState) => JSX.Element; +export const renderMessageBarActions_unstable: (state: MessageBarActionsState, contexts: MessageBarActionsContextValues) => JSX.Element; // @public export const renderMessageBarBody_unstable: (state: MessageBarBodyState) => JSX.Element; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.test.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.test.tsx index 6a6badfa9ec047..54012eec2b44dc 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.test.tsx +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBar/MessageBar.test.tsx @@ -2,6 +2,10 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { MessageBar } from './MessageBar'; +import { AnnounceProvider_unstable } from '@fluentui/react-shared-contexts'; +import { MessageBarBody } from '../MessageBarBody/MessageBarBody'; +import { MessageBarTitle } from '../MessageBarTitle/MessageBarTitle'; +import { MessageBarActions } from '../MessageBarActions/MessageBarActions'; describe('MessageBar', () => { beforeAll(() => { @@ -33,10 +37,52 @@ describe('MessageBar', () => { }, }); - // TODO add more tests here, and create visual regression tests in /apps/vr-tests - it('renders a default state', () => { const result = render(Default MessageBar); expect(result.container).toMatchSnapshot(); }); + + it.each([ + ['assertive', 'error'] as const, + ['assertive', 'warning'] as const, + ['assertive', 'success'] as const, + ['polite', 'info'] as const, + ])('should announce %s with %s intent', (politeness, intent) => { + const announce = jest.fn(); + render( + + + + TitleBody + + + , + ); + + expect(announce).toHaveBeenCalledTimes(1); + expect(announce).toHaveBeenCalledWith('TitleBody', { + alert: politeness === 'assertive', + polite: politeness === 'polite', + }); + }); + + it('should announce actions', () => { + const announce = jest.fn(); + render( + + + + TitleBody + + Container action}> + + + + + , + ); + + expect(announce).toHaveBeenCalledTimes(1); + expect(announce).toHaveBeenCalledWith('TitleBody,Action 1Action 2', expect.anything()); + }); }); 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 9de1424f0a04f4..383262e1199e48 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 type { MessageBarContextValue } from '../../contexts/messageBarContext'; +import * as React from 'react'; export type MessageBarSlots = { root: Slot<'div'>; @@ -14,8 +15,9 @@ export type MessageBarContextValues = { * MessageBar Props */ export type MessageBarProps = ComponentProps & - Pick & { + Pick, 'layout'> & { intent?: 'info' | 'success' | 'warning' | 'error'; + politeness?: 'assertive' | 'polite'; }; /** @@ -24,4 +26,6 @@ export type MessageBarProps = ComponentProps & export type MessageBarState = ComponentState & Required> & { transitionClassName: string; + actionsRef: React.MutableRefObject; + bodyRef: React.MutableRefObject; }; 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 c296460ad70060..c5c384369237b3 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 { useAnnounce_unstable } from '@fluentui/react-shared-contexts'; import type { MessageBarProps, MessageBarState } from './MessageBar.types'; import { getIntentIcon } from './getIntentIcon'; import { useMessageBarReflow } from './useMessageBarReflow'; @@ -15,11 +16,23 @@ import { useMessageBarTransitionContext } from '../../contexts/messageBarTransit * @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', politeness } = props; + const computedPolitness = politeness ?? intent === 'info' ? 'polite' : 'assertive'; const autoReflow = layout === 'auto'; const { ref: reflowRef, reflowing } = useMessageBarReflow(autoReflow); const computedLayout = autoReflow ? (reflowing ? 'multiline' : 'singleline') : layout; const { className: transitionClassName, nodeRef } = useMessageBarTransitionContext(); + const actionsRef = React.useRef(null); + const bodyRef = React.useRef(null); + const { announce } = useAnnounce_unstable(); + + React.useEffect(() => { + const bodyMessage = bodyRef.current?.textContent; + const actionsMessage = actionsRef.current?.textContent; + + const message = [bodyMessage, actionsMessage].filter(Boolean).join(','); + announce(message, { polite: computedPolitness === 'polite', alert: computedPolitness === 'assertive' }); + }, [bodyRef, actionsRef, announce, computedPolitness]); return { components: { @@ -42,5 +55,7 @@ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref ({ layout, + actionsRef, + bodyRef, }), - [layout], + [layout, actionsRef, bodyRef], ); return { diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/MessageBarActions.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/MessageBarActions.tsx index 1e20b5f1f8e83c..75694b0b150cd4 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/MessageBarActions.tsx +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/MessageBarActions.tsx @@ -4,6 +4,7 @@ import { useMessageBarActions_unstable } from './useMessageBarActions'; import { renderMessageBarActions_unstable } from './renderMessageBarActions'; import { useMessageBarActionsStyles_unstable } from './useMessageBarActionsStyles.styles'; import type { MessageBarActionsProps } from './MessageBarActions.types'; +import { useMessageBarActionsContextValue_unstable } from './useMessageBarActionsContextValues'; /** * MessageBarActions component @@ -12,7 +13,7 @@ export const MessageBarActions: ForwardRefComponent = Re const state = useMessageBarActions_unstable(props, ref); useMessageBarActionsStyles_unstable(state); - return renderMessageBarActions_unstable(state); + return renderMessageBarActions_unstable(state, useMessageBarActionsContextValue_unstable()); }); MessageBarActions.displayName = 'MessageBarActions'; diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/MessageBarActions.types.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/MessageBarActions.types.ts index c8c04ee199908c..112c3a27e5f191 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/MessageBarActions.types.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/MessageBarActions.types.ts @@ -1,11 +1,16 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { MessageBarContextValue } from '../../contexts/messageBarContext'; +import type { ButtonContextValue } from '@fluentui/react-button'; +import type { MessageBarContextValue } from '../../contexts/messageBarContext'; export type MessageBarActionsSlots = { root: Slot<'div'>; containerAction?: Slot<'div'>; }; +export type MessageBarActionsContextValues = { + button: ButtonContextValue; +}; + /** * MessageBarActions Props */ diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/renderMessageBarActions.tsx b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/renderMessageBarActions.tsx index 15ddc4bf084e33..ec0fad640d8b69 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/renderMessageBarActions.tsx +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/renderMessageBarActions.tsx @@ -2,17 +2,24 @@ /** @jsxImportSource @fluentui/react-jsx-runtime */ import { assertSlots } from '@fluentui/react-utilities'; -import type { MessageBarActionsState, MessageBarActionsSlots } from './MessageBarActions.types'; +import type { + MessageBarActionsState, + MessageBarActionsSlots, + MessageBarActionsContextValues, +} from './MessageBarActions.types'; import { ButtonContextProvider } from '@fluentui/react-button'; /** * Render the final JSX of MessageBarActions */ -export const renderMessageBarActions_unstable = (state: MessageBarActionsState) => { +export const renderMessageBarActions_unstable = ( + state: MessageBarActionsState, + contexts: MessageBarActionsContextValues, +) => { assertSlots(state); if (state.layout === 'multiline') { return ( - + {state.containerAction && } @@ -20,7 +27,7 @@ export const renderMessageBarActions_unstable = (state: MessageBarActionsState) } return ( - + {state.containerAction && } diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/useMessageBarActions.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/useMessageBarActions.ts index 5ccf97a36a7188..eedcb87dd7ab74 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/useMessageBarActions.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/useMessageBarActions.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { getNativeElementProps, slot } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot, useMergedRefs } from '@fluentui/react-utilities'; import type { MessageBarActionsProps, MessageBarActionsState } from './MessageBarActions.types'; import { useMessageBarContext } from '../../contexts/messageBarContext'; @@ -16,7 +16,7 @@ export const useMessageBarActions_unstable = ( props: MessageBarActionsProps, ref: React.Ref, ): MessageBarActionsState => { - const { layout = 'singleline' } = useMessageBarContext(); + const { layout = 'singleline', actionsRef } = useMessageBarContext(); return { components: { root: 'div', @@ -25,7 +25,7 @@ export const useMessageBarActions_unstable = ( containerAction: slot.optional(props.containerAction, { renderByDefault: false, elementType: 'div' }), root: slot.always( getNativeElementProps('div', { - ref, + ref: useMergedRefs(ref, actionsRef), ...props, }), { elementType: 'div' }, diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/useMessageBarActionsContextValues.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/useMessageBarActionsContextValues.ts new file mode 100644 index 00000000000000..94f37f0a57d24a --- /dev/null +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarActions/useMessageBarActionsContextValues.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { MessageBarActionsContextValues } from './MessageBarActions.types'; + +export function useMessageBarActionsContextValue_unstable(): MessageBarActionsContextValues { + const buttonContext = React.useMemo( + () => ({ + size: 'small' as const, + }), + [], + ); + + return { + button: buttonContext, + }; +} diff --git a/packages/react-components/react-message-bar-preview/src/components/MessageBarBody/useMessageBarBody.ts b/packages/react-components/react-message-bar-preview/src/components/MessageBarBody/useMessageBarBody.ts index ed99cf2f18a9bb..cb91a13aebe212 100644 --- a/packages/react-components/react-message-bar-preview/src/components/MessageBarBody/useMessageBarBody.ts +++ b/packages/react-components/react-message-bar-preview/src/components/MessageBarBody/useMessageBarBody.ts @@ -1,6 +1,7 @@ import * as React from 'react'; -import { getNativeElementProps, slot } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot, useMergedRefs } from '@fluentui/react-utilities'; import type { MessageBarBodyProps, MessageBarBodyState } from './MessageBarBody.types'; +import { useMessageBarContext } from '../../contexts/messageBarContext'; /** * Create the state required to render MessageBarBody. @@ -15,13 +16,14 @@ export const useMessageBarBody_unstable = ( props: MessageBarBodyProps, ref: React.Ref, ): MessageBarBodyState => { + const { bodyRef } = useMessageBarContext(); return { components: { root: 'div', }, root: slot.always( getNativeElementProps('div', { - ref, + ref: useMergedRefs(ref, bodyRef), ...props, }), { elementType: 'div' }, diff --git a/packages/react-components/react-message-bar-preview/src/contexts/messageBarContext.ts b/packages/react-components/react-message-bar-preview/src/contexts/messageBarContext.ts index 59c72989b360d1..f2bb40b5a84905 100644 --- a/packages/react-components/react-message-bar-preview/src/contexts/messageBarContext.ts +++ b/packages/react-components/react-message-bar-preview/src/contexts/messageBarContext.ts @@ -1,12 +1,16 @@ import * as React from 'react'; export type MessageBarContextValue = { - layout?: 'multiline' | 'singleline' | 'auto'; + layout: 'multiline' | 'singleline' | 'auto'; + actionsRef: React.MutableRefObject; + bodyRef: React.MutableRefObject; }; const messageBarContext = React.createContext(undefined); export const messageBarContextDefaultValue: MessageBarContextValue = { layout: 'singleline', + actionsRef: React.createRef(), + bodyRef: React.createRef(), }; export const MessageBarContextProvider = messageBarContext.Provider;