diff --git a/packages/react-components/react-toast/src/components/Toast.tsx b/packages/react-components/react-toast/src/components/Toast.tsx index 3d29381e27d5ac..d0a43e10340773 100644 --- a/packages/react-components/react-toast/src/components/Toast.tsx +++ b/packages/react-components/react-toast/src/components/Toast.tsx @@ -77,9 +77,9 @@ const useStyles = makeStyles({ export const Toast: React.FC & { visible: boolean }> = props => { const styles = useStyles(); - const { visible, children, close, remove, timeout } = props; - const { play, running } = useToast(); - const toastRef = React.useRef(null); + const { visible, children, close, remove, ...toastOptions } = props; + const { timeout } = toastOptions; + const { play, running, toastRef } = useToast(toastOptions); // start the toast once it's fully in useIsomorphicLayoutEffect(() => { diff --git a/packages/react-components/react-toast/src/components/Toaster.tsx b/packages/react-components/react-toast/src/components/Toaster.tsx index b387b043d05e3f..ebb779ff4881b4 100644 --- a/packages/react-components/react-toast/src/components/Toaster.tsx +++ b/packages/react-components/react-toast/src/components/Toaster.tsx @@ -27,11 +27,7 @@ export const Toaster: React.FC = () => {
{toasts.map(({ content, ...toastProps }) => { return ( - + {content as React.ReactNode} ); diff --git a/packages/react-components/react-toast/src/state/types.ts b/packages/react-components/react-toast/src/state/types.ts index d5a359e2f5ade3..686ee1e3a04a23 100644 --- a/packages/react-components/react-toast/src/state/types.ts +++ b/packages/react-components/react-toast/src/state/types.ts @@ -9,9 +9,16 @@ export interface ToastOptions { position?: ToastPosition; content?: unknown; timeout?: number; + pauseOnWindowBlur?: boolean; + pauseOnHover?: boolean; } -export interface Toast extends Required> { +export interface DefaultToastOptions + extends Pick {} + +export interface ValidatedToastOptions extends Required {} + +export interface Toast extends Required { close: () => void; remove: () => void; } diff --git a/packages/react-components/react-toast/src/state/useToast.ts b/packages/react-components/react-toast/src/state/useToast.ts index 2142da55df6fa7..9699389d1c0686 100644 --- a/packages/react-components/react-toast/src/state/useToast.ts +++ b/packages/react-components/react-toast/src/state/useToast.ts @@ -1,18 +1,52 @@ import * as React from 'react'; -import { Toast } from './vanilla/toast'; import { useForceUpdate } from '@fluentui/react-utilities'; +import { Toast } from './vanilla/toast'; +import { ValidatedToastOptions } from './types'; + +const noop = () => null; -export function useToast() { +export function useToast(options: ValidatedToastOptions) { + const toastOptions = useToastOptions(options); const forceRender = useForceUpdate(); - const [toast] = React.useState(() => { - const newToast = new Toast(); - newToast.onUpdate = forceRender; - return newToast; - }); + const [toast] = React.useState(() => new Toast()); + + const toastRef = React.useRef(null); + + React.useEffect(() => { + if (toast && toastRef.current) { + toast.onUpdate = forceRender; + toast.connectToDOM(toastRef.current, toastOptions); + return () => toast.disconnect(); + } + }, [toast, toastOptions, forceRender]); + + if (!toast) { + return { + toastRef, + play: noop, + pause: noop, + running: false, + }; + } return { + toastRef, play: toast.play, pause: toast.pause, running: toast.running, }; } + +function useToastOptions(options: ValidatedToastOptions) { + const { pauseOnHover, pauseOnWindowBlur, position, timeout } = options; + + return React.useMemo( + () => ({ + pauseOnHover, + pauseOnWindowBlur, + position, + timeout, + }), + [pauseOnHover, pauseOnWindowBlur, position, timeout], + ); +} diff --git a/packages/react-components/react-toast/src/state/vanilla/toast.ts b/packages/react-components/react-toast/src/state/vanilla/toast.ts index 544cca1479e6bc..c7b5aee5b1903a 100644 --- a/packages/react-components/react-toast/src/state/vanilla/toast.ts +++ b/packages/react-components/react-toast/src/state/vanilla/toast.ts @@ -1,13 +1,66 @@ +import { ValidatedToastOptions } from '../types'; + // TODO convert to closure export class Toast { - public running = false; - public onUpdate: () => void = () => null; + public running: boolean; + public onUpdate: () => void; + private toastElement?: HTMLElement; + private pauseOnWindowBlur: ValidatedToastOptions['pauseOnWindowBlur']; + private pauseOnHover: ValidatedToastOptions['pauseOnHover']; - public play() { - this.running = true; + constructor() { + this.running = false; + this.onUpdate = () => null; + + this.pauseOnHover = false; + this.pauseOnWindowBlur = false; } - public pause() { - this.running = false; + public disconnect() { + if (!this.toastElement) { + return; + } + + const targetDocument = this.toastElement.ownerDocument; + + if (this.pauseOnWindowBlur) { + targetDocument.defaultView?.removeEventListener('focus', this.play); + targetDocument.defaultView?.removeEventListener('blur', this.pause); + } + + if (this.pauseOnHover) { + this.toastElement.addEventListener('mouseenter', this.pause); + this.toastElement.addEventListener('mouseleave', this.play); + } + + this.toastElement = undefined; } + + public connectToDOM(element: HTMLElement, options: ValidatedToastOptions) { + const { pauseOnHover, pauseOnWindowBlur } = options; + this.pauseOnHover = pauseOnHover; + this.pauseOnWindowBlur = pauseOnWindowBlur; + + this.toastElement = element; + const targetDocument = element.ownerDocument; + if (this.pauseOnWindowBlur) { + targetDocument.defaultView?.addEventListener('focus', this.play); + targetDocument.defaultView?.addEventListener('blur', this.pause); + } + + if (this.pauseOnHover) { + this.toastElement.addEventListener('mouseenter', this.pause); + this.toastElement.addEventListener('mouseleave', this.play); + } + } + + public play = () => { + this.running = true; + this.onUpdate(); + }; + + public pause = () => { + this.running = false; + this.onUpdate(); + }; } 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 64fbb9d7217119..1ed018765e2d86 100644 --- a/packages/react-components/react-toast/src/state/vanilla/toaster.ts +++ b/packages/react-components/react-toast/src/state/vanilla/toaster.ts @@ -74,7 +74,14 @@ export class Toaster { } private _buildToast(toastOptions: ToastOptions) { - const { toastId = '', position = 'bottom-right', timeout = 3000, content = '' } = toastOptions; + const { + toastId = '', + position = 'bottom-right', + timeout = 3000, + content = '', + pauseOnHover = false, + pauseOnWindowBlur = false, + } = toastOptions; if (this.toasts.has(toastId)) { return; } @@ -90,6 +97,8 @@ export class Toaster { }; const toast: Toast = { + pauseOnHover, + pauseOnWindowBlur, position, toastId, timeout, diff --git a/packages/react-components/react-toast/stories/Toast/PauseOnHover.stories.tsx b/packages/react-components/react-toast/stories/Toast/PauseOnHover.stories.tsx new file mode 100644 index 00000000000000..5b052fa6ce31cb --- /dev/null +++ b/packages/react-components/react-toast/stories/Toast/PauseOnHover.stories.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Toaster, useToastController } from '@fluentui/react-toast'; + +export const PauseOnHover = () => { + const { dispatchToast } = useToastController(); + const notify = () => dispatchToast('Hover me!', { pauseOnHover: true }); + + return ( + <> + + + + ); +}; diff --git a/packages/react-components/react-toast/stories/Toast/PauseOnWindowBlur.stories.tsx b/packages/react-components/react-toast/stories/Toast/PauseOnWindowBlur.stories.tsx new file mode 100644 index 00000000000000..01477029bff0d3 --- /dev/null +++ b/packages/react-components/react-toast/stories/Toast/PauseOnWindowBlur.stories.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Toaster, useToastController } from '@fluentui/react-toast'; + +export const PauseOnWindowBlur = () => { + const { dispatchToast } = useToastController(); + const notify = () => dispatchToast('Click on another window!', { pauseOnWindowBlur: true }); + + 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 d1a749f4a62e00..ca6306ae12b230 100644 --- a/packages/react-components/react-toast/stories/Toast/index.stories.tsx +++ b/packages/react-components/react-toast/stories/Toast/index.stories.tsx @@ -3,6 +3,8 @@ export { CustomTimeout } from './CustomTimeout.stories'; export { ToastPositions } from './ToastPositions.stories'; export { DismissToast } from './DismissToast.stories'; export { DismissAll } from './DismissAll.stories'; +export { PauseOnWindowBlur } from './PauseOnWindowBlur.stories'; +export { PauseOnHover } from './PauseOnHover.stories'; export default { title: 'Preview Components/Toast',