Run letter shortcuts with non-Latin keyboard layouts (#6755)

* fix(app): run recorded letter shortcuts with non-Latin layouts

Recording unsupported letters saves the physical A-Z key, but dispatch rejected that same event. Accept its letter code while keeping ASCII character matching for layouts such as Dvorak.

* fix(app): run recorded dotted-letter shortcuts

Check the original single-letter key before case folding expands uppercase dotted I into two code points.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-13 05:00:54 +02:00
committed by GitHub
parent b0f054de90
commit b5e51cf839
2 changed files with 71 additions and 0 deletions
@@ -1027,6 +1027,71 @@ describe("unassigned shortcuts", () => {
});
describe("matching", () => {
it("dispatches a captured Russian letter shortcut on the same key", () => {
const event = keyboardInput({ key: "и", code: "KeyB", ctrlKey: true });
const combo = keyboardEventToComboString(event as KeyboardEvent);
expect(combo).toBe("Ctrl+B");
const bindings = buildEffectiveBindings({
"sidebar-toggle-left-ctrl-period-non-mac": combo!,
});
expect(resolveShortcut({ event, context: desktopNonMac, bindings }).match?.action).toBe(
"sidebar.toggle.left",
);
});
it("dispatches a recorded dotted-uppercase letter with Shift held", () => {
const event = keyboardInput({ key: "İ", code: "KeyS", ctrlKey: true, shiftKey: true });
const combo = keyboardEventToComboString(event as KeyboardEvent);
expect(combo).toBe("Ctrl+Shift+S");
const bindings = buildEffectiveBindings({
"sidebar-toggle-left-ctrl-period-non-mac": combo!,
});
expect(resolveShortcut({ event, context: desktopNonMac, bindings }).match?.action).toBe(
"sidebar.toggle.left",
);
});
it.each(["и", "И", "β", "ش", "ב", "é"])(
"matches default and recorded letter bindings for unsupported letter %s",
(key) => {
const event = keyboardInput({ key, code: "KeyB", ctrlKey: true });
expectShortcutResolution({
event,
context: desktopNonMac,
action: "sidebar.toggle.left",
});
const combo = keyboardEventToComboString(event as KeyboardEvent);
expect(combo).toBe("Ctrl+B");
expect(
resolveShortcut({
event,
context: desktopNonMac,
bindings: buildEffectiveBindings({
"sidebar-toggle-left-ctrl-period-non-mac": combo!,
}),
}).match?.action,
).toBe("sidebar.toggle.left");
},
);
it.each(["v", "†", "Dead", "Unidentified", ""])(
"does not match an unrelated character or named key %s by physical letter",
(key) => {
expectNoShortcutResolution({
event: { key, code: "KeyB", ctrlKey: true },
context: desktopNonMac,
});
},
);
it("does not match non-letter bindings by physical key for unsupported letters", () => {
expectNoShortcutResolution({
event: { key: "ю", code: "Period", ctrlKey: true },
context: desktopNonMac,
});
});
it("dispatches a captured Dvorak binding on the same logical key", () => {
const event = keyboardInput({ key: "b", code: "KeyN", metaKey: true });
const combo = keyboardEventToComboString(event as KeyboardEvent);
@@ -1285,6 +1285,12 @@ function matchesKeyOrCode(combo: KeyCombo, event: KeyboardShortcutInput): boolea
if (combo.shift === true && combo.shiftedKey !== undefined && eventKey === combo.shiftedKey) {
return true;
}
// Recording an unsupported letter (for example Cyrillic и on KeyB)
// stores its physical A-Z key. Dispatch must accept that same key without
// giving Latin layouts such as Dvorak a second physical-key binding.
if (/^[a-z]$/.test(combo.key) && /^\p{Letter}$/u.test(event.key) && !/^[a-z]$/.test(eventKey)) {
return event.code === combo.code;
}
// macOS rewrites event.key when Option is held (Option+T -> "†",
// Option+[ -> "“"), so Alt-bound letter / bracket bindings can only
// match by event.code. Stay key-first for non-Alt bindings so Dvorak