diff --git a/src/NoticeList.tsx b/src/NoticeList.tsx index 3cc797e..319d286 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,66 @@ 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 + setHoverKeys((prev) => { + if (prev.length === newHoverKeys.length && prev.every((k, i) => k === newHoverKeys[i])) { + return prev; + } + return 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 ( + <> +