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
31 changes: 16 additions & 15 deletions web/typescript/ims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>([
"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
Expand All @@ -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
Expand Down
43 changes: 43 additions & 0 deletions web/typescripttest/ims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<a id="link" href="#">link</a>
<button id="button">button</button>
<input id="checkbox" type="checkbox">
<input id="text" type="text">
<input id="search" type="search">
<textarea id="textarea"></textarea>
<select id="select"><option>a</option></select>
<div id="editable" contenteditable="true" tabindex="0"></div>
`;
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
Expand Down
Loading