diff --git a/packages/react-components/react-toast/etc/react-toast.api.md b/packages/react-components/react-toast/etc/react-toast.api.md index 04a9c293cea64..212a5f8126af5 100644 --- a/packages/react-components/react-toast/etc/react-toast.api.md +++ b/packages/react-components/react-toast/etc/react-toast.api.md @@ -19,6 +19,7 @@ export type ToastPosition = 'top-right' | 'top-center' | 'top-left' | 'bottom-ri export function useToastController(): { dispatchToast: (content: React_2.ReactNode, options?: Partial | undefined) => void; dismissToast: (toastId?: string | undefined) => void; + updateToast: (options: UpdateToastEventDetail) => void; }; // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/react-toast/src/components/Toast.tsx b/packages/react-components/react-toast/src/components/Toast.tsx index 19c538935f37a..77f89beeeb851 100644 --- a/packages/react-components/react-toast/src/components/Toast.tsx +++ b/packages/react-components/react-toast/src/components/Toast.tsx @@ -77,7 +77,7 @@ const useStyles = makeStyles({ export const Toast: React.FC = props => { const styles = useStyles(); - const { visible, children, close, remove, ...toastOptions } = props; + const { visible, children, close, remove, updateId, ...toastOptions } = props; const { timeout } = toastOptions; const { play, running, toastRef } = useToast({ ...toastOptions, content: children }); @@ -99,7 +99,7 @@ export const Toast: React.FC = props => {
{children} - +
); diff --git a/packages/react-components/react-toast/src/state/constants.ts b/packages/react-components/react-toast/src/state/constants.ts index 9ca243fc8dcc6..003bad5391867 100644 --- a/packages/react-components/react-toast/src/state/constants.ts +++ b/packages/react-components/react-toast/src/state/constants.ts @@ -1,4 +1,5 @@ export const EVENTS = { show: 'fui-toast-show', dismiss: 'fui-toast-dismiss', + update: 'fui-toast-update', } as const; diff --git a/packages/react-components/react-toast/src/state/types.ts b/packages/react-components/react-toast/src/state/types.ts index a9161b84757cb..86c419864ecf4 100644 --- a/packages/react-components/react-toast/src/state/types.ts +++ b/packages/react-components/react-toast/src/state/types.ts @@ -22,12 +22,17 @@ export interface ToasterOptions export interface Toast extends ToastOptions { close: () => void; remove: () => void; + updateId: number; } export interface ShowToastEventDetail extends Partial { toastId: ToastId; } +export interface UpdateToastEventDetail extends Partial { + toastId: ToastId; +} + export interface DismissToastEventDetail { toastId: ToastId | undefined; } @@ -37,4 +42,5 @@ type EventListener = (e: CustomEvent) => void; export type ToastListenerMap = { [EVENTS.show]: EventListener; [EVENTS.dismiss]: EventListener; + [EVENTS.update]: EventListener; }; diff --git a/packages/react-components/react-toast/src/state/useToastController.ts b/packages/react-components/react-toast/src/state/useToastController.ts index fbd64588edace..5eb4c33ae377c 100644 --- a/packages/react-components/react-toast/src/state/useToastController.ts +++ b/packages/react-components/react-toast/src/state/useToastController.ts @@ -1,31 +1,36 @@ -import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; -import { dispatchToast as dispatchToastVanilla, dismissToast as dismissToastVanilla } from './vanilla'; import * as React from 'react'; -import { ToastId, ToastOptions } from './types'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +import { + dispatchToast as dispatchToastVanilla, + dismissToast as dismissToastVanilla, + updateToast as updateToastVanilla, +} from './vanilla'; +import { ToastId, ToastOptions, UpdateToastEventDetail } from './types'; + +const noop = () => undefined; export function useToastController() { const { targetDocument } = useFluent(); - const dispatchToast = React.useCallback( - (content: React.ReactNode, options?: Partial) => { - if (targetDocument) { - dispatchToastVanilla(content, options, targetDocument); - } - }, - [targetDocument], - ); + return React.useMemo(() => { + if (!targetDocument) { + return { + dispatchToast: noop, + dismissToast: noop, + updateToast: noop, + }; + } - const dismissToast = React.useCallback( - (toastId?: ToastId) => { - if (targetDocument) { + return { + dispatchToast: (content: React.ReactNode, options?: Partial) => { + dispatchToastVanilla(content, options, targetDocument); + }, + dismissToast: (toastId?: Partial) => { dismissToastVanilla(toastId, targetDocument); - } - }, - [targetDocument], - ); - - return { - dispatchToast, - dismissToast, - }; + }, + updateToast: (options: UpdateToastEventDetail) => { + updateToastVanilla(options, targetDocument); + }, + }; + }, [targetDocument]); } diff --git a/packages/react-components/react-toast/src/state/vanilla/index.ts b/packages/react-components/react-toast/src/state/vanilla/index.ts index 78e6c1e54e5b2..c7a2e61893079 100644 --- a/packages/react-components/react-toast/src/state/vanilla/index.ts +++ b/packages/react-components/react-toast/src/state/vanilla/index.ts @@ -1,5 +1,6 @@ export * from './dispatchToast'; export * from './dismissToast'; +export * from './updateToast'; export * from './toast'; export * from './toaster'; export * from './getPositionStyles'; diff --git a/packages/react-components/react-toast/src/state/vanilla/toaster.ts b/packages/react-components/react-toast/src/state/vanilla/toaster.ts index 38d327e638b9a..24314a84e76f1 100644 --- a/packages/react-components/react-toast/src/state/vanilla/toaster.ts +++ b/packages/react-components/react-toast/src/state/vanilla/toaster.ts @@ -1,4 +1,4 @@ -import { Toast, ToasterOptions, ToastId, ToastOptions, ToastListenerMap } from '../types'; +import { Toast, ToasterOptions, ToastId, ToastOptions, ToastListenerMap, UpdateToastEventDetail } from '../types'; import { EVENTS } from '../constants'; function assignDefined(a: Partial, b: Partial) { @@ -49,6 +49,7 @@ export class Toaster { assignDefined(this.toasterOptions, options); const buildToast: ToastListenerMap[typeof EVENTS.show] = e => this._buildToast(e.detail); + const updateToast: ToastListenerMap[typeof EVENTS.update] = e => this._updateToast(e.detail); const dismissToast: ToastListenerMap[typeof EVENTS.dismiss] = e => { const { toastId } = e.detail; if (toastId) { @@ -58,11 +59,9 @@ export class Toaster { } }; - this.listeners.set(EVENTS.show, buildToast); - this.listeners.set(EVENTS.dismiss, dismissToast); - this._addEventListener(EVENTS.show, buildToast); this._addEventListener(EVENTS.dismiss, dismissToast); + this._addEventListener(EVENTS.update, updateToast); } public isToastVisible = (toastId: ToastId) => { @@ -74,6 +73,7 @@ export class Toaster { return; } + this.listeners.set(eventType, callback); const targetDocument = this.toasterElement?.ownerDocument; targetDocument.addEventListener(eventType, callback as () => void); } @@ -90,6 +90,18 @@ export class Toaster { targetDocument.removeEventListener(eventType, callback as () => void); } + private _updateToast(toastOptions: UpdateToastEventDetail) { + const { toastId } = toastOptions; + const toastToUpdate = this.toasts.get(toastId); + if (!toastToUpdate) { + return; + } + + Object.assign(toastToUpdate, toastOptions); + toastToUpdate.updateId++; + this.onUpdate(); + } + private _dismissToast(toastId: ToastId) { this.visibleToasts.delete(toastId); this.onUpdate(); @@ -123,6 +135,7 @@ export class Toaster { remove, toastId, content, + updateId: 0, }; assignDefined(toast, toastOptions); diff --git a/packages/react-components/react-toast/src/state/vanilla/updateToast.ts b/packages/react-components/react-toast/src/state/vanilla/updateToast.ts new file mode 100644 index 0000000000000..e708df31823ef --- /dev/null +++ b/packages/react-components/react-toast/src/state/vanilla/updateToast.ts @@ -0,0 +1,12 @@ +import { UpdateToastEventDetail } from '../types'; +import { EVENTS } from '../constants'; + +export function updateToast(options: UpdateToastEventDetail, targetDocument: Document) { + const event = new CustomEvent(EVENTS.update, { + bubbles: false, + cancelable: false, + detail: options, + }); + + targetDocument.dispatchEvent(event); +} diff --git a/packages/react-components/react-toast/stories/Toast/UpdateToast.stories.tsx b/packages/react-components/react-toast/stories/Toast/UpdateToast.stories.tsx new file mode 100644 index 0000000000000..b20ab45a4ec5e --- /dev/null +++ b/packages/react-components/react-toast/stories/Toast/UpdateToast.stories.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { Toaster, useToastController, ToastId } from '@fluentui/react-toast'; + +export const UpdateToast = () => { + const { dispatchToast, updateToast } = useToastController(); + const notify = (id: ToastId) => dispatchToast('This toast never closes', { toastId: id, timeout: -1 }); + const update = (id: ToastId) => updateToast({ content: 'This toast will close soon', toastId: id, timeout: 1000 }); + + return ( + <> + + + + + ); +}; diff --git a/packages/react-components/react-toast/stories/Toast/index.stories.tsx b/packages/react-components/react-toast/stories/Toast/index.stories.tsx index 96353ca5bdbd8..67513a4be27e2 100644 --- a/packages/react-components/react-toast/stories/Toast/index.stories.tsx +++ b/packages/react-components/react-toast/stories/Toast/index.stories.tsx @@ -6,6 +6,7 @@ export { DismissToast } from './DismissToast.stories'; export { DismissAll } from './DismissAll.stories'; export { PauseOnWindowBlur } from './PauseOnWindowBlur.stories'; export { PauseOnHover } from './PauseOnHover.stories'; +export { UpdateToast } from './UpdateToast.stories'; export default { title: 'Preview Components/Toast',