| author | |
| committer | |
| log | 71a90ae70e3ff880396363809bd9bfcfd520f30c |
| tree | f6d90ccb957ee376a9cd24797c725666e12ad23c |
| parent | 921cb3c8bc772309ccab4c422b091d0a91497a49 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>4 files changed, 32 insertions(+), 5 deletions(-)
apps/web/src/components/settings/KeybindingsSettings.logic.test.ts+25| ... | @@ -113,6 +113,31 @@ describe("KeybindingsSettings.logic", () => { | ... | @@ -113,6 +113,31 @@ describe("KeybindingsSettings.logic", () => { |
| 113 | ).toBe("mod+shift+k"); | 113 | ).toBe("mod+shift+k"); |
| 114 | }); | 114 | }); |
| 115 | 115 | ||
| 116 | it.each([ | ||
| 117 | ["k", "KeyK", "k"], | ||
| 118 | ["Tab", "Tab", "tab"], | ||
| 119 | ["F5", "F5", "f5"], | ||
| 120 | ])("captures %s without a modifier", (key, code, expected) => { | ||
| 121 | const noModifiers = { metaKey: false, ctrlKey: false, altKey: false, shiftKey: false }; | ||
| 122 | expect(keybindingFromKeyboardEvent({ key, code, ...noModifiers }, "MacIntel")).toBe(expected); | ||
| 123 | }); | ||
| 124 | |||
| 125 | it("waits for a key when only a modifier is pressed", () => { | ||
| 126 | expect( | ||
| 127 | keybindingFromKeyboardEvent( | ||
| 128 | { | ||
| 129 | key: "Meta", | ||
| 130 | code: "MetaLeft", | ||
| 131 | metaKey: true, | ||
| 132 | ctrlKey: false, | ||
| 133 | altKey: false, | ||
| 134 | shiftKey: false, | ||
| 135 | }, | ||
| 136 | "MacIntel", | ||
| 137 | ), | ||
| 138 | ).toBeNull(); | ||
| 139 | }); | ||
| 140 | |||
| 116 | it.each([ | 141 | it.each([ |
| 117 | ["@", "Digit2", "mod+shift+2"], | 142 | ["@", "Digit2", "mod+shift+2"], |
| 118 | ['"', "Digit2", "mod+shift+2"], | 143 | ['"', "Digit2", "mod+shift+2"], |
apps/web/src/components/settings/KeybindingsSettings.logic.ts+1-3| ... | @@ -358,6 +358,7 @@ function normalizeShortcutKeyToken(key: string): string | null { | ... | @@ -358,6 +358,7 @@ function normalizeShortcutKeyToken(key: string): string | null { |
| 358 | return null; | 358 | return null; |
| 359 | } | 359 | } |
| 360 | 360 | ||
| 361 | /** Turns a keydown into a binding such as `mod+shift+k` or `tab`. Null for modifier-only presses. */ | ||
| 361 | export function keybindingFromKeyboardEvent( | 362 | export function keybindingFromKeyboardEvent( |
| 362 | event: Pick<KeyboardEvent, "key" | "code" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">, | 363 | event: Pick<KeyboardEvent, "key" | "code" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">, |
| 363 | platform: string, | 364 | platform: string, |
| ... | @@ -375,9 +376,6 @@ export function keybindingFromKeyboardEvent( | ... | @@ -375,9 +376,6 @@ export function keybindingFromKeyboardEvent( |
| 375 | } | 376 | } |
| 376 | if (event.altKey) parts.push("alt"); | 377 | if (event.altKey) parts.push("alt"); |
| 377 | if (event.shiftKey) parts.push("shift"); | 378 | if (event.shiftKey) parts.push("shift"); |
| 378 | if (parts.length === 0) { | ||
| 379 | return null; | ||
| 380 | } | ||
| 381 | parts.push(keyToken); | 379 | parts.push(keyToken); |
| 382 | return parts.join("+"); | 380 | return parts.join("+"); |
| 383 | } | 381 | } |
apps/web/src/components/settings/KeybindingsSettings.tsx+4-2| ... | @@ -768,7 +768,8 @@ function useKeybindingRowEditor({ | ... | @@ -768,7 +768,8 @@ function useKeybindingRowEditor({ |
| 768 | }; | 768 | }; |
| 769 | 769 | ||
| 770 | const captureKeybinding = (event: KeyboardEvent<HTMLInputElement>) => { | 770 | const captureKeybinding = (event: KeyboardEvent<HTMLInputElement>) => { |
| 771 | if (event.key === "Tab") return; | 771 | // Tab is recorded like any key while recording; after that it moves focus on. |
| 772 | if (event.key === "Tab" && !isRecording) return; | ||
| 772 | event.preventDefault(); | 773 | event.preventDefault(); |
| 773 | if (event.key === "Escape") { | 774 | if (event.key === "Escape") { |
| 774 | setDraft({ keyDraft: row.key, isRecording: false }); | 775 | setDraft({ keyDraft: row.key, isRecording: false }); |
| ... | @@ -1083,7 +1084,8 @@ function useNewKeybindingDraft({ | ... | @@ -1083,7 +1084,8 @@ function useNewKeybindingDraft({ |
| 1083 | }; | 1084 | }; |
| 1084 | 1085 | ||
| 1085 | const captureKeybinding = (event: KeyboardEvent<HTMLInputElement>) => { | 1086 | const captureKeybinding = (event: KeyboardEvent<HTMLInputElement>) => { |
| 1086 | if (event.key === "Tab") return; | 1087 | // Tab is recorded like any key while recording; after that it moves focus on. |
| 1088 | if (event.key === "Tab" && !isRecording) return; | ||
| 1087 | event.preventDefault(); | 1089 | event.preventDefault(); |
| 1088 | if (event.key === "Escape") { | 1090 | if (event.key === "Escape") { |
| 1089 | setDraft({ keyDraft: "", isRecording: false }); | 1091 | setDraft({ keyDraft: "", isRecording: false }); |
apps/web/src/components/settings/useSnapShotShortcutRecorder.tsx+2| ... | @@ -106,6 +106,8 @@ export function useSnapShotShortcutRecorder({ | ... | @@ -106,6 +106,8 @@ export function useSnapShotShortcutRecorder({ |
| 106 | } | 106 | } |
| 107 | return; | 107 | return; |
| 108 | } | 108 | } |
| 109 | // A global shortcut without a modifier would take that key from every app. | ||
| 110 | if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) return; | ||
| 109 | const input = keybindingFromKeyboardEvent(event, navigator.platform); | 111 | const input = keybindingFromKeyboardEvent(event, navigator.platform); |
| 110 | if (!input) return; | 112 | if (!input) return; |
| 111 | const next = parseKeybindingShortcut(input); | 113 | const next = parseKeybindingShortcut(input); |