Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/lib/keyboardUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ describe("normalizeCapturedKey", () => {
expect(normalizeCapturedKey(keyEvent("F5", "F5"))).toBe("F5");
});

it("resolves position-invariant keys from the code, not the character", () => {
expect(
normalizeCapturedKey(keyEvent("\u00A0", "Space", { altKey: true })),
).toBe("Space");
expect(
normalizeCapturedKey(keyEvent("ArrowUp", "ArrowUp", { altKey: true })),
).toBe("Up");
expect(normalizeCapturedKey(keyEvent("F5", "F5", { altKey: true }))).toBe(
"F5",
);
});

it("keeps reading layout-dependent keys from the character", () => {
// Semicolon is `m` and Digit1 is `&` on AZERTY.
expect(normalizeCapturedKey(keyEvent("m", "Semicolon"))).toBe("M");
expect(normalizeCapturedKey(keyEvent("&", "Digit1"))).toBe("&");
});

it("rejects dead keys and IME", () => {
expect(normalizeCapturedKey(keyEvent("Dead", "KeyQ"))).toBeNull();
expect(normalizeCapturedKey(keyEvent("Process", "KeyA"))).toBeNull();
Expand Down
25 changes: 23 additions & 2 deletions src/lib/keyboardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ const MODIFIER_MAP: Record<string, string> = {
AltRight: "Alt",
};

// Read before KeyboardEvent.key, since macOS Option composes: Opt+Space emits
// U+00A0, not " ". Position-invariant codes only — punctuation and digits move
// between layouts, so those must still come from the key.
const STABLE_LOGICAL_FROM_CODE: Record<string, string> = {
Space: "Space",
Enter: "Enter",
Tab: "Tab",
Backspace: "Backspace",
Delete: "Delete",
Escape: "Escape",
ArrowUp: "Up",
ArrowDown: "Down",
ArrowLeft: "Left",
ArrowRight: "Right",
};

/** Stable logical names for non-printable keys from KeyboardEvent.key */
const LOGICAL_KEY_FROM_EVENT_KEY: Record<string, string> = {
" ": "Space",
Expand Down Expand Up @@ -96,11 +112,16 @@ export function normalizeModifierFromCode(code: string): string | null {
}

/**
* Logical non-modifier key token from KeyboardEvent.key.
* Logical non-modifier key token, read from KeyboardEvent.key so it follows the
* active layout — except for position-invariant keys, which come from the code.
* Returns null for modifiers, dead keys, IME composition, or unsupported keys.
*/
export function normalizeCapturedKey(event: KeyboardEvent): string | null {
const { key } = event;
const { key, code } = event;

const stable = STABLE_LOGICAL_FROM_CODE[code];
if (stable) return stable;
if (/^F\d{1,2}$/.test(code)) return code;

if (key === "Dead" || key === "Process" || key === "Unidentified") {
return null;
Expand Down