Skip to content

Commit 78075d0

Browse files
author
kyvg
committed
fix: pause timer with multiple impressions (#71)
1 parent e32f7a2 commit 78075d0

File tree

5 files changed

+167
-46
lines changed

5 files changed

+167
-46
lines changed

.eslintrc.cjs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ module.exports = {
1717
rules: {
1818
'@typescript-eslint/ban-types': 'off',
1919
'@typescript-eslint/ban-ts-comment': 'off',
20-
'@typescript-eslint/indent': ['error', 2],
2120
'@typescript-eslint/no-explicit-any': 'off',
2221
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
2322
'@typescript-eslint/prefer-interface': 'off',
@@ -80,12 +79,4 @@ module.exports = {
8079
parserOptions: {
8180
parser: '@typescript-eslint/parser',
8281
},
83-
overrides: [
84-
{
85-
files: '*.d.ts',
86-
rules: {
87-
'@typescript-eslint/indent': ['error', 4],
88-
},
89-
},
90-
],
9182
};

src/components/Notifications.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { HTMLAttributes, PropType, SlotsType, TransitionGroup, TransitionGroupProps, computed, defineComponent, onMounted, ref } from 'vue';
22
import { params } from '@/params';
3-
import { Id, listToDirection, Timer, NotificationItemWithTimer, emitter, parse } from '@/utils';
3+
import { Id, listToDirection, emitter, parse } from '@/utils';
44
import defaults from '@/defaults';
55
import { NotificationItem, NotificationsOptions } from '@/types';
6+
import { createTimer, NotificationItemWithTimer } from '@/utils/timer';
67
import './Notifications.css';
78

89
const STATE = {
@@ -119,7 +120,6 @@ export default defineComponent({
119120
}>,
120121
setup: (props, { emit, slots, expose }) => {
121122
const list = ref<NotificationItemExtended[]>([]);
122-
const timerControl = ref<Timer | null>(null);
123123
const velocity = params.get('velocity');
124124

125125
const isVA = computed(() => {
@@ -179,14 +179,14 @@ export default defineComponent({
179179
}
180180
};
181181

182-
const pauseTimeout = () => {
182+
const pauseTimeout = (item: NotificationItemExtended): undefined => {
183183
if (props.pauseOnHover) {
184-
timerControl.value?.pause();
184+
item.timer?.stop();
185185
}
186186
};
187-
const resumeTimeout = () => {
187+
const resumeTimeout = (item: NotificationItemExtended): undefined => {
188188
if (props.pauseOnHover) {
189-
timerControl.value?.resume();
189+
item.timer?.start();
190190
}
191191
};
192192
const addItem = (event: NotificationsOptions = {}): void => {
@@ -229,7 +229,7 @@ export default defineComponent({
229229
};
230230

231231
if (duration >= 0) {
232-
timerControl.value = new Timer(() => destroy(item), item.length, item);
232+
item.timer = createTimer(() => destroy(item), item.length);
233233
}
234234

235235
const botToTop = 'bottom' in styles.value;
@@ -289,7 +289,7 @@ export default defineComponent({
289289
};
290290

291291
const destroy = (item: NotificationItemExtended): void => {
292-
clearTimeout(item.timer);
292+
item.timer?.stop();
293293
item.state = STATE.DESTROYED;
294294

295295
clean();
@@ -372,8 +372,8 @@ export default defineComponent({
372372
class='vue-notification-wrapper'
373373
style={notifyWrapperStyle(item)}
374374
data-id={item.id}
375-
onMouseenter={pauseTimeout}
376-
onMouseleave={resumeTimeout}
375+
onMouseenter={() => pauseTimeout(item)}
376+
onMouseleave={() => resumeTimeout(item)}
377377
>
378378
{
379379
slots.body ? slots.body({

src/utils/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export * from './timer';
21
export * from './emitter';
32
export * from './parser';
43

src/utils/timer.ts

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
11
import { NotificationItem } from '@/types';
22

3-
export type NotificationItemWithTimer = NotificationItem & {
4-
timer?: number;
3+
interface Timer {
4+
start: () => void;
5+
stop: () => void;
56
}
67

7-
export class Timer {
8-
private start!: number;
9-
private remaining: number;
10-
private notifyItem: NotificationItemWithTimer;
11-
private callback: () => void;
12-
13-
constructor(callback: () => void, delay: number, notifyItem: NotificationItemWithTimer) {
14-
this.remaining = delay;
15-
this.callback = callback;
16-
this.notifyItem = notifyItem;
17-
this.resume();
18-
}
19-
20-
pause(): void {
21-
clearTimeout(this.notifyItem.timer);
22-
this.remaining -= Date.now() - this.start;
23-
}
24-
25-
resume(): void {
26-
this.start = Date.now();
27-
clearTimeout(this.notifyItem.timer);
28-
// @ts-ignore FIXME Node.js timer type
29-
this.notifyItem.timer = setTimeout(this.callback, this.remaining);
30-
}
8+
9+
export type NotificationItemWithTimer = NotificationItem & {
10+
timer?: Timer;
3111
}
12+
13+
export const createTimer = (callback: () => void, delay: number): Timer => {
14+
let timer: number;
15+
let startTime: number;
16+
let remainingTime = delay;
17+
18+
const start = () => {
19+
startTime = Date.now();
20+
timer = setTimeout(callback, remainingTime);
21+
};
22+
23+
const stop = () => {
24+
clearTimeout(timer);
25+
remainingTime -= Date.now() - startTime;
26+
};
27+
28+
start();
29+
30+
return {
31+
start,
32+
stop,
33+
};
34+
};

test/unit/specs/Notifications.spec.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,134 @@ describe('Notifications', () => {
459459
});
460460
});
461461

462+
describe('features', () => {
463+
describe('pauseOnHover', () => {
464+
describe('when pauseOnHover is true', () => {
465+
const duration = 50;
466+
const speed = 25;
467+
468+
const props = {
469+
pauseOnHover: true,
470+
duration,
471+
speed,
472+
};
473+
474+
it('pause timer', async () => {
475+
const wrapper = mount(Notifications, { props });
476+
477+
const event = {
478+
title: 'Title',
479+
text: 'Text',
480+
type: 'success',
481+
};
482+
483+
wrapper.vm.addItem(event);
484+
485+
vi.useFakeTimers();
486+
487+
await wrapper.vm.$nextTick();
488+
489+
const [notification] = wrapper.findAll('.vue-notification-wrapper');
490+
notification.trigger('mouseenter');
491+
492+
await vi.runAllTimersAsync();
493+
494+
expect(wrapper.vm.list.length).toBe(1);
495+
});
496+
497+
it('resume timer', async () => {
498+
const wrapper = mount(Notifications, { props });
499+
500+
const event = {
501+
title: 'Title',
502+
text: 'Text',
503+
type: 'success',
504+
};
505+
vi.useFakeTimers();
506+
507+
wrapper.vm.addItem(event);
508+
509+
await wrapper.vm.$nextTick();
510+
511+
const [notification] = wrapper.findAll('.vue-notification-wrapper');
512+
notification.trigger('mouseenter');
513+
await wrapper.vm.$nextTick();
514+
notification.trigger('mouseleave');
515+
516+
await vi.runAllTimersAsync();
517+
518+
expect(wrapper.vm.list.length).toBe(0);
519+
});
520+
521+
it('pause exact notification', async () => {
522+
const wrapper = mount(Notifications, { props });
523+
524+
const event1 = {
525+
title: 'Title1',
526+
text: 'Text1',
527+
type: 'success',
528+
};
529+
530+
const event2 = {
531+
title: 'Title2',
532+
text: 'Text2',
533+
type: 'success',
534+
};
535+
vi.useFakeTimers();
536+
537+
wrapper.vm.addItem(event1);
538+
wrapper.vm.addItem(event2);
539+
await wrapper.vm.$nextTick();
540+
expect(wrapper.vm.list.length).toBe(2);
541+
542+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
543+
const [_, notification] = wrapper.findAll('.vue-notification-wrapper');
544+
notification.trigger('mouseenter');
545+
546+
await vi.runAllTimersAsync();
547+
548+
expect(wrapper.vm.list.length).toBe(1);
549+
expect(wrapper.vm.list[0].title).toBe('Title1');
550+
});
551+
});
552+
553+
describe('when pauseOnHover is false', () => {
554+
const duration = 50;
555+
const speed = 25;
556+
557+
const props = {
558+
pauseOnHover: false,
559+
duration,
560+
speed,
561+
};
562+
563+
it('does not pause timer', async () => {
564+
const wrapper = mount(Notifications, { props });
565+
566+
const event = {
567+
title: 'Title',
568+
text: 'Text',
569+
type: 'success',
570+
};
571+
572+
wrapper.vm.addItem(event);
573+
574+
vi.useFakeTimers();
575+
576+
await wrapper.vm.$nextTick();
577+
578+
const [notification] = wrapper.findAll('.vue-notification-wrapper');
579+
notification.trigger('mouseenter');
580+
581+
await vi.runAllTimersAsync();
582+
583+
expect(wrapper.vm.list.length).toBe(0);
584+
});
585+
586+
});
587+
});
588+
});
589+
462590
describe('with velocity animation library', () => {
463591
const velocity = vi.fn();
464592
config.global.plugins = [[Plugin, { velocity }]];

0 commit comments

Comments
 (0)