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", () => {
113113 ).toBe("mod+shift+k");
114114 });
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
116141 it.each([
117142 ["@", "Digit2", "mod+shift+2"],
118143 ['"', "Digit2", "mod+shift+2"],
apps/web/src/components/settings/KeybindingsSettings.logic.ts+1-3
......@@ -358,6 +358,7 @@ function normalizeShortcutKeyToken(key: string): string | null {
358358 return null;
359359}
360360
361/** Turns a keydown into a binding such as `mod+shift+k` or `tab`. Null for modifier-only presses. */
361362export function keybindingFromKeyboardEvent(
362363 event: Pick<KeyboardEvent, "key" | "code" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">,
363364 platform: string,
......@@ -375,9 +376,6 @@ export function keybindingFromKeyboardEvent(
375376 }
376377 if (event.altKey) parts.push("alt");
377378 if (event.shiftKey) parts.push("shift");
378 if (parts.length === 0) {
379 return null;
380 }
381379 parts.push(keyToken);
382380 return parts.join("+");
383381}
apps/web/src/components/settings/KeybindingsSettings.tsx+4-2
......@@ -768,7 +768,8 @@ function useKeybindingRowEditor({
768768 };
769769
770770 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;
772773 event.preventDefault();
773774 if (event.key === "Escape") {
774775 setDraft({ keyDraft: row.key, isRecording: false });
......@@ -1083,7 +1084,8 @@ function useNewKeybindingDraft({
10831084 };
10841085
10851086 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;
10871089 event.preventDefault();
10881090 if (event.key === "Escape") {
10891091 setDraft({ keyDraft: "", isRecording: false });
apps/web/src/components/settings/useSnapShotShortcutRecorder.tsx+2
......@@ -106,6 +106,8 @@ export function useSnapShotShortcutRecorder({
106106 }
107107 return;
108108 }
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;
109111 const input = keybindingFromKeyboardEvent(event, navigator.platform);
110112 if (!input) return;
111113 const next = parseKeybindingShortcut(input);