Skip to content

Commit

Permalink
fix: pause timer with multiple impressions (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyvg committed Oct 28, 2024
1 parent e32f7a2 commit 78075d0
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 46 deletions.
9 changes: 0 additions & 9 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ module.exports = {
rules: {
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/indent': ['error', 2],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
'@typescript-eslint/prefer-interface': 'off',
Expand Down Expand Up @@ -80,12 +79,4 @@ module.exports = {
parserOptions: {
parser: '@typescript-eslint/parser',
},
overrides: [
{
files: '*.d.ts',
rules: {
'@typescript-eslint/indent': ['error', 4],
},
},
],
};
20 changes: 10 additions & 10 deletions src/components/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { HTMLAttributes, PropType, SlotsType, TransitionGroup, TransitionGroupProps, computed, defineComponent, onMounted, ref } from 'vue';
import { params } from '@/params';
import { Id, listToDirection, Timer, NotificationItemWithTimer, emitter, parse } from '@/utils';
import { Id, listToDirection, emitter, parse } from '@/utils';
import defaults from '@/defaults';
import { NotificationItem, NotificationsOptions } from '@/types';
import { createTimer, NotificationItemWithTimer } from '@/utils/timer';
import './Notifications.css';

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

const isVA = computed(() => {
Expand Down Expand Up @@ -179,14 +179,14 @@ export default defineComponent({
}
};

const pauseTimeout = () => {
const pauseTimeout = (item: NotificationItemExtended): undefined => {
if (props.pauseOnHover) {
timerControl.value?.pause();
item.timer?.stop();
}
};
const resumeTimeout = () => {
const resumeTimeout = (item: NotificationItemExtended): undefined => {
if (props.pauseOnHover) {
timerControl.value?.resume();
item.timer?.start();
}
};
const addItem = (event: NotificationsOptions = {}): void => {
Expand Down Expand Up @@ -229,7 +229,7 @@ export default defineComponent({
};

if (duration >= 0) {
timerControl.value = new Timer(() => destroy(item), item.length, item);
item.timer = createTimer(() => destroy(item), item.length);
}

const botToTop = 'bottom' in styles.value;
Expand Down Expand Up @@ -289,7 +289,7 @@ export default defineComponent({
};

const destroy = (item: NotificationItemExtended): void => {
clearTimeout(item.timer);
item.timer?.stop();
item.state = STATE.DESTROYED;

clean();
Expand Down Expand Up @@ -372,8 +372,8 @@ export default defineComponent({
class='vue-notification-wrapper'
style={notifyWrapperStyle(item)}
data-id={item.id}
onMouseenter={pauseTimeout}
onMouseleave={resumeTimeout}
onMouseenter={() => pauseTimeout(item)}
onMouseleave={() => resumeTimeout(item)}
>
{
slots.body ? slots.body({
Expand Down
1 change: 0 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './timer';
export * from './emitter';
export * from './parser';

Expand Down
55 changes: 29 additions & 26 deletions src/utils/timer.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import { NotificationItem } from '@/types';

export type NotificationItemWithTimer = NotificationItem & {
timer?: number;
interface Timer {
start: () => void;
stop: () => void;
}

export class Timer {
private start!: number;
private remaining: number;
private notifyItem: NotificationItemWithTimer;
private callback: () => void;

constructor(callback: () => void, delay: number, notifyItem: NotificationItemWithTimer) {
this.remaining = delay;
this.callback = callback;
this.notifyItem = notifyItem;
this.resume();
}

pause(): void {
clearTimeout(this.notifyItem.timer);
this.remaining -= Date.now() - this.start;
}

resume(): void {
this.start = Date.now();
clearTimeout(this.notifyItem.timer);
// @ts-ignore FIXME Node.js timer type
this.notifyItem.timer = setTimeout(this.callback, this.remaining);
}

export type NotificationItemWithTimer = NotificationItem & {
timer?: Timer;
}

export const createTimer = (callback: () => void, delay: number): Timer => {
let timer: number;
let startTime: number;
let remainingTime = delay;

const start = () => {
startTime = Date.now();
timer = setTimeout(callback, remainingTime);
};

const stop = () => {
clearTimeout(timer);
remainingTime -= Date.now() - startTime;
};

start();

return {
start,
stop,
};
};
128 changes: 128 additions & 0 deletions test/unit/specs/Notifications.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,134 @@ describe('Notifications', () => {
});
});

describe('features', () => {
describe('pauseOnHover', () => {
describe('when pauseOnHover is true', () => {
const duration = 50;
const speed = 25;

const props = {
pauseOnHover: true,
duration,
speed,
};

it('pause timer', async () => {
const wrapper = mount(Notifications, { props });

const event = {
title: 'Title',
text: 'Text',
type: 'success',
};

wrapper.vm.addItem(event);

vi.useFakeTimers();

await wrapper.vm.$nextTick();

const [notification] = wrapper.findAll('.vue-notification-wrapper');
notification.trigger('mouseenter');

await vi.runAllTimersAsync();

expect(wrapper.vm.list.length).toBe(1);
});

it('resume timer', async () => {
const wrapper = mount(Notifications, { props });

const event = {
title: 'Title',
text: 'Text',
type: 'success',
};
vi.useFakeTimers();

wrapper.vm.addItem(event);

await wrapper.vm.$nextTick();

const [notification] = wrapper.findAll('.vue-notification-wrapper');
notification.trigger('mouseenter');
await wrapper.vm.$nextTick();
notification.trigger('mouseleave');

await vi.runAllTimersAsync();

expect(wrapper.vm.list.length).toBe(0);
});

it('pause exact notification', async () => {
const wrapper = mount(Notifications, { props });

const event1 = {
title: 'Title1',
text: 'Text1',
type: 'success',
};

const event2 = {
title: 'Title2',
text: 'Text2',
type: 'success',
};
vi.useFakeTimers();

wrapper.vm.addItem(event1);
wrapper.vm.addItem(event2);
await wrapper.vm.$nextTick();
expect(wrapper.vm.list.length).toBe(2);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, notification] = wrapper.findAll('.vue-notification-wrapper');
notification.trigger('mouseenter');

await vi.runAllTimersAsync();

expect(wrapper.vm.list.length).toBe(1);
expect(wrapper.vm.list[0].title).toBe('Title1');
});
});

describe('when pauseOnHover is false', () => {
const duration = 50;
const speed = 25;

const props = {
pauseOnHover: false,
duration,
speed,
};

it('does not pause timer', async () => {
const wrapper = mount(Notifications, { props });

const event = {
title: 'Title',
text: 'Text',
type: 'success',
};

wrapper.vm.addItem(event);

vi.useFakeTimers();

await wrapper.vm.$nextTick();

const [notification] = wrapper.findAll('.vue-notification-wrapper');
notification.trigger('mouseenter');

await vi.runAllTimersAsync();

expect(wrapper.vm.list.length).toBe(0);
});

});
});
});

describe('with velocity animation library', () => {
const velocity = vi.fn();
config.global.plugins = [[Plugin, { velocity }]];
Expand Down

0 comments on commit 78075d0

Please sign in to comment.