diff --git a/change/@fluentui-react-components-ab114c24-9a9d-42e2-8834-4d2af7fba463.json b/change/@fluentui-react-components-ab114c24-9a9d-42e2-8834-4d2af7fba463.json new file mode 100644 index 00000000000000..f192ed75526b8b --- /dev/null +++ b/change/@fluentui-react-components-ab114c24-9a9d-42e2-8834-4d2af7fba463.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: export AnnounceProvider & useAnnounce()", + "packageName": "@fluentui/react-components", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-shared-contexts-9fc4180f-4a72-41c5-91aa-49564ac7d936.json b/change/@fluentui-react-shared-contexts-9fc4180f-4a72-41c5-91aa-49564ac7d936.json new file mode 100644 index 00000000000000..b5d5be16809da3 --- /dev/null +++ b/change/@fluentui-react-shared-contexts-9fc4180f-4a72-41c5-91aa-49564ac7d936.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: export AnnounceProvider & useAnnounce()", + "packageName": "@fluentui/react-shared-contexts", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} 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 c21816f9e6b559..5efd142700e9d6 100644 --- a/packages/react-components/react-components/etc/react-components.api.md +++ b/packages/react-components/react-components/etc/react-components.api.md @@ -43,6 +43,8 @@ import { AccordionState } from '@fluentui/react-accordion'; import { AccordionToggleData } from '@fluentui/react-accordion'; 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 { arrowHeights } from '@fluentui/react-popover'; import { assertSlots } from '@fluentui/react-utilities'; import { Avatar } from '@fluentui/react-avatar'; @@ -1088,6 +1090,7 @@ import { useAccordionItemStyles_unstable } from '@fluentui/react-accordion'; 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 { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { UseArrowNavigationGroupOptions } from '@fluentui/react-tabster'; import { useAvatar_unstable } from '@fluentui/react-avatar'; @@ -1499,6 +1502,10 @@ export { AccordionToggleEvent } export { AccordionToggleEventHandler } +export { AnnounceContextValue } + +export { AnnounceProvider } + export { arrowHeights } export { assertSlots } @@ -3589,6 +3596,8 @@ export { useAccordionPanelStyles_unstable } export { useAccordionStyles_unstable } +export { useAnnounce } + export { useArrowNavigationGroup } export { UseArrowNavigationGroupOptions } diff --git a/packages/react-components/react-components/src/index.ts b/packages/react-components/react-components/src/index.ts index 67bbf8058cf448..95c2638c672bbc 100644 --- a/packages/react-components/react-components/src/index.ts +++ b/packages/react-components/react-components/src/index.ts @@ -92,12 +92,15 @@ export type { TypographyStyles, } from '@fluentui/react-theme'; export { + AnnounceProvider, + PortalMountNodeProvider, + useAnnounce, useFluent_unstable as useFluent, + usePortalMountNode, useTooltipVisibility_unstable as useTooltipVisibility, useThemeClassName_unstable as useThemeClassName, - PortalMountNodeProvider, - usePortalMountNode, } from '@fluentui/react-shared-contexts'; +export type { AnnounceContextValue } from '@fluentui/react-shared-contexts'; export { // getNativeElementProps is deprecated but removing it would be a breaking change // eslint-disable-next-line deprecation/deprecation diff --git a/packages/react-components/react-shared-contexts/.storybook/main.js b/packages/react-components/react-shared-contexts/.storybook/main.js new file mode 100644 index 00000000000000..26536b61b387f6 --- /dev/null +++ b/packages/react-components/react-shared-contexts/.storybook/main.js @@ -0,0 +1,14 @@ +const rootMain = require('../../../../.storybook/main'); + +module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../stories/**/*.stories.mdx', '../stories/**/index.stories.@(ts|tsx)'], + addons: [...rootMain.addons], + webpackFinal: (config, options) => { + const localConfig = { ...rootMain.webpackFinal(config, options) }; + + // add your own webpack tweaks if needed + + return localConfig; + }, +}); diff --git a/packages/react-components/react-shared-contexts/.storybook/preview.js b/packages/react-components/react-shared-contexts/.storybook/preview.js new file mode 100644 index 00000000000000..1939500a3d18c7 --- /dev/null +++ b/packages/react-components/react-shared-contexts/.storybook/preview.js @@ -0,0 +1,7 @@ +import * as rootPreview from '../../../../.storybook/preview'; + +/** @type {typeof rootPreview.decorators} */ +export const decorators = [...rootPreview.decorators]; + +/** @type {typeof rootPreview.parameters} */ +export const parameters = { ...rootPreview.parameters }; diff --git a/packages/react-components/react-shared-contexts/.storybook/tsconfig.json b/packages/react-components/react-shared-contexts/.storybook/tsconfig.json new file mode 100644 index 00000000000000..ea89218a3d916f --- /dev/null +++ b/packages/react-components/react-shared-contexts/.storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "", + "allowJs": true, + "checkJs": true, + "types": ["static-assets", "environment", "storybook__addons"] + }, + "include": ["../stories/**/*.stories.ts", "../stories/**/*.stories.tsx", "*.js"] +} diff --git a/packages/react-components/react-shared-contexts/etc/react-shared-contexts.api.md b/packages/react-components/react-shared-contexts/etc/react-shared-contexts.api.md index 505f48f84b4be8..16886126374958 100644 --- a/packages/react-components/react-shared-contexts/etc/react-shared-contexts.api.md +++ b/packages/react-components/react-shared-contexts/etc/react-shared-contexts.api.md @@ -7,13 +7,25 @@ import * as React_2 from 'react'; import type { Theme } from '@fluentui/react-theme'; -// @internal (undocumented) -export type AnnounceContextValue_unstable = { +// @public (undocumented) +type AnnounceContextValue = { announce: (message: string, options?: AnnounceOptions) => void; }; +export { AnnounceContextValue } +export { AnnounceContextValue as AnnounceContextValue_unstable } -// @internal (undocumented) -export const AnnounceProvider_unstable: React_2.Provider; +// @public +export type AnnounceOptions = { + alert?: boolean; + batchId?: string; + polite?: boolean; + priority?: number; +}; + +// @public (undocumented) +const AnnounceProvider: React_2.Provider; +export { AnnounceProvider } +export { AnnounceProvider as AnnounceProvider_unstable } // @internal (undocumented) export type BackgroundAppearanceContextValue = 'inverted' | undefined; @@ -441,10 +453,10 @@ export type TooltipVisibilityContextValue_unstable = { // @internal (undocumented) export const TooltipVisibilityProvider_unstable: React_2.Provider; -// Warning: (ae-incompatible-release-tags) The symbol "useAnnounce" is marked as @public, but its signature references "AnnounceContextValue_unstable" which is marked as @internal -// -// @public (undocumented) -export function useAnnounce_unstable(): AnnounceContextValue_unstable; +// @public +function useAnnounce(): AnnounceContextValue; +export { useAnnounce } +export { useAnnounce as useAnnounce_unstable } // Warning: (ae-incompatible-release-tags) The symbol "useBackgroundAppearance" is marked as @public, but its signature references "BackgroundAppearanceContextValue" which is marked as @internal // diff --git a/packages/react-components/react-shared-contexts/package.json b/packages/react-components/react-shared-contexts/package.json index 28963098f1180c..d56de6854e2b2f 100644 --- a/packages/react-components/react-shared-contexts/package.json +++ b/packages/react-components/react-shared-contexts/package.json @@ -20,7 +20,9 @@ "test": "jest --passWithNoTests", "type-check": "tsc -b tsconfig.json", "generate-api": "just-scripts generate-api", - "test-ssr": "test-ssr \"./stories/**/*.stories.tsx\"" + "test-ssr": "test-ssr \"./stories/**/*.stories.tsx\"", + "storybook": "start-storybook", + "start": "yarn storybook" }, "devDependencies": { "@fluentui/eslint-plugin": "*", diff --git a/packages/react-components/react-shared-contexts/src/AnnounceContext/AnnounceContext.ts b/packages/react-components/react-shared-contexts/src/AnnounceContext/AnnounceContext.ts index b78db49fb9a936..dd018bb7e5e022 100644 --- a/packages/react-components/react-shared-contexts/src/AnnounceContext/AnnounceContext.ts +++ b/packages/react-components/react-shared-contexts/src/AnnounceContext/AnnounceContext.ts @@ -1,15 +1,26 @@ import * as React from 'react'; +/** + * Defines options for a message to be announced. + */ export type AnnounceOptions = { alert?: boolean; + + /** + * A unique identifier for the message. If a message with the same id is already announced, it will be replaced. + */ batchId?: string; + + /** + * Indicates that the message announcement can be interrupted by another message and will be announced only + * user is idle. + */ polite?: boolean; + + /** Defines the priority of the message. Higher priority messages will be announced first. */ priority?: number; }; -/** - * @internal - */ export type AnnounceContextValue = { announce: (message: string, options?: AnnounceOptions) => void; }; @@ -19,11 +30,11 @@ export type AnnounceContextValue = { */ const AnnounceContext = React.createContext(undefined); -/** - * @internal - */ export const AnnounceProvider = AnnounceContext.Provider; +/** + * Returns a function that can be used to announce messages to screen readers. + */ export function useAnnounce(): AnnounceContextValue { return React.useContext(AnnounceContext) ?? { announce: () => undefined }; } diff --git a/packages/react-components/react-shared-contexts/src/index.ts b/packages/react-components/react-shared-contexts/src/index.ts index b4c3bc40d4b6aa..8f1d5d0ec12308 100644 --- a/packages/react-components/react-shared-contexts/src/index.ts +++ b/packages/react-components/react-shared-contexts/src/index.ts @@ -34,5 +34,17 @@ export type { BackgroundAppearanceContextValue } from './BackgroundAppearanceCon export { PortalMountNodeProvider, usePortalMountNode } from './PortalMountNodeContext'; -export { AnnounceProvider as AnnounceProvider_unstable, useAnnounce as useAnnounce_unstable } from './AnnounceContext'; -export type { AnnounceContextValue as AnnounceContextValue_unstable } from './AnnounceContext'; +export { + AnnounceProvider, + /** @deprecated Use AnnounceProvider instead. */ + AnnounceProvider as AnnounceProvider_unstable, + useAnnounce, + /** @deprecated Use useAnnounce instead. */ + useAnnounce as useAnnounce_unstable, +} from './AnnounceContext'; +export type { + AnnounceContextValue, + /** @deprecated Use AnnounceContextValue instead. */ + AnnounceContextValue as AnnounceContextValue_unstable, + AnnounceOptions, +} from './AnnounceContext'; diff --git a/packages/react-components/react-shared-contexts/stories/UseAnnouce/UseAnnounceDefault.stories.tsx b/packages/react-components/react-shared-contexts/stories/UseAnnouce/UseAnnounceDefault.stories.tsx new file mode 100644 index 00000000000000..fa395b5f865788 --- /dev/null +++ b/packages/react-components/react-shared-contexts/stories/UseAnnouce/UseAnnounceDefault.stories.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { + AnnounceProvider, + Button, + Divider, + Field, + Input, + Radio, + RadioGroup, + useAnnounce, +} from '@fluentui/react-components'; +import type { AnnounceContextValue } from '@fluentui/react-components'; + +const AnnouncePlayground: React.FC = () => { + const { announce } = useAnnounce(); + + const [message, setMessage] = React.useState('Hello world'); + const [messageType, setMessageType] = React.useState<'polite' | 'assertive'>('polite'); + + return ( + <> + + setMessage(data.value)} value={message} /> + + + setMessageType(data.value as 'polite' | 'assertive')} value={messageType}> + + + + + + + + ); +}; + +export const Default = () => { + const announce: AnnounceContextValue['announce'] = React.useCallback((message, options) => { + alert(`Announced {polite: ${String(options?.polite ?? false)}}: ${message}`); + }, []); + const value: AnnounceContextValue = React.useMemo(() => ({ announce }), [announce]); + + return ( + +

+ This example shows how to use the useAnnounce() hook, however it does not implement `aria-live` + regions. +

+ + + +
+ ); +}; diff --git a/packages/react-components/react-shared-contexts/stories/UseAnnouce/UseAnnounceDescription.md b/packages/react-components/react-shared-contexts/stories/UseAnnouce/UseAnnounceDescription.md new file mode 100644 index 00000000000000..3433056a7fea5c --- /dev/null +++ b/packages/react-components/react-shared-contexts/stories/UseAnnouce/UseAnnounceDescription.md @@ -0,0 +1,60 @@ +`useAnnounce()` is a React hook that provides a function that can be used to announce messages to screen readers. + +**Note:** This hook requires an aria-live announcer implementation that is configured through the ``. Define this context near the top level of your application. + +## useAnnounce + +`useAnnounce(message, options)` + +- `message` `[string]` is a message to announce +- `options` is an optional options object + - `batchId` `[string]` is a unique identifier for the message. If a message with the same id is already announced, it will be replaced. + - `polite` `[boolean]` indicates that the message announcement can be interrupted by another message and will be announced only user is idle. + - `priority` `[number]` defines the priority of the message. Higher priority messages will be announced first. + +#### Example + +```tsx +import { useAnnounce } from '@fluentui/react-components'; + +function Example() { + const { announce } = useAnnounce(); + + return ; +} +``` + +## AnnounceProvider + +`` is a React component that allows to provide `announce()` function implementation that will be consumed by `useAnnounce()`. + +#### Example + +```tsx +import { AnnounceProvider, useAnnounce } from '@fluentui/react-components'; + +function AnnounceConsumer() { + const { announce } = useAnnounce(); + + // ... + // component that triggers announcement +} + +function Announcer(props) { + const announce = message => { + // ... + // implementation of announcement + }; + const contextValue = React.useMemo(() => ({ announce }), [announce]); + + return {props.children}; +} + +function App() { + return ( + + + + ); +} +``` diff --git a/packages/react-components/react-shared-contexts/stories/UseAnnouce/index.stories.tsx b/packages/react-components/react-shared-contexts/stories/UseAnnouce/index.stories.tsx new file mode 100644 index 00000000000000..6ee9101fb6048a --- /dev/null +++ b/packages/react-components/react-shared-contexts/stories/UseAnnouce/index.stories.tsx @@ -0,0 +1,15 @@ +import descriptionMd from './UseAnnounceDescription.md'; + +export { Default } from './UseAnnounceDefault.stories'; + +export default { + title: 'Utilities/ARIA live/useAnnounce', + component: null, + parameters: { + docs: { + description: { + component: [descriptionMd].join('\n'), + }, + }, + }, +}; diff --git a/packages/react-components/react-shared-contexts/tsconfig.json b/packages/react-components/react-shared-contexts/tsconfig.json index 12ca516af1c5b2..1941a041d46c19 100644 --- a/packages/react-components/react-shared-contexts/tsconfig.json +++ b/packages/react-components/react-shared-contexts/tsconfig.json @@ -17,6 +17,9 @@ }, { "path": "./tsconfig.spec.json" + }, + { + "path": "./.storybook/tsconfig.json" } ] } diff --git a/packages/react-components/react-shared-contexts/tsconfig.lib.json b/packages/react-components/react-shared-contexts/tsconfig.lib.json index b2da24eff1b32f..008c602dc19d24 100644 --- a/packages/react-components/react-shared-contexts/tsconfig.lib.json +++ b/packages/react-components/react-shared-contexts/tsconfig.lib.json @@ -9,6 +9,6 @@ "inlineSources": true, "types": ["static-assets", "environment"] }, - "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.test.ts", "**/*.test.tsx"], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.test.ts", "**/*.test.tsx", "**/*.stories.ts", "**/*.stories.tsx"], "include": ["./src/**/*.ts", "./src/**/*.tsx"] }