From 7dbadebcec04d742296d0d8add79220d81a36402 Mon Sep 17 00:00:00 2001 From: Pavel Grinchenko Date: Fri, 20 Feb 2026 00:59:07 +0000 Subject: [PATCH] fix: make key filters work on non-latin layouts Key filters matched event.key only, so keydown.j failed on non-latin layouts. Fallback to event.code for non-ASCII keys and add regression tests. --- src/core/action.ts | 38 ++++++++++++++++++- .../core/action_keyboard_filter_tests.ts | 25 ++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/core/action.ts b/src/core/action.ts index e009399b..94453313 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -59,7 +59,10 @@ export class Action { error(`contains unknown key filter: ${this.keyFilter}`) } - return this.keyMappings[standardFilter].toLowerCase() !== event.key.toLowerCase() + const mappedKey = this.keyMappings[standardFilter].toLowerCase() + const normalizedEventKey = this.normalizedKeyboardEventKey(event, mappedKey) + + return mappedKey !== normalizedEventKey } shouldIgnoreMouseEvent(event: MouseEvent): boolean { @@ -102,6 +105,17 @@ export class Action { return event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== alt || event.shiftKey !== shift } + + private normalizedKeyboardEventKey(event: KeyboardEvent, mappedKey: string): string { + const eventKey = event.key.toLowerCase() + + if (!shouldUseCodeFallback(event, mappedKey)) { + return eventKey + } + + const keyFromCode = keyboardEventKeyFromCode(event.code) + return keyFromCode || eventKey + } } const defaultEventNames: { [tagName: string]: (element: Element) => string } = { @@ -132,3 +146,25 @@ function typecast(value: any): any { return value } } + +function shouldUseCodeFallback(event: KeyboardEvent, mappedKey: string): boolean { + return !event.isComposing && isPrintableASCIICharacter(mappedKey) && !isPrintableASCIICharacter(event.key) +} + +function isPrintableASCIICharacter(value: string): boolean { + return value.length === 1 && value >= " " && value <= "~" +} + +function keyboardEventKeyFromCode(code: string): string | null { + const keyMatch = code.match(/^Key([A-Z])$/) + if (keyMatch) { + return keyMatch[1].toLowerCase() + } + + const digitMatch = code.match(/^Digit([0-9])$/) + if (digitMatch) { + return digitMatch[1] + } + + return null +} diff --git a/src/tests/modules/core/action_keyboard_filter_tests.ts b/src/tests/modules/core/action_keyboard_filter_tests.ts index 64a9303c..5f40238e 100644 --- a/src/tests/modules/core/action_keyboard_filter_tests.ts +++ b/src/tests/modules/core/action_keyboard_filter_tests.ts @@ -22,6 +22,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { ` @@ -197,4 +198,28 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { await this.triggerEvent(button, "jquery.a") this.assertActions({ name: "log2", identifier: "a", eventType: "jquery.a", currentTarget: button }) } + + // Cyrillic (Bulgarian, phonetic layout) + async "test plain j key filter falls back to code for cyrillic key"() { + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "й", code: "KeyJ" }) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + } + + // Greek + async "test plain j key filter falls back to code for greek key"() { + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "ξ", code: "KeyJ" }) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + } + + // Hebrew + async "test plain j key filter falls back to code for hebrew key"() { + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "ח", code: "KeyJ" }) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + } }