Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: export AnnounceProvider & useAnnounce()",
"packageName": "@fluentui/react-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: export AnnounceProvider & useAnnounce()",
"packageName": "@fluentui/react-shared-contexts",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -1499,6 +1502,10 @@ export { AccordionToggleEvent }

export { AccordionToggleEventHandler }

export { AnnounceContextValue }

export { AnnounceProvider }

export { arrowHeights }

export { assertSlots }
Expand Down Expand Up @@ -3589,6 +3596,8 @@ export { useAccordionPanelStyles_unstable }

export { useAccordionStyles_unstable }

export { useAnnounce }

export { useArrowNavigationGroup }

export { UseArrowNavigationGroupOptions }
Expand Down
7 changes: 5 additions & 2 deletions packages/react-components/react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const rootMain = require('../../../../.storybook/main');

module.exports = /** @type {Omit<import('../../../../.storybook/main'), 'typescript'|'babel'>} */ ({
...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;
},
});
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
@@ -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"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnnounceContextValue_unstable | undefined>;
// @public
export type AnnounceOptions = {
alert?: boolean;
batchId?: string;
polite?: boolean;
priority?: number;
};

// @public (undocumented)
const AnnounceProvider: React_2.Provider<AnnounceContextValue | undefined>;
export { AnnounceProvider }
export { AnnounceProvider as AnnounceProvider_unstable }

// @internal (undocumented)
export type BackgroundAppearanceContextValue = 'inverted' | undefined;
Expand Down Expand Up @@ -441,10 +453,10 @@ export type TooltipVisibilityContextValue_unstable = {
// @internal (undocumented)
export const TooltipVisibilityProvider_unstable: React_2.Provider<TooltipVisibilityContextValue_unstable>;

// 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
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": "*",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
Expand All @@ -19,11 +30,11 @@ export type AnnounceContextValue = {
*/
const AnnounceContext = React.createContext<AnnounceContextValue | undefined>(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 };
}
16 changes: 14 additions & 2 deletions packages/react-components/react-shared-contexts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Field label="A message for annoucement">
<Input onChange={(ev, data) => setMessage(data.value)} value={message} />
</Field>
<Field label="Message type">
<RadioGroup onChange={(ev, data) => setMessageType(data.value as 'polite' | 'assertive')} value={messageType}>
<Radio label="assertive" value="assertive" />
<Radio label="polite" value="polite" />
</RadioGroup>
</Field>

<Button
onClick={() => {
announce(message, {
polite: messageType === 'polite',
});
}}
>
Announce message
</Button>
</>
);
};

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 (
<AnnounceProvider value={value}>
<p>
This example shows how to use the <code>useAnnounce()</code> hook, however it does not implement `aria-live`
regions.
</p>

<Divider />
<AnnouncePlayground />
</AnnounceProvider>
);
};
Original file line number Diff line number Diff line change
@@ -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 `<AnnounceProvider />`. 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 <button onClick={() => announce('Hello world!', { polite: true })}>Announce</button>;
}
```

## AnnounceProvider

`<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 <AnnounceProvider value={contextValue}>{props.children}</AnnounceProvider>;
}

function App() {
return (
<AnnounceProvider>
<AnnounceConsumer />
</AnnounceProvider>
);
}
```
Original file line number Diff line number Diff line change
@@ -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'),
},
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./.storybook/tsconfig.json"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}