authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-30 23:40:17-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-30 23:40:17-07:00
log71a90ae70e3ff880396363809bd9bfcfd520f30c
treef6d90ccb957ee376a9cd24797c725666e12ad23c
parent921cb3c8bc772309ccab4c422b091d0a91497a49
signature Signed by PGP key B5690EEEBB952194

fix(web): hotkey settings record plain keys and Tab (#14548)

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 });
115115
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}
360360
361/** Turns a keydown into a binding such as `mod+shift+k` or `tab`. Null for modifier-only presses. */
361export function keybindingFromKeyboardEvent(362export 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 };
769769
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 };
10841085
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);