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
Expand Up @@ -101,6 +101,9 @@ export type ToastId = string;
// @public (undocumented)
export type ToastOffset = Partial<Record<ToastPosition, ToastOffsetObject>> | ToastOffsetObject;

// @public (undocumented)
export type ToastPoliteness = 'assertive' | 'polite';

// @public (undocumented)
export type ToastPosition = 'top-end' | 'top-start' | 'bottom-end' | 'bottom-start';

Expand Down
2 changes: 1 addition & 1 deletion packages/react-components/react-toast/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { useToastController } from './state';
export type { ToastPosition, ToastId, ToastOffset } from './state';
export type { ToastPosition, ToastId, ToastOffset, ToastPoliteness } from './state';

export { ToastTrigger } from './ToastTrigger';
export type { ToastTriggerChildProps, ToastTriggerProps, ToastTriggerState } from './ToastTrigger';
Expand Down
48 changes: 47 additions & 1 deletion packages/react-components/react-toast/src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,51 @@ export type ToastId = string;
export type ToasterId = string;

export type ToastPosition = 'top-end' | 'top-start' | 'bottom-end' | 'bottom-start';
export type ToastPoliteness = 'assertive' | 'polite';

export interface ToastOptions<TData = object> {
/**
* Uniquely identifies a toast, used for update and dismiss operations
*/
toastId: ToastId;
/**
* The position the toast should render to
*/
position: ToastPosition;
/**
* Toast content
*/
content: unknown;
/**
* Auto dismiss timeout in milliseconds
* @default 3000
*/
timeout: number;
/**
* Toast timeout pauses while focus is on another window
* @default false
*/
pauseOnWindowBlur: boolean;
/**
* Toast timeout pauses while user cursor is on the toast
* @default false
*/
pauseOnHover: boolean;
/**
* Toast belongs to a specific toaster
*/
toasterId: ToasterId | undefined;
/**
* Higher priority toasts will be rendered before lower priority toasts
*/
priority: number;
politeness: 'assertive' | 'polite';
/**
* Used to determine [aria-live](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) narration
*/
politeness: ToastPoliteness;
/**
* Additional data that needs to be passed to the toast
*/
data: TData;
}

Expand All @@ -34,9 +68,21 @@ export interface ToasterOptions
}

