diff --git a/src/Notification.tsx b/src/Notification.tsx index 2d47fe2..17b8cd3 100644 --- a/src/Notification.tsx +++ b/src/Notification.tsx @@ -118,7 +118,7 @@ const Notification = React.forwardRef((props, // ======================== Duration ======================== const [hovering, setHovering] = React.useState(false); - const [onResume, onPause] = useNoticeTimer(duration, onInternalClose, setPercent, !!showProgress); + const [onResume, onPause] = useNoticeTimer(duration, onInternalClose, setPercent); const validPercent = 100 - Math.min(Math.max(percent * 100, 0), 100); const Progress = components?.progress || DefaultProgress; diff --git a/src/hooks/useNoticeTimer.ts b/src/hooks/useNoticeTimer.ts index 6eeedb9..5cea2f6 100644 --- a/src/hooks/useNoticeTimer.ts +++ b/src/hooks/useNoticeTimer.ts @@ -10,7 +10,6 @@ export default function useNoticeTimer( duration: number | false | null, onClose: VoidFunction, onUpdate: (ptg: number) => void, - trackProgress = true, ) { const mergedDuration = typeof duration === 'number' ? duration : 0; const durationMs = Math.max(mergedDuration, 0) * 1000; @@ -18,14 +17,18 @@ export default function useNoticeTimer( const onEventUpdate = useEvent(onUpdate); const [walking, setWalking] = React.useState(durationMs > 0); - const startTimestampRef = React.useRef(null); const passTimeRef = React.useRef(0); + const lastRafTimeRef = React.useRef(null); function syncPassTime() { const now = Date.now(); - const passedTime = now - (startTimestampRef.current || now); - startTimestampRef.current = now; - passTimeRef.current += passedTime; + const lastRafTime = lastRafTimeRef.current; + + if (lastRafTime !== null) { + passTimeRef.current += now - lastRafTime; + } + + lastRafTimeRef.current = now; } const onPause = React.useCallback(() => { @@ -35,62 +38,43 @@ export default function useNoticeTimer( const onResume = React.useCallback(() => { if (durationMs > 0) { + lastRafTimeRef.current = Date.now(); setWalking(true); } else { onEventUpdate(0); } - }, [durationMs, onEventUpdate]); + }, [durationMs]); + // Reset when durationMs changed. React.useEffect(() => { - if (durationMs <= 0) { - startTimestampRef.current = null; - onEventUpdate(0); - return; - } - - syncPassTime(); - onEventUpdate(Math.min(passTimeRef.current / durationMs, 1)); + passTimeRef.current = 0; + setWalking(durationMs > 0); + }, [durationMs]); + // Trigger update when walking changed. + React.useEffect(() => { if (!walking) { - startTimestampRef.current = null; - return; - } - - if (passTimeRef.current >= durationMs) { - onEventUpdate(1); - onEventClose(); return; } - const timeout = window.setTimeout(() => { - passTimeRef.current = durationMs; - onEventUpdate(1); - onEventClose(); - }, durationMs - passTimeRef.current); - - if (!trackProgress) { - return () => { - window.clearTimeout(timeout); - }; - } - let rafId: number | null = null; function step() { syncPassTime(); - onEventUpdate(Math.min(passTimeRef.current / durationMs, 1)); - if (passTimeRef.current < durationMs) { + if (passTimeRef.current >= durationMs) { + onEventUpdate(1); + onEventClose(); + } else { + onEventUpdate(Math.min(passTimeRef.current / durationMs, 1)); rafId = raf(step); } } - startTimestampRef.current = Date.now(); - rafId = raf(step); + step(); return () => { - window.clearTimeout(timeout); - raf.cancel(rafId); + raf.cancel(rafId!); }; }, [durationMs, walking]); diff --git a/tests/hooks.test.tsx b/tests/hooks.test.tsx index 8313514..a0bb650 100644 --- a/tests/hooks.test.tsx +++ b/tests/hooks.test.tsx @@ -30,6 +30,15 @@ describe('Notification.Hooks', () => { return { ...renderResult, instance }; } + function step(time: number, slice = 16) { + let current = 0; + + while (current < time) { + vi.advanceTimersByTime(slice); + current += slice; + } + } + it('works', async () => { const Context = React.createContext({ name: 'light' }); @@ -102,7 +111,7 @@ describe('Notification.Hooks', () => { expect(document.querySelector('.context-content').textContent).toEqual('light'); act(() => { - vi.runAllTimers(); + step(500); }); expect(document.querySelector('.context-content').textContent).toEqual('bamboo'); diff --git a/tests/index.test.tsx b/tests/index.test.tsx index d79deae..e45e5d5 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -32,6 +32,15 @@ describe('Notification.Basic', () => { return { ...renderResult, instance }; } + function step(time: number, slice = 16) { + let current = 0; + + while (current < time) { + vi.advanceTimersByTime(slice); + current += slice; + } + } + it('works', () => { const { instance, unmount } = renderDemo(); @@ -285,26 +294,27 @@ describe('Notification.Basic', () => { // Wait for 500ms act(() => { - vi.advanceTimersByTime(500); + step(500); }); expect(document.querySelector('.test')).toBeTruthy(); // Mouse in should not remove fireEvent.mouseEnter(document.querySelector('.rc-notification-notice')); act(() => { - vi.advanceTimersByTime(1000); + // Elapsed time should not advance while hovering. + step(1000); }); expect(document.querySelector('.test')).toBeTruthy(); // Mouse out should not remove until 500ms later fireEvent.mouseLeave(document.querySelector('.rc-notification-notice')); act(() => { - vi.advanceTimersByTime(450); + step(450); }); expect(document.querySelector('.test')).toBeTruthy(); act(() => { - vi.advanceTimersByTime(100); + step(100); }); expect(document.querySelector('.test')).toBeFalsy(); }); @@ -351,27 +361,27 @@ describe('Notification.Basic', () => { // Wait for 500ms act(() => { - vi.advanceTimersByTime(500); + step(500); }); expect(document.querySelector('.test')).toBeTruthy(); // Mouse in should not remove fireEvent.mouseEnter(document.querySelector('.rc-notification-notice')); act(() => { - vi.advanceTimersByTime(200); + step(200); }); expect(document.querySelector('.test')).toBeTruthy(); // Mouse out should not remove until 500ms later fireEvent.mouseLeave(document.querySelector('.rc-notification-notice')); act(() => { - vi.advanceTimersByTime(200); + step(200); }); expect(document.querySelector('.test')).toBeTruthy(); // act(() => { - vi.advanceTimersByTime(100); + step(100); }); expect(document.querySelector('.test')).toBeFalsy(); }); @@ -1139,13 +1149,13 @@ describe('Notification.Basic', () => { expect(document.querySelector('.rc-notification-notice-progress')).toBeTruthy(); act(() => { - vi.advanceTimersByTime(500); + step(500); }); expect(document.querySelector('.rc-notification-notice-progress')).toBeTruthy(); act(() => { - vi.advanceTimersByTime(500); + step(500); }); expect(document.querySelector('.rc-notification-notice-progress')).toBeFalsy(); @@ -1210,7 +1220,7 @@ describe('Notification.Basic', () => { expect(document.querySelectorAll('.rc-notification-notice').length).toBe(2); act(() => { - vi.advanceTimersByTime(5000); + step(5000); }); expect(document.querySelectorAll('.rc-notification-notice').length).toBe(1);