From ca3d6781da05bc6cfaf7a28a1f3dbd2aa5091b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=BF=A0=E6=B3=BD?= Date: Sat, 28 Feb 2026 16:49:37 +0800 Subject: [PATCH 1/2] fix: Continue timing when mouse moves away --- src/NoticeList.tsx | 57 ++++++++++++++-- tests/stack.test.tsx | 159 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 211 insertions(+), 5 deletions(-) diff --git a/src/NoticeList.tsx b/src/NoticeList.tsx index 3cc797e..4e0d390 100644 --- a/src/NoticeList.tsx +++ b/src/NoticeList.tsx @@ -46,7 +46,8 @@ const NoticeList: FC = (props) => { const { classNames: ctxCls } = useContext(NotificationContext); const dictRef = useRef>({}); - const [latestNotice, setLatestNotice] = useState(null); + const mousePositionRef = useRef<{ x: number; y: number } | null>(null); + const [latestNotice, setLatestNotice] = useState(null); const [hoverKeys, setHoverKeys] = useState([]); const keys = configList.map((config) => ({ @@ -60,15 +61,63 @@ const NoticeList: FC = (props) => { const placementMotion = typeof motion === 'function' ? motion(placement) : motion; + // Track mouse position globally when in stack mode + useEffect(() => { + if (!stack) return; + + const handleMouseMove = (e: MouseEvent) => { + mousePositionRef.current = { x: e.clientX, y: e.clientY }; + }; + + document.addEventListener('mousemove', handleMouseMove, { passive: true }); + return () => document.removeEventListener('mousemove', handleMouseMove); + }, [stack]); + // Clean hover key useEffect(() => { if (stack && hoverKeys.length > 1) { - setHoverKeys((prev) => - prev.filter((key) => keys.some(({ key: dataKey }) => key === dataKey)), - ); + // Only update if there's a change to avoid unnecessary re-renders + setHoverKeys((prev) => { + const filtered = prev.filter((key) => keys.some(({ key: dataKey }) => key === dataKey)); + return filtered.length === prev.length ? prev : filtered; + }); } }, [hoverKeys, keys, stack]); + // Check mouse position when keys change (notification list updates) + useEffect(() => { + if (!stack || !mousePositionRef.current) return; + + // Use requestAnimationFrame to wait for DOM updates + const rafId = requestAnimationFrame(() => { + const mousePos = mousePositionRef.current; + if (!mousePos) return; + + const newHoverKeys: string[] = []; + keys.forEach(({ key: strKey }) => { + const element = dictRef.current[strKey]; + if (element) { + const rect = element.getBoundingClientRect(); + if ( + mousePos.x >= rect.left && + mousePos.x <= rect.right && + mousePos.y >= rect.top && + mousePos.y <= rect.bottom + ) { + newHoverKeys.push(strKey); + } + } + }); + + // Only update if there's a change to avoid unnecessary re-renders + if (newHoverKeys.length > 0 || hoverKeys.length > 0) { + setHoverKeys(newHoverKeys); + } + }); + + return () => cancelAnimationFrame(rafId); + }, [keys, stack]); + // Force update latest notice useEffect(() => { if (stack && dictRef.current[keys[keys.length - 1]?.key]) { diff --git a/tests/stack.test.tsx b/tests/stack.test.tsx index 628bbcb..d871705 100644 --- a/tests/stack.test.tsx +++ b/tests/stack.test.tsx @@ -1,5 +1,5 @@ import { useNotification } from '../src'; -import { fireEvent, render } from '@testing-library/react'; +import { act, fireEvent, render } from '@testing-library/react'; import React from 'react'; require('../assets/index.less'); @@ -87,3 +87,160 @@ describe('stack', () => { expect(document.querySelector('.rc-notification-stack-expanded')).toBeFalsy(); }); }); + +describe('hover state after closing notice in stack', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should clear hover state and resume timers when closing a hovered notice', () => { + const onClose = vi.fn(); + + const Demo = () => { + const [api, holder] = useNotification({ + stack: { threshold: 3 }, + }); + return ( + <> +