返回 CodeWhale
slash-shortcut.ts
根目录 / web / lib / slash-shortcut.ts
1 /**
2 * The "/" focus-search shortcut shared by the FAQ and docs search boxes.
3 *
4 * A single-character shortcut must not steal keys the user is typing
5 * elsewhere (WCAG 2.1.4 Character Key Shortcuts): it ignores modified keys,
6 * IME composition, keys an inner widget already handled (defaultPrevented),
7 * and any event aimed at an editable control — native fields,
8 * contenteditable, or an ARIA text widget — including one inside a shadow
9 * root, which the window listener would otherwise only see as its host.
10 */
11
12 type ShortcutTarget = {
13 tagName?: string;
14 isContentEditable?: boolean;
15 getAttribute?: (name: string) => string | null;
16 } | null;
17
18 export type ShortcutKeyEvent = Pick<
19 KeyboardEvent,
20 "key" | "ctrlKey" | "metaKey" | "altKey" | "isComposing"
21 > & {
22 target: EventTarget | ShortcutTarget | null;
23 defaultPrevented?: boolean;
24 composedPath?: () => ReadonlyArray<EventTarget | ShortcutTarget>;
25 };
26
27 const EDITABLE_TAGS = new Set(["INPUT", "TEXTAREA", "SELECT"]);
28 const EDITABLE_ROLES = new Set(["textbox", "searchbox", "combobox"]);
29
30 export function isEditableTarget(target: EventTarget | ShortcutTarget | null): boolean {
31 if (!target || typeof target !== "object") return false;
32 const el = target as ShortcutTarget & object;
33 if (el.isContentEditable) return true;
34 if (typeof el.tagName === "string" && EDITABLE_TAGS.has(el.tagName.toUpperCase())) return true;
35 const role = typeof el.getAttribute === "function" ? el.getAttribute("role") : null;
36 return role !== null && EDITABLE_ROLES.has(role.trim().toLowerCase());
37 }
38
39 export function isSlashShortcut(e: ShortcutKeyEvent): boolean {
40 if (e.key !== "/") return false;
41 if (e.defaultPrevented) return false;
42 if (e.ctrlKey || e.metaKey || e.altKey || e.isComposing) return false;
43 const origin = e.composedPath?.()[0] ?? e.target;
44 return !isEditableTarget(origin);
45 }
46
46 lines TYPESCRIPT