From 5bb58683412eeef6e8d7cf5b3b1749aa529fe3d9 Mon Sep 17 00:00:00 2001 From: etcd Date: Mon, 29 Jan 2024 01:38:55 -0600 Subject: [PATCH] Implement option to pause when document is hidden (#304) * Implement option to pause when document is hidden * Add option to documentation * Rename pauseWhenDocumentHidden -> pauseWhenPageIsHidden for conciseness; improve documentation with examples --------- Co-authored-by: etcd --- src/hooks.tsx | 15 +++++++++++++++ src/index.tsx | 22 ++++++++++++++++++++-- src/types.ts | 2 ++ website/src/pages/toaster.mdx | 31 ++++++++++++++++--------------- 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 src/hooks.tsx diff --git a/src/hooks.tsx b/src/hooks.tsx new file mode 100644 index 0000000..e5dd2db --- /dev/null +++ b/src/hooks.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +export const useIsDocumentHidden = () => { + const [isDocumentHidden, setIsDocumentHidden] = React.useState(false); + + React.useEffect(() => { + const callback = () => { + setIsDocumentHidden(document.hidden); + }; + document.addEventListener('visibilitychange', callback); + return () => window.removeEventListener('visibilitychange', callback); + }, []); + + return isDocumentHidden; +}; diff --git a/src/index.tsx b/src/index.tsx index 824bc9d..74fa774 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ import ReactDOM from 'react-dom'; import './styles.css'; import { getAsset, Loader } from './assets'; +import { useIsDocumentHidden } from './hooks'; import type { HeightT, ToastT, ToastToDismiss, ExternalToast, ToasterProps, ToastProps } from './types'; import { ToastState, toast } from './state'; @@ -57,6 +58,7 @@ const Toast = (props: ToastProps) => { expandByDefault, classNames, closeButtonAriaLabel = 'Close toast', + pauseWhenPageIsHidden, } = props; const [mounted, setMounted] = React.useState(false); const [removed, setRemoved] = React.useState(false); @@ -100,6 +102,8 @@ const Toast = (props: ToastProps) => { return prev + curr.height; }, 0); }, [heights, heightIndex]); + const isDocumentHidden = useIsDocumentHidden(); + const invert = toast.invert || ToasterInvert; const disabled = toastType === 'loading'; @@ -145,6 +149,7 @@ const Toast = (props: ToastProps) => { if ((toast.promise && toastType === 'loading') || toast.duration === Infinity || toast.type === 'loading') return; let timeoutId: NodeJS.Timeout; let remainingTime = duration; + // Pause the timer on each hover const pauseTimer = () => { if (lastCloseTimerStartTimeRef.current < closeTimerStartTimeRef.current) { @@ -167,14 +172,25 @@ const Toast = (props: ToastProps) => { }, remainingTime); }; - if (expanded || interacting) { + if (expanded || interacting || (pauseWhenPageIsHidden && isDocumentHidden)) { pauseTimer(); } else { startTimer(); } return () => clearTimeout(timeoutId); - }, [expanded, interacting, expandByDefault, toast, duration, deleteToast, toast.promise, toastType]); + }, [ + expanded, + interacting, + expandByDefault, + toast, + duration, + deleteToast, + toast.promise, + toastType, + pauseWhenPageIsHidden, + isDocumentHidden, + ]); React.useEffect(() => { const toastNode = toastRef.current; @@ -428,6 +444,7 @@ const Toaster = (props: ToasterProps) => { gap, loadingIcon, containerAriaLabel = 'Notifications', + pauseWhenPageIsHidden, } = props; const [toasts, setToasts] = React.useState([]); const possiblePositions = React.useMemo(() => { @@ -648,6 +665,7 @@ const Toaster = (props: ToasterProps) => { gap={gap} loadingIcon={loadingIcon} expanded={expanded} + pauseWhenPageIsHidden={pauseWhenPageIsHidden} /> ))} diff --git a/src/types.ts b/src/types.ts index 37961e6..e9b3c87 100644 --- a/src/types.ts +++ b/src/types.ts @@ -101,6 +101,7 @@ export interface ToasterProps { dir?: 'rtl' | 'ltr' | 'auto'; loadingIcon?: React.ReactNode; containerAriaLabel?: string; + pauseWhenPageIsHidden?: boolean; } export interface ToastProps { @@ -128,6 +129,7 @@ export interface ToastProps { loadingIcon?: React.ReactNode; classNames?: ToastClassnames; closeButtonAriaLabel?: string; + pauseWhenPageIsHidden: boolean; } export enum SwipeStateTypes { diff --git a/website/src/pages/toaster.mdx b/website/src/pages/toaster.mdx index 06dc2df..3086366 100644 --- a/website/src/pages/toaster.mdx +++ b/website/src/pages/toaster.mdx @@ -49,18 +49,19 @@ Changes the directionality of the toast's text. ## API Reference -| Property | Description | Default | -| :------------ | :------------------------------------------------------------------------------------------------: | -------------: | -| theme | Toast's theme, either `light`, `dark`, or `system` | `light` | -| richColors | Makes error and success state more colorful | `false` | -| expand | Toasts will be expanded by default | `false` | -| visibleToasts | Amount of visible toasts | `3` | -| position | Place where the toasts will be rendered | `bottom-right` | -| closeButton | Adds a close button to all toasts | `false` | -| offset | Offset from the edges of the screen. | `32px` | -| dir | Directionality of toast's text | `ltr` | -| hotkey | Keyboard shortcut that will move focus to the toaster area. | `⌥/alt + T` | -| invert | Dark toasts in light mode and vice versa. | `false` | -| toastOptions | These will act as default options for all toasts. See [toast()](/toast) for all available options. | `4000` | -| gap | Gap between toasts when expanded | `14` | -| loadingIcon | Changes the default loading icon | `-` | +| Property | Description | Default | +| :-------------------- | :-----------------------------------------------------------------------------------------------------------------------------: | -------------: | +| theme | Toast's theme, either `light`, `dark`, or `system` | `light` | +| richColors | Makes error and success state more colorful | `false` | +| expand | Toasts will be expanded by default | `false` | +| visibleToasts | Amount of visible toasts | `3` | +| position | Place where the toasts will be rendered | `bottom-right` | +| closeButton | Adds a close button to all toasts | `false` | +| offset | Offset from the edges of the screen. | `32px` | +| dir | Directionality of toast's text | `ltr` | +| hotkey | Keyboard shortcut that will move focus to the toaster area. | `⌥/alt + T` | +| invert | Dark toasts in light mode and vice versa. | `false` | +| toastOptions | These will act as default options for all toasts. See [toast()](/toast) for all available options. | `4000` | +| gap | Gap between toasts when expanded | `14` | +| loadingIcon | Changes the default loading icon | `-` | +| pauseWhenPageIsHidden | Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked. | `false` |