From 2d01f84c93e0f8f378a51f7e6bb7404219cab900 Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Wed, 2 Jul 2025 19:48:33 +0200 Subject: [PATCH 1/9] refactor(useKeydown): optimize keydown event handling with registrar pattern --- .../v0/src/composables/useKeydown/index.ts | 96 ++++++++++++++++--- .../composables/useKeydown/useKeydown.test.ts | 91 ++++++++++++++++++ 2 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 packages/v0/src/composables/useKeydown/useKeydown.test.ts diff --git a/packages/v0/src/composables/useKeydown/index.ts b/packages/v0/src/composables/useKeydown/index.ts index 12625436..70c2bc78 100644 --- a/packages/v0/src/composables/useKeydown/index.ts +++ b/packages/v0/src/composables/useKeydown/index.ts @@ -1,4 +1,7 @@ -import { onMounted, getCurrentScope, onScopeDispose } from 'vue' +import { onMounted, getCurrentScope, onScopeDispose, ref, readonly } from 'vue' +import { useRegistrar } from '../useRegistrar' +import type { ID } from '#v0/types' +import type { RegistrarItem, RegistrarTicket, RegistrarContext } from '../useRegistrar' export interface KeyHandler { key: string @@ -7,27 +10,93 @@ export interface KeyHandler { stopPropagation?: boolean } -export function useKeydown (handlers: KeyHandler[] | KeyHandler) { - const keyHandlers = Array.isArray(handlers) ? handlers : [handlers] +export interface UseKeydownOptions { + autoStart?: boolean +} + +interface KeydownRegistrarItem extends RegistrarItem { + key: string + handler: (event: KeyboardEvent) => void + preventDefault?: boolean + stopPropagation?: boolean +} + +interface KeydownRegistrarTicket extends RegistrarTicket { + key: string + handler: (event: KeyboardEvent) => void + preventDefault?: boolean + stopPropagation?: boolean +} + +interface KeydownRegistrarContext extends RegistrarContext {} + +const [ + _, + __, + keydownRegistrar, +] = useRegistrar('keydown') - const onKeydown = (event: KeyboardEvent) => { - const handler = keyHandlers.find(h => h.key === event.key) - if (handler) { - if (handler.preventDefault) event.preventDefault() - if (handler.stopPropagation) event.stopPropagation() - handler.handler(event) +let globalListener: ((event: KeyboardEvent) => void) | null = null + +function startGlobalListener () { + if (typeof document === 'undefined') return + + if (!globalListener) { + globalListener = (event: KeyboardEvent) => { + for (const h of keydownRegistrar.registeredItems.values()) { + if (h.key === event.key) { + if (h.preventDefault) event.preventDefault() + if (h.stopPropagation) event.stopPropagation() + h.handler(event) + } + } } + document.addEventListener('keydown', globalListener) + } +} + +function stopGlobalListener () { + if (typeof document === 'undefined') return + + if (globalListener && keydownRegistrar.registeredItems.size === 0) { + document.removeEventListener('keydown', globalListener) + globalListener = null } +} + +export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKeydownOptions = {}) { + const { autoStart = true } = options + const keyHandlers = Array.isArray(handlers) ? handlers : [handlers] + const handlerIds = ref([]) + const isListening = ref(false) const startListening = () => { - document.addEventListener('keydown', onKeydown) + if (!isListening.value) { + handlerIds.value = keyHandlers.map(handler => { + const ticket = keydownRegistrar.register(handler) + Object.assign(ticket, handler) + return ticket.id + }) + + if (keydownRegistrar.registeredItems.size > 0) { + startGlobalListener() + } + + isListening.value = true + } } const stopListening = () => { - document.removeEventListener('keydown', onKeydown) + if (isListening.value) { + for (const id of handlerIds.value) keydownRegistrar.unregister(id) + handlerIds.value = [] + isListening.value = false + + stopGlobalListener() + } } - if (getCurrentScope()) { + if (getCurrentScope() && autoStart) { onMounted(startListening) } @@ -36,5 +105,8 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler) { return { startListening, stopListening, + isListening: readonly(isListening), } } + +export { keydownRegistrar } diff --git a/packages/v0/src/composables/useKeydown/useKeydown.test.ts b/packages/v0/src/composables/useKeydown/useKeydown.test.ts new file mode 100644 index 00000000..d0d33073 --- /dev/null +++ b/packages/v0/src/composables/useKeydown/useKeydown.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { useKeydown, keydownRegistrar } from './index' + +describe('useKeydown optimization', () => { + let addEventListenerSpy: ReturnType + let removeEventListenerSpy: ReturnType + + beforeEach(() => { + vi.clearAllMocks() + addEventListenerSpy = vi.spyOn(document, 'addEventListener') + removeEventListenerSpy = vi.spyOn(document, 'removeEventListener') + }) + + afterEach(() => { + vi.restoreAllMocks() + if (keydownRegistrar && keydownRegistrar.registeredItems.size > 0) { + keydownRegistrar.registeredItems.clear() + } + }) + + it('should create only one global listener for multiple useKeydown calls', () => { + const handler1 = vi.fn() + const handler2 = vi.fn() + const handler3 = vi.fn() + + const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) + const keydown2 = useKeydown({ key: 'Escape', handler: handler2 }) + const keydown3 = useKeydown({ key: 'Space', handler: handler3 }) + + keydown1.startListening() + keydown2.startListening() + keydown3.startListening() + + expect(addEventListenerSpy).toHaveBeenCalledTimes(1) + expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function)) + + expect(keydownRegistrar.registeredItems.size).toBe(3) + + keydown1.stopListening() + keydown2.stopListening() + keydown3.stopListening() + + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) + expect(keydownRegistrar.registeredItems.size).toBe(0) + }) + + it('should handle multiple handlers for the same key', () => { + const handler1 = vi.fn() + const handler2 = vi.fn() + + const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) + const keydown2 = useKeydown({ key: 'Enter', handler: handler2 }) + + keydown1.startListening() + keydown2.startListening() + + const event = { key: 'Enter' } as KeyboardEvent + + const registeredHandler = addEventListenerSpy.mock.calls[0][1] as (event: KeyboardEvent) => void + registeredHandler(event) + + expect(handler1).toHaveBeenCalledWith(event) + expect(handler2).toHaveBeenCalledWith(event) + + keydown1.stopListening() + keydown2.stopListening() + }) + + it('should not remove global listener if other handlers are still active', () => { + const handler1 = vi.fn() + const handler2 = vi.fn() + + const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) + const keydown2 = useKeydown({ key: 'Escape', handler: handler2 }) + + keydown1.startListening() + keydown2.startListening() + + expect(addEventListenerSpy).toHaveBeenCalledTimes(1) + + keydown1.stopListening() + + expect(removeEventListenerSpy).not.toHaveBeenCalled() + expect(keydownRegistrar.registeredItems.size).toBe(1) + + keydown2.stopListening() + + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) + expect(keydownRegistrar.registeredItems.size).toBe(0) + }) +}) From dd63e72e4cf07721de2a7b736681d4be2b13525b Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Wed, 2 Jul 2025 20:27:29 +0200 Subject: [PATCH 2/9] refactor: drop registrar --- .../v0/src/composables/useKeydown/index.ts | 47 ++++++------------- .../composables/useKeydown/useKeydown.test.ts | 14 +++--- 2 files changed, 20 insertions(+), 41 deletions(-) diff --git a/packages/v0/src/composables/useKeydown/index.ts b/packages/v0/src/composables/useKeydown/index.ts index 70c2bc78..512785f9 100644 --- a/packages/v0/src/composables/useKeydown/index.ts +++ b/packages/v0/src/composables/useKeydown/index.ts @@ -1,7 +1,5 @@ import { onMounted, getCurrentScope, onScopeDispose, ref, readonly } from 'vue' -import { useRegistrar } from '../useRegistrar' import type { ID } from '#v0/types' -import type { RegistrarItem, RegistrarTicket, RegistrarContext } from '../useRegistrar' export interface KeyHandler { key: string @@ -14,36 +12,15 @@ export interface UseKeydownOptions { autoStart?: boolean } -interface KeydownRegistrarItem extends RegistrarItem { - key: string - handler: (event: KeyboardEvent) => void - preventDefault?: boolean - stopPropagation?: boolean -} - -interface KeydownRegistrarTicket extends RegistrarTicket { - key: string - handler: (event: KeyboardEvent) => void - preventDefault?: boolean - stopPropagation?: boolean -} - -interface KeydownRegistrarContext extends RegistrarContext {} - -const [ - _, - __, - keydownRegistrar, -] = useRegistrar('keydown') - let globalListener: ((event: KeyboardEvent) => void) | null = null +const handlerMap: Map = new Map() function startGlobalListener () { if (typeof document === 'undefined') return if (!globalListener) { globalListener = (event: KeyboardEvent) => { - for (const h of keydownRegistrar.registeredItems.values()) { + for (const h of handlerMap.values()) { if (h.key === event.key) { if (h.preventDefault) event.preventDefault() if (h.stopPropagation) event.stopPropagation() @@ -58,7 +35,7 @@ function startGlobalListener () { function stopGlobalListener () { if (typeof document === 'undefined') return - if (globalListener && keydownRegistrar.registeredItems.size === 0) { + if (globalListener && handlerMap.size === 0) { document.removeEventListener('keydown', globalListener) globalListener = null } @@ -73,12 +50,12 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKey const startListening = () => { if (!isListening.value) { handlerIds.value = keyHandlers.map(handler => { - const ticket = keydownRegistrar.register(handler) - Object.assign(ticket, handler) - return ticket.id + const id = crypto.randomUUID() + handlerMap.set(id, handler) + return id }) - if (keydownRegistrar.registeredItems.size > 0) { + if (handlerMap.size > 0) { startGlobalListener() } @@ -88,11 +65,15 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKey const stopListening = () => { if (isListening.value) { - for (const id of handlerIds.value) keydownRegistrar.unregister(id) + for (const id of handlerIds.value) { + handlerMap.delete(id) + } handlerIds.value = [] isListening.value = false - stopGlobalListener() + if (handlerMap.size === 0) { + stopGlobalListener() + } } } @@ -109,4 +90,4 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKey } } -export { keydownRegistrar } +export { handlerMap } diff --git a/packages/v0/src/composables/useKeydown/useKeydown.test.ts b/packages/v0/src/composables/useKeydown/useKeydown.test.ts index d0d33073..e588ad1f 100644 --- a/packages/v0/src/composables/useKeydown/useKeydown.test.ts +++ b/packages/v0/src/composables/useKeydown/useKeydown.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import { useKeydown, keydownRegistrar } from './index' +import { useKeydown, handlerMap } from './index' describe('useKeydown optimization', () => { let addEventListenerSpy: ReturnType @@ -13,9 +13,7 @@ describe('useKeydown optimization', () => { afterEach(() => { vi.restoreAllMocks() - if (keydownRegistrar && keydownRegistrar.registeredItems.size > 0) { - keydownRegistrar.registeredItems.clear() - } + handlerMap.clear() }) it('should create only one global listener for multiple useKeydown calls', () => { @@ -34,14 +32,14 @@ describe('useKeydown optimization', () => { expect(addEventListenerSpy).toHaveBeenCalledTimes(1) expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function)) - expect(keydownRegistrar.registeredItems.size).toBe(3) + expect(handlerMap.size).toBe(3) keydown1.stopListening() keydown2.stopListening() keydown3.stopListening() expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) - expect(keydownRegistrar.registeredItems.size).toBe(0) + expect(handlerMap.size).toBe(0) }) it('should handle multiple handlers for the same key', () => { @@ -81,11 +79,11 @@ describe('useKeydown optimization', () => { keydown1.stopListening() expect(removeEventListenerSpy).not.toHaveBeenCalled() - expect(keydownRegistrar.registeredItems.size).toBe(1) + expect(handlerMap.size).toBe(1) keydown2.stopListening() expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) - expect(keydownRegistrar.registeredItems.size).toBe(0) + expect(handlerMap.size).toBe(0) }) }) From 789ed35f67962a7aaa07e4c2baa384fe51011f50 Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Wed, 2 Jul 2025 20:30:29 +0200 Subject: [PATCH 3/9] refactor: remove side-effect from map --- packages/v0/src/composables/useKeydown/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/v0/src/composables/useKeydown/index.ts b/packages/v0/src/composables/useKeydown/index.ts index 512785f9..ef2c80a0 100644 --- a/packages/v0/src/composables/useKeydown/index.ts +++ b/packages/v0/src/composables/useKeydown/index.ts @@ -49,11 +49,13 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKey const startListening = () => { if (!isListening.value) { - handlerIds.value = keyHandlers.map(handler => { - const id = crypto.randomUUID() - handlerMap.set(id, handler) - return id - }) + const ids = Array.from({ length: keyHandlers.length }, () => crypto.randomUUID()) + + for (const [index, id] of ids.entries()) { + handlerMap.set(id, keyHandlers[index]) + } + + handlerIds.value = ids if (handlerMap.size > 0) { startGlobalListener() From 4d41b313e25e1f605869e2c5d398390385978e7b Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Wed, 2 Jul 2025 22:30:42 +0200 Subject: [PATCH 4/9] perf: use shallowReadonly instead of readonly --- packages/v0/src/composables/useKeydown/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/v0/src/composables/useKeydown/index.ts b/packages/v0/src/composables/useKeydown/index.ts index ef2c80a0..8fc89604 100644 --- a/packages/v0/src/composables/useKeydown/index.ts +++ b/packages/v0/src/composables/useKeydown/index.ts @@ -1,4 +1,4 @@ -import { onMounted, getCurrentScope, onScopeDispose, ref, readonly } from 'vue' +import { onMounted, getCurrentScope, onScopeDispose, ref, shallowReadonly } from 'vue' import type { ID } from '#v0/types' export interface KeyHandler { @@ -88,7 +88,7 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKey return { startListening, stopListening, - isListening: readonly(isListening), + isListening: shallowReadonly(isListening), } } From 90236131dab4e24b880ff88f018db41fdda8fe19 Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Mon, 7 Jul 2025 22:03:22 +0200 Subject: [PATCH 5/9] chore: use dedicated vitest configs --- packages/0/vitest.config.ts | 15 +++++++++++++++ packages/paper/vitest.config.ts | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/0/vitest.config.ts create mode 100644 packages/paper/vitest.config.ts diff --git a/packages/0/vitest.config.ts b/packages/0/vitest.config.ts new file mode 100644 index 00000000..df09ec37 --- /dev/null +++ b/packages/0/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config' +import { fileURLToPath } from 'node:url' + +export default defineConfig({ + resolve: { + alias: { + '@': fileURLToPath(new URL('src', import.meta.url)), + '#v0': fileURLToPath(new URL('src', import.meta.url)), + }, + }, + test: { + environment: 'happy-dom', + globals: true, + }, +}) diff --git a/packages/paper/vitest.config.ts b/packages/paper/vitest.config.ts new file mode 100644 index 00000000..5e00c98e --- /dev/null +++ b/packages/paper/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' +import { fileURLToPath } from 'node:url' + +export default defineConfig({ + resolve: { + alias: { + '@': fileURLToPath(new URL('src', import.meta.url)), + '#paper': fileURLToPath(new URL('src', import.meta.url)), + '#v0': fileURLToPath(new URL('../0/src', import.meta.url)), + }, + }, + test: { + environment: 'happy-dom', + globals: true, + }, +}) From edd8854f771110d1cc4cfab1318abcdc4de3e752 Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Mon, 7 Jul 2025 22:03:46 +0200 Subject: [PATCH 6/9] test: port tests from prev file --- .../src/composables/useKeydown/index.test.ts | 154 +++++++----------- .../composables/useKeydown/useKeydown.test.ts | 89 ---------- 2 files changed, 56 insertions(+), 187 deletions(-) delete mode 100644 packages/0/src/composables/useKeydown/useKeydown.test.ts diff --git a/packages/0/src/composables/useKeydown/index.test.ts b/packages/0/src/composables/useKeydown/index.test.ts index c1569d34..d9be59ce 100644 --- a/packages/0/src/composables/useKeydown/index.test.ts +++ b/packages/0/src/composables/useKeydown/index.test.ts @@ -1,131 +1,89 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { useKeydown } from './index' - -// Mock Vue's lifecycle hooks -vi.mock('vue', async () => { - const actual = await vi.importActual('vue') - return { - ...actual, - onMounted: vi.fn(), - getCurrentScope: vi.fn(() => true), - onScopeDispose: vi.fn(), - } -}) +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { useKeydown, handlerMap } from './index' describe('useKeydown', () => { + let addEventListenerSpy: ReturnType + let removeEventListenerSpy: ReturnType + beforeEach(() => { vi.clearAllMocks() - // Mock document.addEventListener and removeEventListener - vi.spyOn(document, 'addEventListener') - vi.spyOn(document, 'removeEventListener') + addEventListenerSpy = vi.spyOn(document, 'addEventListener') + removeEventListenerSpy = vi.spyOn(document, 'removeEventListener') }) afterEach(() => { vi.restoreAllMocks() + handlerMap.clear() }) - describe('basic functionality', () => { - it('should return startListening and stopListening functions', () => { - const handler = { key: 'Enter', handler: vi.fn() } - const result = useKeydown(handler) - - expect(result).toHaveProperty('startListening') - expect(result).toHaveProperty('stopListening') - expect(typeof result.startListening).toBe('function') - expect(typeof result.stopListening).toBe('function') - }) - - it('should accept single handler', () => { - const handler = { key: 'Enter', handler: vi.fn() } - - expect(() => useKeydown(handler)).not.toThrow() - }) - - it('should accept array of handlers', () => { - const handlers = [ - { key: 'Enter', handler: vi.fn() }, - { key: 'Escape', handler: vi.fn() }, - ] - - expect(() => useKeydown(handlers)).not.toThrow() - }) - }) - - describe('event handling', () => { - it('should call handler when matching key is pressed', () => { - const mockHandler = vi.fn() - const handler = { key: 'Enter', handler: mockHandler } - const { startListening } = useKeydown(handler) + it('should create only one global listener for multiple useKeydown calls', () => { + const handler1 = vi.fn() + const handler2 = vi.fn() + const handler3 = vi.fn() - startListening() + const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) + const keydown2 = useKeydown({ key: 'Escape', handler: handler2 }) + const keydown3 = useKeydown({ key: 'Space', handler: handler3 }) - // Simulate keydown event - const event = new KeyboardEvent('keydown', { key: 'Enter' }) - document.dispatchEvent(event) + keydown1.startListening() + keydown2.startListening() + keydown3.startListening() - expect(mockHandler).toHaveBeenCalledWith(event) - }) + expect(addEventListenerSpy).toHaveBeenCalledTimes(1) + expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function)) - it('should not call handler when non-matching key is pressed', () => { - const mockHandler = vi.fn() - const handler = { key: 'Enter', handler: mockHandler } - const { startListening } = useKeydown(handler) + expect(handlerMap.size).toBe(3) - startListening() + keydown1.stopListening() + keydown2.stopListening() + keydown3.stopListening() - // Simulate keydown event with different key - const event = new KeyboardEvent('keydown', { key: 'Escape' }) - document.dispatchEvent(event) + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) + expect(handlerMap.size).toBe(0) + }) - expect(mockHandler).not.toHaveBeenCalled() - }) + it('should handle multiple handlers for the same key', () => { + const handler1 = vi.fn() + const handler2 = vi.fn() - it('should prevent default when preventDefault is true', () => { - const mockHandler = vi.fn() - const handler = { key: 'Enter', handler: mockHandler, preventDefault: true } - const { startListening } = useKeydown(handler) + const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) + const keydown2 = useKeydown({ key: 'Enter', handler: handler2 }) - startListening() + keydown1.startListening() + keydown2.startListening() - const event = new KeyboardEvent('keydown', { key: 'Enter' }) - const preventDefaultSpy = vi.spyOn(event, 'preventDefault') - document.dispatchEvent(event) + const event = { key: 'Enter' } as KeyboardEvent - expect(preventDefaultSpy).toHaveBeenCalled() - }) + const registeredHandler = addEventListenerSpy.mock.calls[0][1] as (event: KeyboardEvent) => void + registeredHandler(event) - it('should stop propagation when stopPropagation is true', () => { - const mockHandler = vi.fn() - const handler = { key: 'Enter', handler: mockHandler, stopPropagation: true } - const { startListening } = useKeydown(handler) + expect(handler1).toHaveBeenCalledWith(event) + expect(handler2).toHaveBeenCalledWith(event) - startListening() + keydown1.stopListening() + keydown2.stopListening() + }) - const event = new KeyboardEvent('keydown', { key: 'Enter' }) - const stopPropagationSpy = vi.spyOn(event, 'stopPropagation') - document.dispatchEvent(event) + it('should not remove global listener if other handlers are still active', () => { + const handler1 = vi.fn() + const handler2 = vi.fn() - expect(stopPropagationSpy).toHaveBeenCalled() - }) - }) + const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) + const keydown2 = useKeydown({ key: 'Escape', handler: handler2 }) - describe('lifecycle management', () => { - it('should add event listener when startListening is called', () => { - const handler = { key: 'Enter', handler: vi.fn() } - const { startListening } = useKeydown(handler) + keydown1.startListening() + keydown2.startListening() - startListening() + expect(addEventListenerSpy).toHaveBeenCalledTimes(1) - expect(document.addEventListener).toHaveBeenCalledWith('keydown', expect.any(Function)) - }) + keydown1.stopListening() - it('should remove event listener when stopListening is called', () => { - const handler = { key: 'Enter', handler: vi.fn() } - const { stopListening } = useKeydown(handler) + expect(removeEventListenerSpy).not.toHaveBeenCalled() + expect(handlerMap.size).toBe(1) - stopListening() + keydown2.stopListening() - expect(document.removeEventListener).toHaveBeenCalledWith('keydown', expect.any(Function)) - }) + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) + expect(handlerMap.size).toBe(0) }) }) diff --git a/packages/0/src/composables/useKeydown/useKeydown.test.ts b/packages/0/src/composables/useKeydown/useKeydown.test.ts deleted file mode 100644 index e588ad1f..00000000 --- a/packages/0/src/composables/useKeydown/useKeydown.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import { useKeydown, handlerMap } from './index' - -describe('useKeydown optimization', () => { - let addEventListenerSpy: ReturnType - let removeEventListenerSpy: ReturnType - - beforeEach(() => { - vi.clearAllMocks() - addEventListenerSpy = vi.spyOn(document, 'addEventListener') - removeEventListenerSpy = vi.spyOn(document, 'removeEventListener') - }) - - afterEach(() => { - vi.restoreAllMocks() - handlerMap.clear() - }) - - it('should create only one global listener for multiple useKeydown calls', () => { - const handler1 = vi.fn() - const handler2 = vi.fn() - const handler3 = vi.fn() - - const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) - const keydown2 = useKeydown({ key: 'Escape', handler: handler2 }) - const keydown3 = useKeydown({ key: 'Space', handler: handler3 }) - - keydown1.startListening() - keydown2.startListening() - keydown3.startListening() - - expect(addEventListenerSpy).toHaveBeenCalledTimes(1) - expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function)) - - expect(handlerMap.size).toBe(3) - - keydown1.stopListening() - keydown2.stopListening() - keydown3.stopListening() - - expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) - expect(handlerMap.size).toBe(0) - }) - - it('should handle multiple handlers for the same key', () => { - const handler1 = vi.fn() - const handler2 = vi.fn() - - const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) - const keydown2 = useKeydown({ key: 'Enter', handler: handler2 }) - - keydown1.startListening() - keydown2.startListening() - - const event = { key: 'Enter' } as KeyboardEvent - - const registeredHandler = addEventListenerSpy.mock.calls[0][1] as (event: KeyboardEvent) => void - registeredHandler(event) - - expect(handler1).toHaveBeenCalledWith(event) - expect(handler2).toHaveBeenCalledWith(event) - - keydown1.stopListening() - keydown2.stopListening() - }) - - it('should not remove global listener if other handlers are still active', () => { - const handler1 = vi.fn() - const handler2 = vi.fn() - - const keydown1 = useKeydown({ key: 'Enter', handler: handler1 }) - const keydown2 = useKeydown({ key: 'Escape', handler: handler2 }) - - keydown1.startListening() - keydown2.startListening() - - expect(addEventListenerSpy).toHaveBeenCalledTimes(1) - - keydown1.stopListening() - - expect(removeEventListenerSpy).not.toHaveBeenCalled() - expect(handlerMap.size).toBe(1) - - keydown2.stopListening() - - expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) - expect(handlerMap.size).toBe(0) - }) -}) From 5ef5ef41bdc2217ab20e3cf2416b411a98be4a0e Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Fri, 11 Jul 2025 16:12:36 +0200 Subject: [PATCH 7/9] fix: use genId --- packages/0/src/composables/useKeydown/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/0/src/composables/useKeydown/index.ts b/packages/0/src/composables/useKeydown/index.ts index 8fc89604..3d328b73 100644 --- a/packages/0/src/composables/useKeydown/index.ts +++ b/packages/0/src/composables/useKeydown/index.ts @@ -1,5 +1,6 @@ import { onMounted, getCurrentScope, onScopeDispose, ref, shallowReadonly } from 'vue' import type { ID } from '#v0/types' +import { genId } from '#v0/utils/helpers' export interface KeyHandler { key: string @@ -49,7 +50,7 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKey const startListening = () => { if (!isListening.value) { - const ids = Array.from({ length: keyHandlers.length }, () => crypto.randomUUID()) + const ids = Array.from({ length: keyHandlers.length }, genId) for (const [index, id] of ids.entries()) { handlerMap.set(id, keyHandlers[index]) From 1dc6fce61787ccbd4b279325710feda2c9ca73a4 Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Fri, 11 Jul 2025 20:55:09 +0200 Subject: [PATCH 8/9] chore: use named function --- packages/0/src/composables/useKeydown/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/0/src/composables/useKeydown/index.ts b/packages/0/src/composables/useKeydown/index.ts index 3d328b73..4c0e890b 100644 --- a/packages/0/src/composables/useKeydown/index.ts +++ b/packages/0/src/composables/useKeydown/index.ts @@ -66,7 +66,7 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKey } } - const stopListening = () => { + function stopListening () { if (isListening.value) { for (const id of handlerIds.value) { handlerMap.delete(id) From 242bb48e94d244b5ea9f03cae07c3d57fa99522d Mon Sep 17 00:00:00 2001 From: Andrei Elkin Date: Sun, 31 Aug 2025 17:50:48 +0200 Subject: [PATCH 9/9] refactor(useKeydown): rename autoStart option to immediate --- packages/0/src/composables/useKeydown/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/0/src/composables/useKeydown/index.ts b/packages/0/src/composables/useKeydown/index.ts index 01eb23be..49240e08 100644 --- a/packages/0/src/composables/useKeydown/index.ts +++ b/packages/0/src/composables/useKeydown/index.ts @@ -10,7 +10,7 @@ export interface KeyHandler { } export interface UseKeydownOptions { - autoStart?: boolean + immediate?: boolean } let globalListener: ((event: KeyboardEvent) => void) | null = null @@ -52,7 +52,7 @@ function stopGlobalListener () { */ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKeydownOptions = {}) { - const { autoStart = true } = options + const { immediate = true } = options const keyHandlers = Array.isArray(handlers) ? handlers : [handlers] const handlerIds = ref([]) const isListening = ref(false) @@ -89,7 +89,7 @@ export function useKeydown (handlers: KeyHandler[] | KeyHandler, options: UseKey } } - if (getCurrentScope() && autoStart) { + if (getCurrentScope() && immediate) { onMounted(startListening) }