From e75d0313303afb3ed130527f195856aff190671b Mon Sep 17 00:00:00 2001 From: "Sean R. Abraham" Date: Mon, 28 Sep 2026 21:29:10 -0400 Subject: [PATCH] fix: let single-key shortcuts fire unless focus is in a text field --- web/typescript/ims.ts | 31 ++++++++++++------------ web/typescripttest/ims.test.ts | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/web/typescript/ims.ts b/web/typescript/ims.ts index 71f150bf..8c5433da 100644 --- a/web/typescript/ims.ts +++ b/web/typescript/ims.ts @@ -2033,8 +2033,16 @@ export function hideLoadingOverlay(): void { } } -// Returns whether an input text-ish field is active. This is meant to talk about fields -// for which keyboard a-z letters are used, such as text field and select fields. +// Input types that don't take typed characters, so shortcuts can fire while +// one is focused. +const nonTextInputTypes = new Set([ + "button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit", +]); + +// Returns whether single-key shortcuts should be ignored right now: because +// they're switched off, a dialog is open, or focus is in a field that takes +// typed characters (text inputs, textareas, selects, and contenteditables). +// Anything else, like a link or button, lets them through. export function blockKeyboardShortcutFieldActive(): boolean { // IMS's shortcuts are single characters with no modifier, which speech-input // users trigger by accident just by talking. WCAG 2.1.4 therefore requires @@ -2045,22 +2053,15 @@ export function blockKeyboardShortcutFieldActive(): boolean { if (isDialogOpen()) { return true; } - if (document.activeElement === document.body) { - return false; - } - if (document.activeElement?.id === "main") { - return false; - } - if (document.activeElement instanceof HTMLInputElement) { - return document.activeElement.type !== "checkbox"; + const active = document.activeElement; + if (active instanceof HTMLInputElement) { + return !nonTextInputTypes.has(active.type); } - if (document.activeElement instanceof HTMLButtonElement) { - return false; - } - if (document.activeElement instanceof HTMLElement && document.activeElement.isContentEditable) { + // A select jumps to the option matching a typed letter. + if (active instanceof HTMLTextAreaElement || active instanceof HTMLSelectElement) { return true; } - return true; + return active instanceof HTMLElement && active.isContentEditable; } // The kinds of records the Go to… modal can jump to. Each kind numbers its diff --git a/web/typescripttest/ims.test.ts b/web/typescripttest/ims.test.ts index 0d89cae7..ef582704 100644 --- a/web/typescripttest/ims.test.ts +++ b/web/typescripttest/ims.test.ts @@ -420,6 +420,49 @@ test("blockKeyboardShortcutFieldActive blocks everything when shortcuts are off" localStorage.clear(); }); +test("blockKeyboardShortcutFieldActive blocks only while typing into a field", (): void => { + localStorage.clear(); + document.body.innerHTML = ` + link + + + + + + +
+ `; + const focus = (id: string): void => { + document.getElementById(id)!.focus(); + }; + + focus("link"); + expect(ims.blockKeyboardShortcutFieldActive()).toBe(false); + + focus("button"); + expect(ims.blockKeyboardShortcutFieldActive()).toBe(false); + + focus("checkbox"); + expect(ims.blockKeyboardShortcutFieldActive()).toBe(false); + + focus("text"); + expect(ims.blockKeyboardShortcutFieldActive()).toBe(true); + + focus("search"); + expect(ims.blockKeyboardShortcutFieldActive()).toBe(true); + + focus("textarea"); + expect(ims.blockKeyboardShortcutFieldActive()).toBe(true); + + focus("select"); + expect(ims.blockKeyboardShortcutFieldActive()).toBe(true); + + focus("editable"); + expect(ims.blockKeyboardShortcutFieldActive()).toBe(true); + + document.body.innerHTML = ""; +}); + // This is the shape DataTables leaves behind: the table sits inside a // .dt-container, and the header the user sees is a *clone* in a separate table // (.dt-scroll-head), while the real table's own header is hidden. Sort controls