export interface Toast<TData = object> extends ToastOptions<TData> {
/**
* Determines the visiblity of a toast
*/
close: () => void;
/**
* Removes a toast completely
*/
remove: () => void;
/**
* A number used to track updates immutably
*/
updateId: number;
/**
* Used to determine default priority when the user does not set one
*/
dispatchedAt: number;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { DispatchToastOptions, ToastId, ToasterId, UpdateToastOptions } from './

const noop = () => undefined;

/**
* @param toasterId - If an id is provided all imperative methods control that specific toaster
* @returns Imperative methods to control toasts
*/
export function useToastController(toasterId?: ToasterId) {
const { targetDocument } = useFluent();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export class Toaster {

const remove = () => {
this.toasts.delete(toastId);
if (this.queue.peek()) {
if (this.visibleToasts.size < this.limit && this.queue.peek()) {
const nextToast = this.queue.dequeue();
this.toasts.set(nextToast.toastId, nextToast);
this.visibleToasts.add(nextToast.toastId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Toaster, useToastController, Toast, ToastTitle, ToasterProps } from '@fluentui/react-toast';
import { useId, Link, makeStyles, shorthands } from '@fluentui/react-components';
import { Toaster, useToastController, Toast, ToastTitle, ToasterProps, ToastPoliteness } from '@fluentui/react-toast';
import { useId, makeStyles, shorthands, Button, Field, RadioGroup, Radio } from '@fluentui/react-components';

const useStyles = makeStyles({
visuallyHidden: {
Expand All @@ -20,26 +20,17 @@ export const CustomAnnounce = () => {
const styles = useStyles();
const [alert, setAlert] = React.useState('');
const [status, setStatus] = React.useState('');
const [politeness, setPoliteness] = React.useState<ToastPoliteness>('polite');
const toasterId = useId('toaster');
const { dispatchToast } = useToastController(toasterId);
const dispatchAlert = () =>
const notify = () =>
dispatchToast(
<Toast>
<ToastTitle intent="success" action={<Link>Undo</Link>}>
Assertive toast {counter++}
<ToastTitle intent="success">
{politeness === 'polite' ? 'Polite' : 'Assertive'} toast {counter++}
</ToastTitle>
</Toast>,
{ politeness: 'assertive' },
);

const dispatchStatus = () =>
dispatchToast(
<Toast>
<ToastTitle intent="success" action={<Link>Undo</Link>}>
Polite toast {counter++}
</ToastTitle>
</Toast>,
{ politeness: 'polite' },
{ politeness },
);

const announce: ToasterProps['announce'] = (msg, options) => {
Expand All @@ -54,9 +45,27 @@ export const CustomAnnounce = () => {
<div role="alert" className={styles.visuallyHidden}>
{alert}
</div>
<Field label="Toast politeness">
<RadioGroup value={politeness} onChange={(e, data) => setPoliteness(data.value as ToastPoliteness)}>
<Radio label="Polite" value="polite" />
<Radio label="Assertive" value="assertive" />
</RadioGroup>
</Field>
<br />
<Toaster announce={announce} toasterId={toasterId} />
<button onClick={dispatchAlert}>Dispatch assertive</button>
<button onClick={dispatchStatus}>Dispatch polite</button>
<Button onClick={notify}>Make toast</Button>
</>
);
};

CustomAnnounce.parameters = {
docs: {
description: {
story: [
'The `Toaster` manages an [aria-live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions)',
"internally so that toasts are announced to screen readers on render. It's possible to opt-out of this default",
'behaviour by providing a custom `announce` callback.',
].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
import * as React from 'react';
import { Toaster, useToastController, ToastTitle, Toast } from '@fluentui/react-toast';
import { useId } from '@fluentui/react-components';
import { Toaster, useToastController, ToastTitle, Toast, ToastTrigger } from '@fluentui/react-toast';
import { useId, Button, Link, SpinButton, Field } from '@fluentui/react-components';

export const CustomTimeout = () => {
const [timeout, setDismissTimeout] = React.useState(1000);
const toasterId = useId('toaster');
const { dispatchToast } = useToastController(toasterId);
const notify = () =>
dispatchToast(
<Toast>
<ToastTitle intent="info">Custom timeout 1000ms</ToastTitle>
<ToastTitle
action={
<ToastTrigger>
<Link>Dismiss</Link>
</ToastTrigger>
}
intent="info"
>
{timeout >= 0 ? `Custom timeout ${timeout}ms` : `Dismiss manually`}
</ToastTitle>
</Toast>,
{ timeout: 1000 },
{ timeout },
);

return (
<>
<Field label="Timeout" hint="Timeout is in milliseconds">
<SpinButton
value={timeout}
onChange={(e, data) => {
if (data.value) {
setDismissTimeout(data.value);
} else if (data.displayValue !== undefined) {
const newValue = parseFloat(data.displayValue);
if (!Number.isNaN(newValue)) {
setDismissTimeout(newValue);
}
}
}}
/>
</Field>
<br />
<Toaster toasterId={toasterId} />
<button onClick={notify}>Make toast</button>
<Button onClick={notify}>Make toast</Button>
</>
);
};

CustomTimeout.parameters = {
docs: {
description: {
story: [
'The timeout of toasts can be customized in milliseconds. Using a negative timeout value results in the toast',
'never being auto-dismissed.',
].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Toaster, useToastController, Toast, ToastTitle, ToastBody, ToastFooter } from '@fluentui/react-toast';
import { useId, Link } from '@fluentui/react-components';
import { useId, Link, Button } from '@fluentui/react-components';

export const Default = () => {
const toasterId = useId('toaster');
Expand All @@ -22,7 +22,7 @@ export const Default = () => {
return (
<>
<Toaster toasterId={toasterId} />
<button onClick={notify}>Make toast</button>
<Button onClick={notify}>Make toast</Button>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Toaster, useToastController, ToastTitle, Toast } from '@fluentui/react-toast';
import { useId } from '@fluentui/react-components';
import { useId, Button } from '@fluentui/react-components';

export const DefaultToastOptions = () => {
const toasterId = useId('toaster');
Expand All @@ -15,7 +15,18 @@ export const DefaultToastOptions = () => {
return (
<>
<Toaster toasterId={toasterId} position="top-end" pauseOnHover pauseOnWindowBlur timeout={1000} />
<button onClick={notify}>Make toast</button>
<Button onClick={notify}>Make toast</Button>
</>
);
};

DefaultToastOptions.parameters = {
docs: {
description: {
story: [
'Default options for all toasts can be configured on the `Toaster`.',
'These options are only defaults and can be overriden using `dispatchToast',
].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Toaster, useToastController, ToastTitle, Toast } from '@fluentui/react-toast';
import { useId } from '@fluentui/react-components';
import { useId, Button } from '@fluentui/react-components';

export const DismissAll = () => {
const toasterId = useId('toaster');
Expand All @@ -16,8 +16,16 @@ export const DismissAll = () => {
return (
<>
<Toaster toasterId={toasterId} />
<button onClick={notify}>Make toast</button>
<button onClick={dismissAll}>Dismiss all</button>
<Button onClick={notify}>Make toast</Button>
<Button onClick={dismissAll}>Dismiss all toasts</Button>
</>
);
};

DismissAll.parameters = {
docs: {
description: {
story: ['The `dismissAllToasts imperative API will dismiss all rendered Toasts.'].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Toaster, useToastController, ToastTitle, Toast } from '@fluentui/react-toast';
import { useId } from '@fluentui/react-components';
import { useId, Button } from '@fluentui/react-components';

export const DismissToast = () => {
const toasterId = useId('toaster');
Expand All @@ -18,8 +18,21 @@ export const DismissToast = () => {
return (
<>
<Toaster toasterId={toasterId} />
<button onClick={notify}>Make toast</button>
<button onClick={dismiss}>Dismiss toast</button>
<Button onClick={notify}>Make toast</Button>
<Button onClick={dismiss}>Dismiss toast</Button>
</>
);
};

DismissToast.parameters = {
docs: {
description: {
story: [
'Toasts can be dismissed imperatively using the `dismissToast` API. In order to imperatively dismiss a ',
"Toast, it's necessary to dispatch it with a user provided id. You can use the id to dismiss the toast.",
"**Don't** use this API to dismiss toats when clicking on an action inside the toast, use the `ToastTrigger`",
'instead.',
].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Toaster, useToastController, ToastTitle, ToastTrigger, Toast } from '@fluentui/react-toast';
import { useId, Link } from '@fluentui/react-components';
import { useId, Link, Button } from '@fluentui/react-components';

export const DismissToastWithAction = () => {
const toasterId = useId('toaster');
Expand All @@ -25,7 +25,18 @@ export const DismissToastWithAction = () => {
return (
<>
<Toaster toasterId={toasterId} />
<button onClick={notify}>Make toast</button>
<Button onClick={notify}>Make toast</Button>
</>
);
};

DismissToastWithAction.parameters = {
docs: {
description: {
story: [
"By wrapping a button or link with a `ToastTrigger`, it's possible to make that actionable",
'element dismiss the toast with a click.',
].join('\n'),
},
},
};
Loading