Skip to content

Commit 55ff4c9

Browse files
committed
fix hover pause timer
1 parent ce28148 commit 55ff4c9

2 files changed

Lines changed: 59 additions & 47 deletions

File tree

src/hooks/useNoticeTimer.ts

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ export default function useNoticeTimer(
1818
const onEventUpdate = useEvent(onUpdate);
1919

2020
const [walking, setWalking] = React.useState(durationMs > 0);
21-
const startTimestampRef = React.useRef<number | null>(null);
2221
const passTimeRef = React.useRef(0);
22+
const lastRafTimeRef = React.useRef<number | null>(null);
2323

2424
function syncPassTime() {
2525
const now = Date.now();
26-
const passedTime = now - (startTimestampRef.current || now);
27-
startTimestampRef.current = now;
28-
passTimeRef.current += passedTime;
26+
const lastRafTime = lastRafTimeRef.current;
27+
28+
if (lastRafTime !== null) {
29+
passTimeRef.current += now - lastRafTime;
30+
}
31+
32+
lastRafTimeRef.current = now;
2933
}
3034

3135
const onPause = React.useCallback(() => {
@@ -35,64 +39,55 @@ export default function useNoticeTimer(
3539

3640
const onResume = React.useCallback(() => {
3741
if (durationMs > 0) {
42+
lastRafTimeRef.current = Date.now();
3843
setWalking(true);
3944
} else {
4045
onEventUpdate(0);
4146
}
42-
}, [durationMs, onEventUpdate]);
47+
}, [durationMs]);
4348

49+
// Reset when durationMs changed.
4450
React.useEffect(() => {
45-
if (durationMs <= 0) {
46-
startTimestampRef.current = null;
47-
onEventUpdate(0);
48-
return;
49-
}
50-
51-
syncPassTime();
52-
onEventUpdate(Math.min(passTimeRef.current / durationMs, 1));
51+
passTimeRef.current = 0;
52+
setWalking(durationMs > 0);
53+
}, [durationMs]);
5354

55+
// Trigger update when walking changed.
56+
React.useEffect(() => {
5457
if (!walking) {
55-
startTimestampRef.current = null;
5658
return;
5759
}
5860

59-
if (passTimeRef.current >= durationMs) {
60-
onEventUpdate(1);
61-
onEventClose();
62-
return;
63-
}
61+
// Use raf to update since it can provide more accurate timing and better performance for animations.
62+
if (trackProgress) {
63+
let rafId: number | null = null;
6464

65-
const timeout = window.setTimeout(() => {
66-
passTimeRef.current = durationMs;
67-
onEventUpdate(1);
68-
onEventClose();
69-
}, durationMs - passTimeRef.current);
65+
function step() {
66+
syncPassTime();
7067

71-
if (!trackProgress) {
72-
return () => {
73-
window.clearTimeout(timeout);
74-
};
75-
}
68+
if (passTimeRef.current >= durationMs) {
69+
onEventUpdate(1);
70+
onEventClose();
71+
} else {
72+
onEventUpdate(Math.min(passTimeRef.current / durationMs, 1));
73+
rafId = raf(step);
74+
}
75+
}
7676

77-
let rafId: number | null = null;
77+
step();
7878

79-
function step() {
80-
syncPassTime();
81-
onEventUpdate(Math.min(passTimeRef.current / durationMs, 1));
79+
return () => {
80+
raf.cancel(rafId!);
81+
};
82+
} else {
83+
// Fallback to setTimeout when progress tracking is not needed, which can save some resources.
84+
const timeoutId = window.setTimeout(onEventClose, durationMs);
8285

83-
if (passTimeRef.current < durationMs) {
84-
rafId = raf(step);
85-
}
86+
return () => {
87+
window.clearTimeout(timeoutId);
88+
};
8689
}
87-
88-
startTimestampRef.current = Date.now();
89-
rafId = raf(step);
90-
91-
return () => {
92-
window.clearTimeout(timeout);
93-
raf.cancel(rafId);
94-
};
95-
}, [durationMs, walking]);
90+
}, [walking]);
9691

9792
return [onResume, onPause] as const;
9893
}

tests/index.test.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ describe('Notification.Basic', () => {
3232
return { ...renderResult, instance };
3333
}
3434

35+
function step(time: number, slice = 16) {
36+
let current = 0;
37+
38+
while (current < time) {
39+
vi.advanceTimersByTime(slice);
40+
current += slice;
41+
}
42+
}
43+
3544
it('works', () => {
3645
const { instance, unmount } = renderDemo();
3746

@@ -273,6 +282,7 @@ describe('Notification.Basic', () => {
273282
it('continue timing after hover', () => {
274283
const { instance } = renderDemo({
275284
duration: 1,
285+
showProgress: true,
276286
});
277287

278288
act(() => {
@@ -291,10 +301,17 @@ describe('Notification.Basic', () => {
291301

292302
// Mouse in should not remove
293303
fireEvent.mouseEnter(document.querySelector('.rc-notification-notice'));
304+
const pausedProgress = document
305+
.querySelector<HTMLProgressElement>('.rc-notification-notice-progress')
306+
.getAttribute('value');
294307
act(() => {
308+
// Elapsed time should not advance while hovering.
295309
vi.advanceTimersByTime(1000);
296310
});
297311
expect(document.querySelector('.test')).toBeTruthy();
312+
expect(
313+
document.querySelector<HTMLProgressElement>('.rc-notification-notice-progress'),
314+
).toHaveAttribute('value', pausedProgress);
298315

299316
// Mouse out should not remove until 500ms later
300317
fireEvent.mouseLeave(document.querySelector('.rc-notification-notice'));
@@ -1139,13 +1156,13 @@ describe('Notification.Basic', () => {
11391156
expect(document.querySelector('.rc-notification-notice-progress')).toBeTruthy();
11401157

11411158
act(() => {
1142-
vi.advanceTimersByTime(500);
1159+
step(500);
11431160
});
11441161

11451162
expect(document.querySelector('.rc-notification-notice-progress')).toBeTruthy();
11461163

11471164
act(() => {
1148-
vi.advanceTimersByTime(500);
1165+
step(500);
11491166
});
11501167

11511168
expect(document.querySelector('.rc-notification-notice-progress')).toBeFalsy();

0 commit comments

Comments
 (0)