diff --git a/.changeset/conflict-detection.md b/.changeset/conflict-detection.md new file mode 100644 index 0000000..1e9d614 --- /dev/null +++ b/.changeset/conflict-detection.md @@ -0,0 +1,14 @@ +--- +'@tanstack/keys': minor +--- + +Add hotkey conflict detection with configurable behavior + +Implements conflict detection when registering hotkeys with the same combination on the same target. Adds a new `conflictBehavior` option to `HotkeyOptions`: + +- `'warn'` (default) - Log a warning to console but allow both registrations +- `'error'` - Throw an error and prevent the new registration +- `'replace'` - Unregister the existing hotkey and register the new one +- `'allow'` - Allow multiple registrations without warning + +This addresses the "Warn/error on conflicting shortcuts (TBD)" item from the README. diff --git a/docs/reference/classes/HotkeyManager.md b/docs/reference/classes/HotkeyManager.md index 9ae621c..6d7b7e5 100644 --- a/docs/reference/classes/HotkeyManager.md +++ b/docs/reference/classes/HotkeyManager.md @@ -5,7 +5,7 @@ title: HotkeyManager # Class: HotkeyManager -Defined in: [hotkey-manager.ts:145](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L145) +Defined in: [hotkey-manager.ts:158](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L158) Singleton manager for hotkey registrations. @@ -34,7 +34,7 @@ unregister() destroy(): void; ``` -Defined in: [hotkey-manager.ts:609](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L609) +Defined in: [hotkey-manager.ts:683](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L683) Destroys the manager and removes all listeners. @@ -50,7 +50,7 @@ Destroys the manager and removes all listeners. getRegistrationCount(): number; ``` -Defined in: [hotkey-manager.ts:580](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L580) +Defined in: [hotkey-manager.ts:654](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L654) Gets the number of registered hotkeys. @@ -66,7 +66,7 @@ Gets the number of registered hotkeys. isRegistered(hotkey, target?): boolean; ``` -Defined in: [hotkey-manager.ts:591](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L591) +Defined in: [hotkey-manager.ts:665](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L665) Checks if a specific hotkey is registered. @@ -101,7 +101,7 @@ register( options): HotkeyRegistrationHandle; ``` -Defined in: [hotkey-manager.ts:209](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L209) +Defined in: [hotkey-manager.ts:222](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L222) Registers a hotkey handler and returns a handle for updating the registration. @@ -157,7 +157,7 @@ handle.unregister() static getInstance(): HotkeyManager; ``` -Defined in: [hotkey-manager.ts:167](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L167) +Defined in: [hotkey-manager.ts:180](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L180) Gets the singleton instance of HotkeyManager. @@ -173,7 +173,7 @@ Gets the singleton instance of HotkeyManager. static resetInstance(): void; ``` -Defined in: [hotkey-manager.ts:177](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L177) +Defined in: [hotkey-manager.ts:190](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L190) Resets the singleton instance. Useful for testing. diff --git a/docs/reference/functions/getHotkeyManager.md b/docs/reference/functions/getHotkeyManager.md index 22b56fd..0deade3 100644 --- a/docs/reference/functions/getHotkeyManager.md +++ b/docs/reference/functions/getHotkeyManager.md @@ -9,7 +9,7 @@ title: getHotkeyManager function getHotkeyManager(): HotkeyManager; ``` -Defined in: [hotkey-manager.ts:625](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L625) +Defined in: [hotkey-manager.ts:699](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L699) Gets the singleton HotkeyManager instance. Convenience function for accessing the manager. diff --git a/docs/reference/index.md b/docs/reference/index.md index 1d2f154..6b95e4a 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -30,6 +30,7 @@ title: "@tanstack/keys" ## Type Aliases - [CanonicalModifier](type-aliases/CanonicalModifier.md) +- [ConflictBehavior](type-aliases/ConflictBehavior.md) - [EditingKey](type-aliases/EditingKey.md) - [FunctionKey](type-aliases/FunctionKey.md) - [HeldKey](type-aliases/HeldKey.md) diff --git a/docs/reference/interfaces/HotkeyOptions.md b/docs/reference/interfaces/HotkeyOptions.md index 61362cc..a964631 100644 --- a/docs/reference/interfaces/HotkeyOptions.md +++ b/docs/reference/interfaces/HotkeyOptions.md @@ -5,7 +5,7 @@ title: HotkeyOptions # Interface: HotkeyOptions -Defined in: [hotkey-manager.ts:14](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L14) +Defined in: [hotkey-manager.ts:24](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L24) Options for registering a hotkey. @@ -15,13 +15,25 @@ Options for registering a hotkey. ## Properties +### conflictBehavior? + +```ts +optional conflictBehavior: ConflictBehavior; +``` + +Defined in: [hotkey-manager.ts:42](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L42) + +Behavior when this hotkey conflicts with an existing registration on the same target. Defaults to 'warn' + +*** + ### enabled? ```ts optional enabled: boolean; ``` -Defined in: [hotkey-manager.ts:16](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L16) +Defined in: [hotkey-manager.ts:26](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L26) Whether the hotkey is enabled. Defaults to true @@ -33,7 +45,7 @@ Whether the hotkey is enabled. Defaults to true optional eventType: "keydown" | "keyup"; ``` -Defined in: [hotkey-manager.ts:18](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L18) +Defined in: [hotkey-manager.ts:28](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L28) The event type to listen for. Defaults to 'keydown' @@ -45,7 +57,7 @@ The event type to listen for. Defaults to 'keydown' optional ignoreInputs: boolean; ``` -Defined in: [hotkey-manager.ts:20](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L20) +Defined in: [hotkey-manager.ts:30](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L30) Whether to ignore hotkeys when keyboard events originate from input-like elements (input, textarea, select, contenteditable). Defaults to true @@ -57,7 +69,7 @@ Whether to ignore hotkeys when keyboard events originate from input-like element optional platform: "mac" | "windows" | "linux"; ``` -Defined in: [hotkey-manager.ts:22](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L22) +Defined in: [hotkey-manager.ts:32](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L32) The target platform for resolving 'Mod' @@ -69,7 +81,7 @@ The target platform for resolving 'Mod' optional preventDefault: boolean; ``` -Defined in: [hotkey-manager.ts:24](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L24) +Defined in: [hotkey-manager.ts:34](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L34) Prevent the default browser action when the hotkey matches @@ -81,7 +93,7 @@ Prevent the default browser action when the hotkey matches optional requireReset: boolean; ``` -Defined in: [hotkey-manager.ts:26](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L26) +Defined in: [hotkey-manager.ts:36](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L36) If true, only trigger once until all keys are released. Default: false @@ -93,7 +105,7 @@ If true, only trigger once until all keys are released. Default: false optional stopPropagation: boolean; ``` -Defined in: [hotkey-manager.ts:28](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L28) +Defined in: [hotkey-manager.ts:38](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L38) Stop event propagation when the hotkey matches @@ -105,6 +117,6 @@ Stop event propagation when the hotkey matches optional target: Document | Window | HTMLElement | null; ``` -Defined in: [hotkey-manager.ts:30](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L30) +Defined in: [hotkey-manager.ts:40](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L40) The DOM element to attach the event listener to. Defaults to document. diff --git a/docs/reference/interfaces/HotkeyRegistration.md b/docs/reference/interfaces/HotkeyRegistration.md index 5c664cf..5a5a80a 100644 --- a/docs/reference/interfaces/HotkeyRegistration.md +++ b/docs/reference/interfaces/HotkeyRegistration.md @@ -5,7 +5,7 @@ title: HotkeyRegistration # Interface: HotkeyRegistration -Defined in: [hotkey-manager.ts:36](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L36) +Defined in: [hotkey-manager.ts:48](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L48) A registered hotkey handler in the HotkeyManager. @@ -17,7 +17,7 @@ A registered hotkey handler in the HotkeyManager. callback: HotkeyCallback; ``` -Defined in: [hotkey-manager.ts:44](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L44) +Defined in: [hotkey-manager.ts:56](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L56) The callback to invoke @@ -29,7 +29,7 @@ The callback to invoke hasFired: boolean; ``` -Defined in: [hotkey-manager.ts:48](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L48) +Defined in: [hotkey-manager.ts:60](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L60) Whether this registration has fired and needs reset (for requireReset) @@ -41,7 +41,7 @@ Whether this registration has fired and needs reset (for requireReset) hotkey: Hotkey; ``` -Defined in: [hotkey-manager.ts:40](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L40) +Defined in: [hotkey-manager.ts:52](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L52) The original hotkey string @@ -53,7 +53,7 @@ The original hotkey string id: string; ``` -Defined in: [hotkey-manager.ts:38](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L38) +Defined in: [hotkey-manager.ts:50](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L50) Unique identifier for this registration @@ -65,7 +65,7 @@ Unique identifier for this registration options: HotkeyOptions; ``` -Defined in: [hotkey-manager.ts:46](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L46) +Defined in: [hotkey-manager.ts:58](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L58) Options for this registration @@ -77,7 +77,7 @@ Options for this registration parsedHotkey: ParsedHotkey; ``` -Defined in: [hotkey-manager.ts:42](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L42) +Defined in: [hotkey-manager.ts:54](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L54) The parsed hotkey @@ -89,6 +89,6 @@ The parsed hotkey target: Document | Window | HTMLElement; ``` -Defined in: [hotkey-manager.ts:50](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L50) +Defined in: [hotkey-manager.ts:62](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L62) The resolved target element for this registration diff --git a/docs/reference/interfaces/HotkeyRegistrationHandle.md b/docs/reference/interfaces/HotkeyRegistrationHandle.md index 7903b84..30be008 100644 --- a/docs/reference/interfaces/HotkeyRegistrationHandle.md +++ b/docs/reference/interfaces/HotkeyRegistrationHandle.md @@ -5,7 +5,7 @@ title: HotkeyRegistrationHandle # Interface: HotkeyRegistrationHandle -Defined in: [hotkey-manager.ts:79](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L79) +Defined in: [hotkey-manager.ts:91](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L91) A handle returned from HotkeyManager.register() that allows updating the callback and options without re-registering the hotkey. @@ -41,7 +41,7 @@ handle.unregister() callback: HotkeyCallback; ``` -Defined in: [hotkey-manager.ts:90](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L90) +Defined in: [hotkey-manager.ts:102](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L102) The callback function. Can be set directly to update without re-registering. This avoids stale closures when the callback references React state. @@ -54,7 +54,7 @@ This avoids stale closures when the callback references React state. readonly id: string; ``` -Defined in: [hotkey-manager.ts:81](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L81) +Defined in: [hotkey-manager.ts:93](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L93) Unique identifier for this registration @@ -66,7 +66,7 @@ Unique identifier for this registration readonly isActive: boolean; ``` -Defined in: [hotkey-manager.ts:99](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L99) +Defined in: [hotkey-manager.ts:111](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L111) Check if this registration is still active (not unregistered) @@ -78,7 +78,7 @@ Check if this registration is still active (not unregistered) setOptions: (options) => void; ``` -Defined in: [hotkey-manager.ts:96](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L96) +Defined in: [hotkey-manager.ts:108](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L108) Update options (merged with existing options). Useful for updating `enabled`, `preventDefault`, etc. without re-registering. @@ -101,7 +101,7 @@ Useful for updating `enabled`, `preventDefault`, etc. without re-registering. unregister: () => void; ``` -Defined in: [hotkey-manager.ts:84](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L84) +Defined in: [hotkey-manager.ts:96](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L96) Unregister this hotkey diff --git a/docs/reference/interfaces/SequenceOptions.md b/docs/reference/interfaces/SequenceOptions.md index 845a6b9..93e9e52 100644 --- a/docs/reference/interfaces/SequenceOptions.md +++ b/docs/reference/interfaces/SequenceOptions.md @@ -15,13 +15,29 @@ Options for hotkey sequence matching. ## Properties +### conflictBehavior? + +```ts +optional conflictBehavior: ConflictBehavior; +``` + +Defined in: [hotkey-manager.ts:42](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L42) + +Behavior when this hotkey conflicts with an existing registration on the same target. Defaults to 'warn' + +#### Inherited from + +[`HotkeyOptions`](HotkeyOptions.md).[`conflictBehavior`](HotkeyOptions.md#conflictbehavior) + +*** + ### enabled? ```ts optional enabled: boolean; ``` -Defined in: [hotkey-manager.ts:16](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L16) +Defined in: [hotkey-manager.ts:26](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L26) Whether the hotkey is enabled. Defaults to true @@ -37,7 +53,7 @@ Whether the hotkey is enabled. Defaults to true optional eventType: "keydown" | "keyup"; ``` -Defined in: [hotkey-manager.ts:18](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L18) +Defined in: [hotkey-manager.ts:28](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L28) The event type to listen for. Defaults to 'keydown' @@ -53,7 +69,7 @@ The event type to listen for. Defaults to 'keydown' optional ignoreInputs: boolean; ``` -Defined in: [hotkey-manager.ts:20](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L20) +Defined in: [hotkey-manager.ts:30](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L30) Whether to ignore hotkeys when keyboard events originate from input-like elements (input, textarea, select, contenteditable). Defaults to true @@ -69,7 +85,7 @@ Whether to ignore hotkeys when keyboard events originate from input-like element optional platform: "mac" | "windows" | "linux"; ``` -Defined in: [hotkey-manager.ts:22](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L22) +Defined in: [hotkey-manager.ts:32](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L32) The target platform for resolving 'Mod' @@ -85,7 +101,7 @@ The target platform for resolving 'Mod' optional preventDefault: boolean; ``` -Defined in: [hotkey-manager.ts:24](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L24) +Defined in: [hotkey-manager.ts:34](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L34) Prevent the default browser action when the hotkey matches @@ -101,7 +117,7 @@ Prevent the default browser action when the hotkey matches optional requireReset: boolean; ``` -Defined in: [hotkey-manager.ts:26](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L26) +Defined in: [hotkey-manager.ts:36](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L36) If true, only trigger once until all keys are released. Default: false @@ -117,7 +133,7 @@ If true, only trigger once until all keys are released. Default: false optional stopPropagation: boolean; ``` -Defined in: [hotkey-manager.ts:28](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L28) +Defined in: [hotkey-manager.ts:38](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L38) Stop event propagation when the hotkey matches @@ -133,7 +149,7 @@ Stop event propagation when the hotkey matches optional target: Document | Window | HTMLElement | null; ``` -Defined in: [hotkey-manager.ts:30](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L30) +Defined in: [hotkey-manager.ts:40](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L40) The DOM element to attach the event listener to. Defaults to document. diff --git a/docs/reference/type-aliases/ConflictBehavior.md b/docs/reference/type-aliases/ConflictBehavior.md new file mode 100644 index 0000000..b7254ae --- /dev/null +++ b/docs/reference/type-aliases/ConflictBehavior.md @@ -0,0 +1,19 @@ +--- +id: ConflictBehavior +title: ConflictBehavior +--- + +# Type Alias: ConflictBehavior + +```ts +type ConflictBehavior = "warn" | "error" | "replace" | "allow"; +``` + +Defined in: [hotkey-manager.ts:19](https://github.com/TanStack/keys/blob/main/packages/keys/src/hotkey-manager.ts#L19) + +Behavior when registering a hotkey that conflicts with an existing registration. + +- `'warn'` - Log a warning to the console but allow both registrations (default) +- `'error'` - Throw an error and prevent the new registration +- `'replace'` - Unregister the existing hotkey and register the new one +- `'allow'` - Allow multiple registrations of the same hotkey without warning diff --git a/packages/keys/src/hotkey-manager.ts b/packages/keys/src/hotkey-manager.ts index 5157435..62de2b5 100644 --- a/packages/keys/src/hotkey-manager.ts +++ b/packages/keys/src/hotkey-manager.ts @@ -8,6 +8,16 @@ import type { ParsedHotkey, } from './hotkey' +/** + * Behavior when registering a hotkey that conflicts with an existing registration. + * + * - `'warn'` - Log a warning to the console but allow both registrations (default) + * - `'error'` - Throw an error and prevent the new registration + * - `'replace'` - Unregister the existing hotkey and register the new one + * - `'allow'` - Allow multiple registrations of the same hotkey without warning + */ +export type ConflictBehavior = 'warn' | 'error' | 'replace' | 'allow' + /** * Options for registering a hotkey. */ @@ -28,6 +38,8 @@ export interface HotkeyOptions { stopPropagation?: boolean /** The DOM element to attach the event listener to. Defaults to document. */ target?: HTMLElement | Document | Window | null + /** Behavior when this hotkey conflicts with an existing registration on the same target. Defaults to 'warn' */ + conflictBehavior?: ConflictBehavior } /** @@ -112,6 +124,7 @@ const defaultHotkeyOptions: Omit< requireReset: false, enabled: true, ignoreInputs: true, + conflictBehavior: 'warn', } let registrationIdCounter = 0 @@ -220,6 +233,19 @@ export class HotkeyManager { options.target ?? (typeof document !== 'undefined' ? document : ({} as Document)) + // Resolve conflict behavior + const conflictBehavior = options.conflictBehavior ?? 'warn' + + // Check for existing registrations with the same hotkey and target + const conflictingRegistration = this.#findConflictingRegistration( + hotkey, + target, + ) + + if (conflictingRegistration) { + this.#handleConflict(conflictingRegistration, hotkey, conflictBehavior) + } + const registration: HotkeyRegistration = { id, hotkey, @@ -508,6 +534,54 @@ export class HotkeyManager { return false } + /** + * Finds an existing registration with the same hotkey and target. + */ + #findConflictingRegistration( + hotkey: Hotkey, + target: HTMLElement | Document | Window, + ): HotkeyRegistration | null { + for (const registration of this.#registrations.values()) { + if (registration.hotkey === hotkey && registration.target === target) { + return registration + } + } + return null + } + + /** + * Handles conflicts between hotkey registrations based on conflict behavior. + */ + #handleConflict( + conflictingRegistration: HotkeyRegistration, + hotkey: Hotkey, + conflictBehavior: ConflictBehavior, + ): void { + if (conflictBehavior === 'allow') { + return + } + + if (conflictBehavior === 'warn') { + console.warn( + `Hotkey '${hotkey}' is already registered. Multiple handlers will be triggered. ` + + `Use conflictBehavior: 'replace' to replace the existing handler, ` + + `or conflictBehavior: 'allow' to suppress this warning.`, + ) + return + } + + if (conflictBehavior === 'error') { + throw new Error( + `Hotkey '${hotkey}' is already registered. ` + + `Use conflictBehavior: 'replace' to replace the existing handler, ` + + `or conflictBehavior: 'allow' to allow multiple registrations.`, + ) + } + + // At this point, conflictBehavior must be 'replace' + this.#unregister(conflictingRegistration.id) + } + /** * Checks if an element is an input-like element that should be ignored. * diff --git a/packages/keys/tests/manager.test.ts b/packages/keys/tests/manager.test.ts index b502ac2..b33dd5e 100644 --- a/packages/keys/tests/manager.test.ts +++ b/packages/keys/tests/manager.test.ts @@ -717,4 +717,101 @@ describe('HotkeyManager', () => { } }) }) + + describe('conflict detection', () => { + it('should warn by default when registering a conflicting hotkey', () => { + const manager = HotkeyManager.getInstance() + const callback1 = vi.fn() + const callback2 = vi.fn() + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + manager.register('Mod+S', callback1) + manager.register('Mod+S', callback2) + + expect(warnSpy).toHaveBeenCalled() + expect(warnSpy.mock.calls[0]?.[0]).toContain('already registered') + expect(manager.getRegistrationCount()).toBe(2) + + warnSpy.mockRestore() + }) + + it('should throw error when conflictBehavior is "error"', () => { + const manager = HotkeyManager.getInstance() + const callback1 = vi.fn() + const callback2 = vi.fn() + + manager.register('Mod+S', callback1) + + expect(() => { + manager.register('Mod+S', callback2, { conflictBehavior: 'error' }) + }).toThrow('already registered') + + expect(manager.getRegistrationCount()).toBe(1) + }) + + it('should replace existing registration when conflictBehavior is "replace"', () => { + const manager = HotkeyManager.getInstance() + const callback1 = vi.fn() + const callback2 = vi.fn() + + manager.register('Mod+S', callback1, { platform: 'mac' }) + expect(manager.getRegistrationCount()).toBe(1) + + manager.register('Mod+S', callback2, { + conflictBehavior: 'replace', + platform: 'mac', + }) + expect(manager.getRegistrationCount()).toBe(1) + + document.dispatchEvent( + createKeyboardEvent('keydown', 's', { metaKey: true }), + ) + + expect(callback1).not.toHaveBeenCalled() + expect(callback2).toHaveBeenCalledOnce() + }) + + it('should allow multiple registrations when conflictBehavior is "allow"', () => { + const manager = HotkeyManager.getInstance() + const callback1 = vi.fn() + const callback2 = vi.fn() + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + manager.register('Mod+S', callback1, { platform: 'mac' }) + manager.register('Mod+S', callback2, { + conflictBehavior: 'allow', + platform: 'mac', + }) + + expect(warnSpy).not.toHaveBeenCalled() + expect(manager.getRegistrationCount()).toBe(2) + + document.dispatchEvent( + createKeyboardEvent('keydown', 's', { metaKey: true }), + ) + + expect(callback1).toHaveBeenCalledOnce() + expect(callback2).toHaveBeenCalledOnce() + + warnSpy.mockRestore() + }) + + it('should not conflict when same hotkey is registered on different targets', () => { + const manager = HotkeyManager.getInstance() + const callback1 = vi.fn() + const callback2 = vi.fn() + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + const div1 = document.createElement('div') + const div2 = document.createElement('div') + + manager.register('Mod+S', callback1, { target: div1 }) + manager.register('Mod+S', callback2, { target: div2 }) + + expect(warnSpy).not.toHaveBeenCalled() + expect(manager.getRegistrationCount()).toBe(2) + + warnSpy.mockRestore() + }) + }) }) diff --git a/packages/keys/tests/match.test.ts b/packages/keys/tests/match.test.ts index e561634..be4ec32 100644 --- a/packages/keys/tests/match.test.ts +++ b/packages/keys/tests/match.test.ts @@ -259,7 +259,9 @@ describe('matchesKeyboardEvent', () => { metaKey: true, code: 'KeyT', // uppercase }) - expect(matchesKeyboardEvent(event2, 'Mod+Alt+t' as Hotkey, 'mac')).toBe(true) + expect(matchesKeyboardEvent(event2, 'Mod+Alt+t' as Hotkey, 'mac')).toBe( + true, + ) }) })