From 9dc682ba24a5f1bbcc2d3b75efddd88698eb502f Mon Sep 17 00:00:00 2001 From: Kamil Guszpit Date: Sun, 4 Jan 2026 22:57:11 +0100 Subject: [PATCH 1/2] fix: resolve Next.js hydration mismatches and optimize focus management --- src/hooks.tsx | 3 ++- src/index.tsx | 42 ++++++++++++++++++------------------------ 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/hooks.tsx b/src/hooks.tsx index ee79b01..c517650 100644 --- a/src/hooks.tsx +++ b/src/hooks.tsx @@ -1,9 +1,10 @@ import React from 'react'; export const useIsDocumentHidden = () => { - const [isDocumentHidden, setIsDocumentHidden] = React.useState(document.hidden); + const [isDocumentHidden, setIsDocumentHidden] = React.useState(false); React.useEffect(() => { + setIsDocumentHidden(document.hidden); const callback = () => { setIsDocumentHidden(document.hidden); }; diff --git a/src/index.tsx b/src/index.tsx index ce031e8..8b2d126 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -628,17 +628,9 @@ const Toaster = React.forwardRef(function Toaster(pro const [heights, setHeights] = React.useState([]); const [expanded, setExpanded] = React.useState(false); const [interacting, setInteracting] = React.useState(false); - const [actualTheme, setActualTheme] = React.useState( - theme !== 'system' - ? theme - : typeof window !== 'undefined' - ? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' - : 'light', - ); + const [actualTheme, setActualTheme] = React.useState(theme !== 'system' ? theme : 'light'); - const listRef = React.useRef(null); + const listRefs = React.useRef>([]); const hotkeyLabel = hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, ''); const lastFocusedElementRef = React.useRef(null); const isFocusWithinRef = React.useRef(false); @@ -683,7 +675,7 @@ const Toaster = React.forwardRef(function Toaster(pro }); }); }); - }, [toasts]); + }, []); React.useEffect(() => { if (theme !== 'system') { @@ -743,12 +735,16 @@ const Toaster = React.forwardRef(function Toaster(pro if (isHotkeyPressed) { setExpanded(true); - listRef.current?.focus(); + const firstValidList = listRefs.current.find((el) => el); + firstValidList?.focus(); } if ( event.code === 'Escape' && - (document.activeElement === listRef.current || listRef.current?.contains(document.activeElement)) + (document.activeElement === null || + listRefs.current.some( + (listRef) => listRef === document.activeElement || listRef?.contains(document.activeElement), + )) ) { setExpanded(false); } @@ -759,16 +755,14 @@ const Toaster = React.forwardRef(function Toaster(pro }, [hotkey]); React.useEffect(() => { - if (listRef.current) { - return () => { - if (lastFocusedElementRef.current) { - lastFocusedElementRef.current.focus({ preventScroll: true }); - lastFocusedElementRef.current = null; - isFocusWithinRef.current = false; - } - }; - } - }, [listRef.current]); + return () => { + if (lastFocusedElementRef.current) { + lastFocusedElementRef.current.focus({ preventScroll: true }); + lastFocusedElementRef.current = null; + isFocusWithinRef.current = false; + } + }; + }, []); return ( // Remove item from normal navigation flow, only available via hotkey @@ -792,7 +786,7 @@ const Toaster = React.forwardRef(function Toaster(pro key={position} dir={dir === 'auto' ? getDocumentDirection() : dir} tabIndex={-1} - ref={listRef} + ref={(el) => (listRefs.current[index] = el)} className={className} data-sonner-toaster data-sonner-theme={actualTheme} From fc16b40c2c314171cd47f3ad8c58c948658b8bb8 Mon Sep 17 00:00:00 2001 From: Kamil Guszpit <146444729+kamilguszpit@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:08:55 +0100 Subject: [PATCH 2/2] fix: refine escape key logic based on review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/index.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 8b2d126..72cab53 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -741,10 +741,9 @@ const Toaster = React.forwardRef(function Toaster(pro if ( event.code === 'Escape' && - (document.activeElement === null || - listRefs.current.some( - (listRef) => listRef === document.activeElement || listRef?.contains(document.activeElement), - )) + listRefs.current.some( + (listRef) => listRef === document.activeElement || listRef?.contains(document.activeElement), + ) ) { setExpanded(false); }