diff --git a/.eslintrc.cjs b/.eslintrc.cjs index edf7183..2201b53 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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', @@ -80,12 +79,4 @@ module.exports = { parserOptions: { parser: '@typescript-eslint/parser', }, - overrides: [ - { - files: '*.d.ts', - rules: { - '@typescript-eslint/indent': ['error', 4], - }, - }, - ], }; diff --git a/src/components/Notifications.tsx b/src/components/Notifications.tsx index 8fc3290..51e03a6 100644 --- a/src/components/Notifications.tsx +++ b/src/components/Notifications.tsx @@ -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 = { @@ -119,7 +120,6 @@ export default defineComponent({ }>, setup: (props, { emit, slots, expose }) => { const list = ref([]); - const timerControl = ref(null); const velocity = params.get('velocity'); const isVA = computed(() => { @@ -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 => { @@ -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; @@ -289,7 +289,7 @@ export default defineComponent({ }; const destroy = (item: NotificationItemExtended): void => { - clearTimeout(item.timer); + item.timer?.stop(); item.state = STATE.DESTROYED; clean(); @@ -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({ diff --git a/src/utils/index.ts b/src/utils/index.ts index d315c62..2c06fbe 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,3 @@ -export * from './timer'; export * from './emitter'; export * from './parser'; diff --git a/src/utils/timer.ts b/src/utils/timer.ts index 376db62..867a6d5 100644 --- a/src/utils/timer.ts +++ b/src/utils/timer.ts @@ -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, + }; +}; diff --git a/test/unit/specs/Notifications.spec.ts b/test/unit/specs/Notifications.spec.ts index 12b1119..67a2976 100644 --- a/test/unit/specs/Notifications.spec.ts +++ b/test/unit/specs/Notifications.spec.ts @@ -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 }]];