Skip to content

Commit 43a35f3

Browse files
authored
fix(shortcuts): match single-letter hotkeys by layout-aware key
react-hotkeys-hook 4.6.2 matches a hotkey's letter against both the layout-aware event.key and the physical event.code. For a single-letter shortcut like "mod+i" (Inbox), that means any key at the physical QWERTY-"I" position triggers it — so on a Dvorak layout Cmd+C (whose key sits at that physical slot) navigates to the inbox instead of copying. Every single-letter mod+<letter> shortcut has the same problem on non-QWERTY layouts. Patch the library matcher to derive the key from event.key, falling back to the physical event.code only when a modifier (e.g. macOS Option) turns event.key into a non-alphanumeric glyph. Special keys (arrows, escape, space) and Option shortcuts keep working via the fallback. Generated-By: PostHog Code Task-Id: 570b6a35-794b-45bb-933f-7b54751b014a
1 parent 66bb8b8 commit 43a35f3

4 files changed

Lines changed: 54 additions & 3 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { renderHook } from "@testing-library/react";
2+
import { useHotkeys } from "react-hotkeys-hook";
3+
import { describe, expect, it, vi } from "vitest";
4+
5+
// Regression guard for patches/react-hotkeys-hook.patch. Single-letter shortcuts must match the
6+
// layout-aware event.key, not the physical event.code. On a Dvorak layout the key labelled "c"
7+
// sits at the physical QWERTY-"I" slot (event.code "KeyI"), which used to trigger the "mod+i"
8+
// Inbox shortcut and hijack Cmd+C.
9+
function press(init: KeyboardEventInit): void {
10+
document.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, ...init }));
11+
}
12+
13+
describe("react-hotkeys-hook layout-aware matching", () => {
14+
it("fires mod+i when the logical key is i", () => {
15+
const onInbox = vi.fn();
16+
renderHook(() => useHotkeys("mod+i", onInbox, { enableOnContentEditable: true }));
17+
18+
press({ key: "i", code: "KeyI", metaKey: true });
19+
20+
expect(onInbox).toHaveBeenCalledTimes(1);
21+
});
22+
23+
it("does not fire mod+i on a Dvorak Cmd+C that lands on the physical KeyI slot", () => {
24+
const onInbox = vi.fn();
25+
renderHook(() => useHotkeys("mod+i", onInbox, { enableOnContentEditable: true }));
26+
27+
press({ key: "c", code: "KeyI", metaKey: true });
28+
29+
expect(onInbox).not.toHaveBeenCalled();
30+
});
31+
});

patches/react-hotkeys-hook.patch

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--- a/dist/react-hotkeys-hook.esm.js
2+
+++ b/dist/react-hotkeys-hook.esm.js
3+
@@ -206,7 +206,12 @@
4+
metaKey = e.metaKey,
5+
shiftKey = e.shiftKey,
6+
altKey = e.altKey;
7+
- var keyCode = mapKey(code);
8+
+ var layoutKey = mapKey(pressedKeyUppercase);
9+
+ // PostHog patch: prefer the layout-aware key (event.key). Fall back to the physical
10+
+ // event.code only when a modifier such as macOS Option turns event.key into a
11+
+ // non-alphanumeric glyph, so single-letter shortcuts match the printed letter on
12+
+ // non-QWERTY layouts (Dvorak, AZERTY, ...) instead of the QWERTY key at that slot.
13+
+ var keyCode = /^[a-z0-9]$/.test(layoutKey) ? layoutKey : mapKey(code);
14+
var pressedKey = pressedKeyUppercase.toLowerCase();
15+
if (!(keys != null && keys.includes(keyCode)) && !(keys != null && keys.includes(pressedKey)) && !['ctrl', 'control', 'unknown', 'meta', 'alt', 'shift', 'os'].includes(keyCode)) {
16+
return false;

pnpm-lock.yaml

Lines changed: 6 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,4 @@ onlyBuiltDependencies:
115115

116116
patchedDependencies:
117117
node-pty: patches/node-pty.patch
118+
react-hotkeys-hook: patches/react-hotkeys-hook.patch

0 commit comments

Comments
 (0)