From dd0307a9c9adc8ef36677151d00b66f145fee507 Mon Sep 17 00:00:00 2001 From: Nikhil Tilwalli <8410254+ntilwalli@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:19:22 -0600 Subject: [PATCH 1/2] fix: handle dead keys in matchesKeyboardEvent When event.key is 'Dead' (length 4), the existing event.code fallback (gated behind eventKey.length === 1) was never reached, causing hotkeys to silently fail. This most commonly affects macOS, where Option+letter combinations like Option+E, Option+I, Option+U, and Option+N produce dead keys for accent composition. It also affects Windows and Linux users with international keyboard layouts (e.g., US-International, German, French) where certain key combinations produce dead keys. Adds an early check: when event.key normalizes to 'Dead', immediately fall back to event.code to extract the physical key via the Key*/Digit* prefixes. Punctuation dead keys (e.g., apostrophe on US-International, where event.code is 'Quote') correctly return false since their codes don't match letter or digit patterns. Includes 10 tests covering dead key scenarios for letter keys, digit keys, modifier combinations, mismatches, and missing/invalid codes. Co-authored-by: Cursor --- .changeset/fix-dead-key-fallback.md | 11 +++ .../functions/matchesKeyboardEvent.md | 5 ++ packages/hotkeys/src/match.ts | 24 ++++++ packages/hotkeys/tests/match.test.ts | 83 +++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 .changeset/fix-dead-key-fallback.md diff --git a/.changeset/fix-dead-key-fallback.md b/.changeset/fix-dead-key-fallback.md new file mode 100644 index 0000000..cecfaf6 --- /dev/null +++ b/.changeset/fix-dead-key-fallback.md @@ -0,0 +1,11 @@ +--- +'@tanstack/hotkeys': patch +--- + +fix: handle dead keys in `matchesKeyboardEvent` + +When `event.key` is `'Dead'` (length 4), the existing `event.code` fallback—gated behind `eventKey.length === 1`—was never reached, causing hotkeys to silently fail. + +This most commonly affects macOS, where `Option+letter` combinations like `Option+E`, `Option+I`, `Option+U`, and `Option+N` produce dead keys for accent composition. It also affects Windows and Linux users with international keyboard layouts (e.g., US-International, German, French) where certain key combinations produce dead keys. + +Added an early check: when `event.key` normalizes to `'Dead'`, immediately fall back to `event.code` to extract the physical key via the `Key*`/`Digit*` prefixes. Punctuation dead keys (e.g., `'` on US-International, where `event.code` is `'Quote'`) correctly return `false` since their codes don't match letter or digit patterns. diff --git a/docs/reference/functions/matchesKeyboardEvent.md b/docs/reference/functions/matchesKeyboardEvent.md index 58c4682..a211085 100644 --- a/docs/reference/functions/matchesKeyboardEvent.md +++ b/docs/reference/functions/matchesKeyboardEvent.md @@ -20,6 +20,11 @@ Uses the `key` property from KeyboardEvent for matching, with a fallback to `cod for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively. +Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected +character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E, +Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts. +In these cases, `event.code` is used to determine the physical key. + ## Parameters ### event diff --git a/packages/hotkeys/src/match.ts b/packages/hotkeys/src/match.ts index cfc7972..412d884 100644 --- a/packages/hotkeys/src/match.ts +++ b/packages/hotkeys/src/match.ts @@ -14,6 +14,11 @@ import type { * for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters * (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively. * + * Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected + * character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E, + * Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts. + * In these cases, `event.code` is used to determine the physical key. + * * @param event - The KeyboardEvent to check * @param hotkey - The hotkey string or ParsedHotkey to match against * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection) @@ -55,6 +60,25 @@ export function matchesKeyboardEvent( const eventKey = normalizeKeyName(event.key) const hotkeyKey = parsed.key + // Handle dead keys: certain modifier+letter combos produce event.key === 'Dead' + // (e.g., macOS Option+E, or international layouts on Windows/Linux). + // In this case, event.key is unusable but event.code still identifies the physical key. + if (eventKey === 'Dead') { + if (event.code && event.code.startsWith('Key')) { + const codeLetter = event.code.slice(3) + if (codeLetter.length === 1 && /^[A-Za-z]$/.test(codeLetter)) { + return codeLetter.toUpperCase() === hotkeyKey.toUpperCase() + } + } + if (event.code && event.code.startsWith('Digit')) { + const codeDigit = event.code.slice(5) + if (codeDigit.length === 1 && /^[0-9]$/.test(codeDigit)) { + return codeDigit === hotkeyKey + } + } + return false + } + // For single letters, compare case-insensitively if (eventKey.length === 1 && hotkeyKey.length === 1) { // First try matching with event.key diff --git a/packages/hotkeys/tests/match.test.ts b/packages/hotkeys/tests/match.test.ts index a37421a..7b33281 100644 --- a/packages/hotkeys/tests/match.test.ts +++ b/packages/hotkeys/tests/match.test.ts @@ -265,6 +265,89 @@ describe('matchesKeyboardEvent', () => { }) }) + describe('dead key fallback (macOS Option+letter)', () => { + it('should match Alt+E when event.key is Dead (macOS dead key for accent)', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: 'KeyE', + }) + expect(matchesKeyboardEvent(event, 'Alt+E')).toBe(true) + }) + + it('should match Alt+I when event.key is Dead', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: 'KeyI', + }) + expect(matchesKeyboardEvent(event, 'Alt+I')).toBe(true) + }) + + it('should match Alt+U when event.key is Dead', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: 'KeyU', + }) + expect(matchesKeyboardEvent(event, 'Alt+U')).toBe(true) + }) + + it('should match Alt+N when event.key is Dead', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: 'KeyN', + }) + expect(matchesKeyboardEvent(event, 'Alt+N')).toBe(true) + }) + + it('should match Mod+Alt with dead key on Mac', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + metaKey: true, + code: 'KeyE', + }) + expect(matchesKeyboardEvent(event, 'Mod+Alt+E', 'mac')).toBe(true) + }) + + it('should not match dead key when code does not match hotkey', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: 'KeyE', + }) + expect(matchesKeyboardEvent(event, 'Alt+T')).toBe(false) + }) + + it('should not match dead key when modifiers do not match', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: 'KeyE', + }) + expect(matchesKeyboardEvent(event, 'Control+E')).toBe(false) + }) + + it('should not match dead key when event.code is missing', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: undefined, + }) + expect(matchesKeyboardEvent(event, 'Alt+E')).toBe(false) + }) + + it('should not match dead key when event.code has invalid format', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: 'InvalidCode', + }) + expect(matchesKeyboardEvent(event, 'Alt+E')).toBe(false) + }) + + it('should handle dead key with digit code fallback', () => { + const event = createKeyboardEvent('Dead', { + altKey: true, + code: 'Digit4', + }) + expect(matchesKeyboardEvent(event, 'Alt+4')).toBe(true) + }) + }) + describe('event.code fallback for digit keys', () => { it('should fallback to event.code when event.key produces special character (Shift+4 -> $)', () => { // Simulate Shift+4 where event.key is '$' but event.code is 'Digit4' From c90b91b77a0b03106ac2d3cec06bf6f759d21b4e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:56:52 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- docs/reference/functions/createHotkeyHandler.md | 2 +- docs/reference/functions/createMultiHotkeyHandler.md | 2 +- docs/reference/functions/matchesKeyboardEvent.md | 2 +- docs/reference/interfaces/CreateHotkeyHandlerOptions.md | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/reference/functions/createHotkeyHandler.md b/docs/reference/functions/createHotkeyHandler.md index 07142d7..8be1c8f 100644 --- a/docs/reference/functions/createHotkeyHandler.md +++ b/docs/reference/functions/createHotkeyHandler.md @@ -12,7 +12,7 @@ function createHotkeyHandler( options): (event) => void; ``` -Defined in: [match.ts:122](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L122) +Defined in: [match.ts:146](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L146) Creates a keyboard event handler that calls the callback when the hotkey matches. diff --git a/docs/reference/functions/createMultiHotkeyHandler.md b/docs/reference/functions/createMultiHotkeyHandler.md index 5426f39..52a9d24 100644 --- a/docs/reference/functions/createMultiHotkeyHandler.md +++ b/docs/reference/functions/createMultiHotkeyHandler.md @@ -9,7 +9,7 @@ title: createMultiHotkeyHandler function createMultiHotkeyHandler(handlers, options): (event) => void; ``` -Defined in: [match.ts:173](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L173) +Defined in: [match.ts:197](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L197) Creates a handler that matches multiple hotkeys. diff --git a/docs/reference/functions/matchesKeyboardEvent.md b/docs/reference/functions/matchesKeyboardEvent.md index a211085..c5cefed 100644 --- a/docs/reference/functions/matchesKeyboardEvent.md +++ b/docs/reference/functions/matchesKeyboardEvent.md @@ -12,7 +12,7 @@ function matchesKeyboardEvent( platform): boolean; ``` -Defined in: [match.ts:32](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L32) +Defined in: [match.ts:37](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L37) Checks if a KeyboardEvent matches a hotkey. diff --git a/docs/reference/interfaces/CreateHotkeyHandlerOptions.md b/docs/reference/interfaces/CreateHotkeyHandlerOptions.md index a7b824d..afcd16a 100644 --- a/docs/reference/interfaces/CreateHotkeyHandlerOptions.md +++ b/docs/reference/interfaces/CreateHotkeyHandlerOptions.md @@ -5,7 +5,7 @@ title: CreateHotkeyHandlerOptions # Interface: CreateHotkeyHandlerOptions -Defined in: [match.ts:95](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L95) +Defined in: [match.ts:119](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L119) Options for creating a hotkey handler. @@ -17,7 +17,7 @@ Options for creating a hotkey handler. optional platform: "mac" | "windows" | "linux"; ``` -Defined in: [match.ts:101](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L101) +Defined in: [match.ts:125](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L125) The target platform for resolving 'Mod' @@ -29,7 +29,7 @@ The target platform for resolving 'Mod' optional preventDefault: boolean; ``` -Defined in: [match.ts:97](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L97) +Defined in: [match.ts:121](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L121) Prevent the default browser action when the hotkey matches. Defaults to true @@ -41,6 +41,6 @@ Prevent the default browser action when the hotkey matches. Defaults to true optional stopPropagation: boolean; ``` -Defined in: [match.ts:99](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L99) +Defined in: [match.ts:123](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/match.ts#L123) Stop event propagation when the hotkey matches. Defaults to true