diff --git a/change/@fluentui-react-aria-c3dd5226-825f-4e31-b74b-d6005d0b2d36.json b/change/@fluentui-react-aria-c3dd5226-825f-4e31-b74b-d6005d0b2d36.json new file mode 100644 index 0000000000000..5101dce64d66d --- /dev/null +++ b/change/@fluentui-react-aria-c3dd5226-825f-4e31-b74b-d6005d0b2d36.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add AriaLiveAnnouncer component", + "packageName": "@fluentui/react-aria", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-components-7adb099c-7955-4239-8baf-18294e26ab67.json b/change/@fluentui-react-components-7adb099c-7955-4239-8baf-18294e26ab67.json new file mode 100644 index 0000000000000..ffd75722d03e4 --- /dev/null +++ b/change/@fluentui-react-components-7adb099c-7955-4239-8baf-18294e26ab67.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add AriaLiveAnnouncer component", + "packageName": "@fluentui/react-components", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-aria/etc/react-aria.api.md b/packages/react-components/react-aria/etc/react-aria.api.md index 976d981eeb0e9..c3be4701e4d7e 100644 --- a/packages/react-components/react-aria/etc/react-aria.api.md +++ b/packages/react-components/react-aria/etc/react-aria.api.md @@ -4,6 +4,7 @@ ```ts +import type { AnnounceContextValue } from '@fluentui/react-shared-contexts'; import type { ExtractSlotProps } from '@fluentui/react-utilities'; import * as React_2 from 'react'; import type { ResolveShorthandFunction } from '@fluentui/react-utilities'; @@ -63,6 +64,23 @@ export type ARIAButtonSlotProps = // @public (undocumented) export type ARIAButtonType = 'button' | 'a' | 'div'; +// @public +export const AriaLiveAnnouncer: React_2.FC; + +// @public (undocumented) +export type AriaLiveAnnouncerProps = { + children?: React_2.ReactNode; +}; + +// @public (undocumented) +export type AriaLiveAnnouncerState = { + announce: AriaLiveAnnounceFn; + children?: React_2.ReactNode; +}; + +// @public (undocumented) +export const renderAriaLiveAnnouncer_unstable: (state: AriaLiveAnnouncerState, contextValues: AriaLiveAnnouncerContextValues) => JSX.Element; + // @public (undocumented) export function useActiveDescendant(options: ActiveDescendantOptions): UseActiveDescendantReturn; @@ -72,6 +90,12 @@ export function useARIAButtonProps; +// @public (undocumented) +export const useAriaLiveAnnouncer_unstable: (props: AriaLiveAnnouncerProps) => AriaLiveAnnouncerState; + +// @public (undocumented) +export function useAriaLiveAnnouncerContextValues_unstable(state: AriaLiveAnnouncerState): AriaLiveAnnouncerContextValues; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.test.tsx b/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.test.tsx new file mode 100644 index 0000000000000..c15b5846fc7e8 --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.test.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react'; +import * as React from 'react'; + +import { AriaLiveAnnouncer } from './AriaLiveAnnouncer'; + +describe('AriaLiveAnnouncer', () => { + it('renders a default state', () => { + const result = render(Default AriaLive); + + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.tsx b/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.tsx new file mode 100644 index 0000000000000..f006e49497158 --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +import type { AriaLiveAnnouncerProps } from './AriaLiveAnnouncer.types'; +import { renderAriaLiveAnnouncer_unstable } from './renderAriaLiveAnnouncer'; +import { useAriaLiveAnnouncer_unstable } from './useAriaLiveAnnouncer'; +import { useAriaLiveAnnouncerContextValues_unstable } from './useAriaLiveAnnouncerContextValues'; + +/** + * A sample implementation of a component that manages aria live announcements. + */ +export const AriaLiveAnnouncer: React.FC = props => { + const state = useAriaLiveAnnouncer_unstable(props); + const contextValues = useAriaLiveAnnouncerContextValues_unstable(state); + + return renderAriaLiveAnnouncer_unstable(state, contextValues); +}; + +AriaLiveAnnouncer.displayName = 'AriaLiveAnnouncer'; diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.types.ts b/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.types.ts new file mode 100644 index 0000000000000..cfcf0c9c3e4a1 --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/AriaLiveAnnouncer.types.ts @@ -0,0 +1,26 @@ +import type { AnnounceContextValue } from '@fluentui/react-shared-contexts'; +import * as React from 'react'; + +export type AriaLiveAnnounceFn = AnnounceContextValue['announce']; + +export type AriaLiveMessage = { + message: string; + + createdAt: number; + + priority: number; + batchId?: string; +}; + +export type AriaLiveAnnouncerProps = { + children?: React.ReactNode; +}; + +export type AriaLiveAnnouncerState = { + announce: AriaLiveAnnounceFn; + children?: React.ReactNode; +}; + +export type AriaLiveAnnouncerContextValues = { + announce: { announce: AriaLiveAnnounceFn }; +}; diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/__snapshots__/AriaLiveAnnouncer.test.tsx.snap b/packages/react-components/react-aria/src/AriaLiveAnnouncer/__snapshots__/AriaLiveAnnouncer.test.tsx.snap new file mode 100644 index 0000000000000..2e56624df9d0e --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/__snapshots__/AriaLiveAnnouncer.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AriaLiveAnnouncer renders a default state 1`] = ` +
+ Default AriaLive +
+`; diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/index.ts b/packages/react-components/react-aria/src/AriaLiveAnnouncer/index.ts new file mode 100644 index 0000000000000..93614bb109612 --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/index.ts @@ -0,0 +1,5 @@ +export { AriaLiveAnnouncer } from './AriaLiveAnnouncer'; +export type { AriaLiveAnnouncerProps, AriaLiveAnnouncerState } from './AriaLiveAnnouncer.types'; +export { renderAriaLiveAnnouncer_unstable } from './renderAriaLiveAnnouncer'; +export { useAriaLiveAnnouncer_unstable } from './useAriaLiveAnnouncer'; +export { useAriaLiveAnnouncerContextValues_unstable } from './useAriaLiveAnnouncerContextValues'; diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/renderAriaLiveAnnouncer.tsx b/packages/react-components/react-aria/src/AriaLiveAnnouncer/renderAriaLiveAnnouncer.tsx new file mode 100644 index 0000000000000..98b2700701494 --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/renderAriaLiveAnnouncer.tsx @@ -0,0 +1,13 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import { AnnounceProvider } from '@fluentui/react-shared-contexts'; + +import type { AriaLiveAnnouncerContextValues, AriaLiveAnnouncerState } from './AriaLiveAnnouncer.types'; + +export const renderAriaLiveAnnouncer_unstable = ( + state: AriaLiveAnnouncerState, + contextValues: AriaLiveAnnouncerContextValues, +) => { + return {state.children}; +}; diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.test.tsx b/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.test.tsx new file mode 100644 index 0000000000000..e9fb4577be23b --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.test.tsx @@ -0,0 +1,131 @@ +import { Provider_unstable as Provider } from '@fluentui/react-shared-contexts'; +import { renderHook } from '@testing-library/react-hooks'; +import * as React from 'react'; + +import { useAriaLiveAnnouncer_unstable as useAriaLiveAnnouncer } from './useAriaLiveAnnouncer'; + +const ANNOUNCE_TIMEOUT = 500; +const ANNOUNCE_SUFFIX = '. '; + +describe('useAriaLiveAnnouncer', () => { + describe('announce', () => { + let liveRegionNode: HTMLDivElement | null; + let innerNode: HTMLSpanElement | null; + + const targetDocument = { + createElement: (type: string) => (type === 'div' ? liveRegionNode : innerNode), + body: { + append: jest.fn(), + }, + } as unknown as Document; + const ContextWrapper: React.FC = props => { + return {props.children}; + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + + liveRegionNode = document.createElement('div'); + innerNode = document.createElement('span'); + }); + + afterEach(() => { + jest.useRealTimers(); + + liveRegionNode = null; + innerNode = null; + }); + + it('should append a "div" to ', () => { + const append = jest.spyOn(document.body, 'append'); + const { rerender } = renderHook(() => useAriaLiveAnnouncer({})); + + expect(append).toBeCalledTimes(1); + expect(append).toBeCalledWith(expect.any(HTMLDivElement)); + + // Ensure that the same element is not appended again + rerender(); + expect(append).toBeCalledTimes(1); + }); + + it('should update the announcement message', () => { + const { result } = renderHook(() => useAriaLiveAnnouncer({}), { wrapper: ContextWrapper }); + + result.current.announce('message loaded'); + expect(innerNode?.innerText).toBe('message loaded' + ANNOUNCE_SUFFIX); + }); + + it('should announce frequent messages in batches', () => { + const { result } = renderHook(() => useAriaLiveAnnouncer({}), { wrapper: ContextWrapper }); + + result.current.announce('message loaded'); + jest.advanceTimersByTime(100); + + result.current.announce('message unloaded'); + result.current.announce('message reloaded'); + expect(innerNode?.innerText).toBe('message loaded' + ANNOUNCE_SUFFIX); + + jest.advanceTimersByTime(ANNOUNCE_TIMEOUT); + expect(innerNode?.innerText).toBe('message unloaded' + ANNOUNCE_SUFFIX + 'message reloaded' + ANNOUNCE_SUFFIX); + }); + + it('should only update the last of batched messages', () => { + const { result } = renderHook(() => useAriaLiveAnnouncer({}), { wrapper: ContextWrapper }); + + result.current.announce('message loaded', { batchId: 'test' }); + jest.advanceTimersByTime(100); + + result.current.announce('message reloaded', { batchId: 'test' }); + jest.advanceTimersByTime(100); + + result.current.announce('message revolutions', { batchId: 'test' }); + jest.advanceTimersByTime(ANNOUNCE_TIMEOUT); + expect(innerNode?.innerText).toBe('message revolutions' + ANNOUNCE_SUFFIX); + }); + + it('should handle multiple batches', () => { + const { result } = renderHook(() => useAriaLiveAnnouncer({}), { wrapper: ContextWrapper }); + + result.current.announce('message loaded', { batchId: 'test' }); + jest.advanceTimersByTime(100); + + result.current.announce('message reloaded', { batchId: 'test' }); + jest.advanceTimersByTime(100); + + result.current.announce('message revolutions', { batchId: 'test2' }); + jest.advanceTimersByTime(100); + + result.current.announce('message resurrections', { batchId: 'test2' }); + jest.advanceTimersByTime(ANNOUNCE_TIMEOUT); + + expect(innerNode?.innerText).toBe( + 'message reloaded' + ANNOUNCE_SUFFIX + 'message resurrections' + ANNOUNCE_SUFFIX, + ); + }); + + it('should announce batched and unbatched messages', () => { + const { result } = renderHook(() => useAriaLiveAnnouncer({}), { wrapper: ContextWrapper }); + + result.current.announce('message loaded', { batchId: 'test' }); + jest.advanceTimersByTime(100); + + result.current.announce('message reloaded', { batchId: 'test' }); + jest.advanceTimersByTime(100); + + result.current.announce('message revolutions'); + jest.advanceTimersByTime(ANNOUNCE_TIMEOUT); + expect(innerNode?.innerText).toBe('message reloaded' + ANNOUNCE_SUFFIX + 'message revolutions' + ANNOUNCE_SUFFIX); + }); + + it('should clear the announcement message after a delay', async () => { + const { result } = renderHook(() => useAriaLiveAnnouncer({}), { wrapper: ContextWrapper }); + + result.current.announce('message resurrections'); + expect(innerNode?.innerText).toBe('message resurrections' + ANNOUNCE_SUFFIX); + + jest.advanceTimersByTime(ANNOUNCE_TIMEOUT); + expect(liveRegionNode?.innerText).toBe(''); + }); + }); +}); diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.ts b/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.ts new file mode 100644 index 0000000000000..3337ceb642095 --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.ts @@ -0,0 +1,158 @@ +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +import { createPriorityQueue, useTimeout } from '@fluentui/react-utilities'; +import * as React from 'react'; + +import type { + AriaLiveAnnounceFn, + AriaLiveAnnouncerState, + AriaLiveAnnouncerProps, + AriaLiveMessage, +} from './AriaLiveAnnouncer.types'; + +/** The duration the message needs to be in present in DOM for screen readers to register a change and announce */ +const MESSAGE_DURATION = 500; + +const VISUALLY_HIDDEN_STYLES = { + clip: 'rect(0px, 0px, 0px, 0px)', + height: '1px', + margin: '-1px', + width: '1px', + position: 'absolute', + overflow: 'hidden', + textWrap: 'nowrap', +}; + +export const useAriaLiveAnnouncer_unstable = (props: AriaLiveAnnouncerProps): AriaLiveAnnouncerState => { + const { targetDocument } = useFluent(); + + const timeoutRef = React.useRef(undefined); + const [setAnnounceTimeout, clearAnnounceTimeout] = useTimeout(); + + const elementRef = React.useRef(null); + + const order = React.useRef(0); + + // investigate alert implementation later + // const [alertList, setAlertList] = React.useState([]); + + const batchMessages = React.useRef<{ batchId: string; message: AriaLiveMessage }[]>([]); + + const [messageQueue] = React.useState(() => + createPriorityQueue((a, b) => { + if (a.priority !== b.priority) { + return b.priority - a.priority; + } + + return a.createdAt - b.createdAt; + }), + ); + + const queueMessage = React.useCallback(() => { + if (timeoutRef.current || !elementRef.current) { + return; + } + + const runCycle = () => { + if (!elementRef.current) { + return; + } + + if (targetDocument && messageQueue.peek()) { + // need a wrapping element for Narrator/Edge, which currently does not pick up text-only live region changes + // consistently + // if this is fixed, we can set textContent to the string directly + + const wrappingEl = targetDocument.createElement('span'); + + wrappingEl.innerText = messageQueue + .all() + .filter(msg => msg.message.trim().length > 0) + .reduce((prevText, currMsg) => prevText + currMsg.message + '. ', ''); + + elementRef.current.innerText = ''; + elementRef.current.appendChild(wrappingEl); + + messageQueue.clear(); + batchMessages.current = []; + + // begin new cycle to clear (or update) messages + timeoutRef.current = setAnnounceTimeout(() => { + runCycle(); + }, MESSAGE_DURATION); + } else { + elementRef.current.textContent = ''; + clearAnnounceTimeout(); + + timeoutRef.current = undefined; + } + }; + + runCycle(); + }, [clearAnnounceTimeout, messageQueue, setAnnounceTimeout, targetDocument]); + + const announce: AriaLiveAnnounceFn = React.useMemo( + () => + (message, options = {}) => { + const { alert = false, priority = 0, batchId } = options; + + // check if message is an alert + if (alert) { + // TODO: alert implementation + // setAlertList([...alertList, message]); + } + + const liveMessage: AriaLiveMessage = { + message, + createdAt: order.current++, + priority, + batchId, + }; + + // check if batchId exists + if (batchId) { + // update associated msg if it does + const batchMessage = batchMessages.current.find(msg => msg.batchId === batchId); + + if (batchMessage) { + // replace existing message in queue + messageQueue.remove(batchMessage.message); + + // update list of existing batchIds w/ most recent message + batchMessage.message = liveMessage; + } else { + // update list of existing batchIds, add new if doesn't already exist + batchMessages.current = [...batchMessages.current, { batchId, message: liveMessage }]; + } + } + + // add new message + messageQueue.enqueue(liveMessage); + queueMessage(); + }, + [messageQueue, queueMessage], + ); + + React.useEffect(() => { + if (!targetDocument) { + return; + } + + const element = targetDocument.createElement('div'); + element.setAttribute('aria-live', 'assertive'); + + Object.assign(element.style, VISUALLY_HIDDEN_STYLES); + targetDocument.body.append(element); + + elementRef.current = element; + + return () => { + element.remove(); + elementRef.current = null; + }; + }, [targetDocument]); + + return { + announce, + children: props.children, + }; +}; diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncerContextValues.ts b/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncerContextValues.ts new file mode 100644 index 0000000000000..988f6385a267e --- /dev/null +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncerContextValues.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; +import type { AriaLiveAnnouncerContextValues, AriaLiveAnnouncerState } from './AriaLiveAnnouncer.types'; + +export function useAriaLiveAnnouncerContextValues_unstable( + state: AriaLiveAnnouncerState, +): AriaLiveAnnouncerContextValues { + const { announce } = state; + + return React.useMemo(() => ({ announce: { announce } }), [announce]); +} diff --git a/packages/react-components/react-aria/src/index.ts b/packages/react-components/react-aria/src/index.ts index 2cfe971e0f45b..83a761b04a660 100644 --- a/packages/react-components/react-aria/src/index.ts +++ b/packages/react-components/react-aria/src/index.ts @@ -14,3 +14,11 @@ export type { ARIAButtonElementIntersection, ARIAButtonAlteredProps, } from './button/index'; + +export { + AriaLiveAnnouncer, + renderAriaLiveAnnouncer_unstable, + useAriaLiveAnnouncer_unstable, + useAriaLiveAnnouncerContextValues_unstable, +} from './AriaLiveAnnouncer/index'; +export type { AriaLiveAnnouncerProps, AriaLiveAnnouncerState } from './AriaLiveAnnouncer/index'; diff --git a/packages/react-components/react-aria/stories/AriaLiveAnnouncer/AriaLiveAnnouncerDefault.stories.tsx b/packages/react-components/react-aria/stories/AriaLiveAnnouncer/AriaLiveAnnouncerDefault.stories.tsx new file mode 100644 index 0000000000000..a816913755030 --- /dev/null +++ b/packages/react-components/react-aria/stories/AriaLiveAnnouncer/AriaLiveAnnouncerDefault.stories.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { + AriaLiveAnnouncer, + Button, + Field, + Input, + makeStyles, + shorthands, + tokens, + useAnnounce, +} from '@fluentui/react-components'; + +const useClasses = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + ...shorthands.gap('8px'), + ...shorthands.padding('8px'), + + ...shorthands.border('1px', 'solid', tokens.colorNeutralStroke1), + ...shorthands.borderRadius('4px'), + }, +}); + +const AnnouncePlayground: React.FC = () => { + const classes = useClasses(); + const { announce } = useAnnounce(); + + const [message, setMessage] = React.useState('Hello world'); + + return ( +
+ + setMessage(data.value)} value={message} /> + + +
+ ); +}; + +export const Default = () => { + return ( + +

+ This example shows how to use the useAnnounce() hook connected with `AriaLiveAnnouncer` component. + To check results, open the screen reader and click the button below. +

+ + +
+ ); +}; diff --git a/packages/react-components/react-aria/stories/AriaLiveAnnouncer/AriaLiveAnnouncerDescription.md b/packages/react-components/react-aria/stories/AriaLiveAnnouncer/AriaLiveAnnouncerDescription.md new file mode 100644 index 0000000000000..4aad0b82ca1de --- /dev/null +++ b/packages/react-components/react-aria/stories/AriaLiveAnnouncer/AriaLiveAnnouncerDescription.md @@ -0,0 +1,5 @@ +`AriaLiveAnnouncer` provides a sample implementation of an `aria-live` region that can be used to announce messages to screen readers. + +It injects announcements into the DOM, and also exposes a function (to its children in a React tree) that can be used to announce messages. It's designed to be used with `useAnnounce()` hook. + +To learn more about `aria-live` regions, see [MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). diff --git a/packages/react-components/react-aria/stories/AriaLiveAnnouncer/index.stories.tsx b/packages/react-components/react-aria/stories/AriaLiveAnnouncer/index.stories.tsx new file mode 100644 index 0000000000000..b9fc6dac42807 --- /dev/null +++ b/packages/react-components/react-aria/stories/AriaLiveAnnouncer/index.stories.tsx @@ -0,0 +1,18 @@ +import { AriaLiveAnnouncer } from '@fluentui/react-components'; +import type { Meta } from '@storybook/react'; + +import descriptionMd from './AriaLiveAnnouncerDescription.md'; + +export { Default } from './AriaLiveAnnouncerDefault.stories'; + +export default { + title: 'Utilities/ARIA live/AriaLiveAnnouncer', + component: AriaLiveAnnouncer, + parameters: { + docs: { + description: { + component: [descriptionMd].join('\n'), + }, + }, + }, +} as Meta; diff --git a/packages/react-components/react-components/etc/react-components.api.md b/packages/react-components/react-components/etc/react-components.api.md index 6e4aa3ca9a428..39899cf84cea2 100644 --- a/packages/react-components/react-components/etc/react-components.api.md +++ b/packages/react-components/react-components/etc/react-components.api.md @@ -45,6 +45,9 @@ import { AccordionToggleEvent } from '@fluentui/react-accordion'; import { AccordionToggleEventHandler } from '@fluentui/react-accordion'; import { AnnounceContextValue } from '@fluentui/react-shared-contexts'; import { AnnounceProvider } from '@fluentui/react-shared-contexts'; +import { AriaLiveAnnouncer } from '@fluentui/react-aria'; +import { AriaLiveAnnouncerProps } from '@fluentui/react-aria'; +import { AriaLiveAnnouncerState } from '@fluentui/react-aria'; import { arrowHeights } from '@fluentui/react-popover'; import { assertSlots } from '@fluentui/react-utilities'; import { Avatar } from '@fluentui/react-avatar'; @@ -635,6 +638,7 @@ import { renderAccordion_unstable } from '@fluentui/react-accordion'; import { renderAccordionHeader_unstable } from '@fluentui/react-accordion'; import { renderAccordionItem_unstable } from '@fluentui/react-accordion'; import { renderAccordionPanel_unstable } from '@fluentui/react-accordion'; +import { renderAriaLiveAnnouncer_unstable } from '@fluentui/react-aria'; import { renderAvatar_unstable } from '@fluentui/react-avatar'; import { renderAvatarGroup_unstable } from '@fluentui/react-avatar'; import { renderAvatarGroupItem_unstable } from '@fluentui/react-avatar'; @@ -1091,6 +1095,8 @@ import { useAccordionPanel_unstable } from '@fluentui/react-accordion'; import { useAccordionPanelStyles_unstable } from '@fluentui/react-accordion'; import { useAccordionStyles_unstable } from '@fluentui/react-accordion'; import { useAnnounce } from '@fluentui/react-shared-contexts'; +import { useAriaLiveAnnouncer_unstable } from '@fluentui/react-aria'; +import { useAriaLiveAnnouncerContextValues_unstable } from '@fluentui/react-aria'; import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { UseArrowNavigationGroupOptions } from '@fluentui/react-tabster'; import { useAvatar_unstable } from '@fluentui/react-avatar'; @@ -1507,6 +1513,12 @@ export { AnnounceContextValue } export { AnnounceProvider } +export { AriaLiveAnnouncer } + +export { AriaLiveAnnouncerProps } + +export { AriaLiveAnnouncerState } + export { arrowHeights } export { assertSlots } @@ -2687,6 +2699,8 @@ export { renderAccordionItem_unstable } export { renderAccordionPanel_unstable } +export { renderAriaLiveAnnouncer_unstable } + export { renderAvatar_unstable } export { renderAvatarGroup_unstable } @@ -3599,6 +3613,10 @@ export { useAccordionStyles_unstable } export { useAnnounce } +export { useAriaLiveAnnouncer_unstable } + +export { useAriaLiveAnnouncerContextValues_unstable } + export { useArrowNavigationGroup } export { UseArrowNavigationGroupOptions } diff --git a/packages/react-components/react-components/package.json b/packages/react-components/react-components/package.json index e11c3f1c48d73..bc7fa72278b52 100644 --- a/packages/react-components/react-components/package.json +++ b/packages/react-components/react-components/package.json @@ -82,7 +82,8 @@ "@griffel/react": "^1.5.14", "@swc/helpers": "^0.5.1", "@fluentui/react-message-bar": "^9.0.16", - "@fluentui/react-breadcrumb": "^9.0.11" + "@fluentui/react-breadcrumb": "^9.0.11", + "@fluentui/react-aria": "^9.7.3" }, "peerDependencies": { "@types/react": ">=16.14.0 <19.0.0", diff --git a/packages/react-components/react-components/src/index.ts b/packages/react-components/react-components/src/index.ts index 9ca95fc5a1ba8..a03697da252ad 100644 --- a/packages/react-components/react-components/src/index.ts +++ b/packages/react-components/react-components/src/index.ts @@ -1563,3 +1563,11 @@ export type { BreadcrumbButtonState, BreadcrumbContextValues, } from '@fluentui/react-breadcrumb'; + +export { + AriaLiveAnnouncer, + renderAriaLiveAnnouncer_unstable, + useAriaLiveAnnouncer_unstable, + useAriaLiveAnnouncerContextValues_unstable, +} from '@fluentui/react-aria'; +export type { AriaLiveAnnouncerProps, AriaLiveAnnouncerState } from '@fluentui/react-aria';