From 566eb4dd25fde26731b23e068ff5a4667b47f61e Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Feb 2025 13:23:41 -0600 Subject: [PATCH] fix(toast): resume timers if pointer left region due to region shrinking --- .../@react-aria/toast/src/useToastRegion.ts | 26 +++++++++++++++++++ .../@react-aria/toast/stories/Example.tsx | 2 +- .../toast/stories/useToast.stories.tsx | 15 ++++++++--- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/toast/src/useToastRegion.ts b/packages/@react-aria/toast/src/useToastRegion.ts index fc09c822317..28138eff761 100644 --- a/packages/@react-aria/toast/src/useToastRegion.ts +++ b/packages/@react-aria/toast/src/useToastRegion.ts @@ -49,6 +49,32 @@ export function useToastRegion(props: AriaToastRegionProps, state: ToastState onHoverEnd: state.resumeAll }); + let prevToastCount = useRef(state.visibleToasts.length); + useEffect(() => { + // Resume timers if the user's pointer left the region due to a toast being removed and the region shrinking. + // Waits until the next pointermove after a toast is removed. + let onPointerMove = (e: PointerEvent) => { + if (!ref.current) { + document.removeEventListener('pointermove', onPointerMove); + return; + } + let regionRect = ref.current.getBoundingClientRect(); + const isPointerOverRegion = e.clientX >= regionRect.left && e.clientX <= regionRect.right && e.clientY >= regionRect.top && e.clientY <= regionRect.bottom; + if (!isPointerOverRegion) { + state.resumeAll(); + } + document.removeEventListener('pointermove', onPointerMove); + }; + + if (state.visibleToasts.length < prevToastCount.current && state.visibleToasts.length > 0) { + document.addEventListener('pointermove', onPointerMove); + } + prevToastCount.current = state.visibleToasts.length; + return () => { + document.removeEventListener('pointermove', onPointerMove); + }; + }, [state.visibleToasts, ref, state]); + // Manage focus within the toast region. // If a focused containing toast is removed, move focus to the next toast, or the previous toast if there is no next toast. // We might be making an assumption with how this works if someone implements the priority queue differently, or diff --git a/packages/@react-aria/toast/stories/Example.tsx b/packages/@react-aria/toast/stories/Example.tsx index bf337271c83..1d79d62cad4 100644 --- a/packages/@react-aria/toast/stories/Example.tsx +++ b/packages/@react-aria/toast/stories/Example.tsx @@ -32,7 +32,7 @@ function ToastRegion() { let ref = useRef(null); let {regionProps} = useToastRegion({}, state, ref); return ( -
+
{state.visibleToasts.map(toast => ( ))} diff --git a/packages/@react-aria/toast/stories/useToast.stories.tsx b/packages/@react-aria/toast/stories/useToast.stories.tsx index 985a8b305b8..6e71d8423e2 100644 --- a/packages/@react-aria/toast/stories/useToast.stories.tsx +++ b/packages/@react-aria/toast/stories/useToast.stories.tsx @@ -16,7 +16,14 @@ import {ToastContainer} from './Example'; export default { title: 'useToast', args: { - maxVisibleToasts: 1 + maxVisibleToasts: 1, + timeout: null + }, + argTypes: { + timeout: { + control: 'radio', + options: [null, 5000] + } } }; @@ -25,9 +32,9 @@ let count = 0; export const Default = args => ( {state => (<> - - - + + + )} );