| 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { isSlashShortcut, type ShortcutKeyEvent } from "./slash-shortcut"; |
| 3 | |
| 4 | function key(overrides: Partial<ShortcutKeyEvent> = {}): ShortcutKeyEvent { |
| 5 | return { |
| 6 | key: "/", |
| 7 | ctrlKey: false, |
| 8 | metaKey: false, |
| 9 | altKey: false, |
| 10 | isComposing: false, |
| 11 | target: { tagName: "BODY", isContentEditable: false }, |
| 12 | ...overrides, |
| 13 | }; |
| 14 | } |
| 15 | |
| 16 | describe("isSlashShortcut (WCAG 2.1.4 character key shortcut)", () => { |
| 17 | it("fires for an unmodified slash typed outside a field", () => { |
| 18 | expect(isSlashShortcut(key())).toBe(true); |
| 19 | expect(isSlashShortcut(key({ target: null }))).toBe(true); |
| 20 | }); |
| 21 | |
| 22 | it("ignores other keys", () => { |
| 23 | expect(isSlashShortcut(key({ key: "?" }))).toBe(false); |
| 24 | }); |
| 25 | |
| 26 | it("ignores modified keys and IME composition", () => { |
| 27 | expect(isSlashShortcut(key({ ctrlKey: true }))).toBe(false); |
| 28 | expect(isSlashShortcut(key({ metaKey: true }))).toBe(false); |
| 29 | expect(isSlashShortcut(key({ altKey: true }))).toBe(false); |
| 30 | expect(isSlashShortcut(key({ isComposing: true }))).toBe(false); |
| 31 | }); |
| 32 | |
| 33 | it.each(["INPUT", "TEXTAREA", "SELECT", "textarea"])("leaves typing in a %s alone", (tagName) => { |
| 34 | expect(isSlashShortcut(key({ target: { tagName } }))).toBe(false); |
| 35 | }); |
| 36 | |
| 37 | it("leaves typing in contenteditable content alone", () => { |
| 38 | expect(isSlashShortcut(key({ target: { tagName: "DIV", isContentEditable: true } }))).toBe(false); |
| 39 | }); |
| 40 | |
| 41 | it("leaves a key an inner widget already handled alone", () => { |
| 42 | expect(isSlashShortcut(key({ defaultPrevented: true }))).toBe(false); |
| 43 | }); |
| 44 | |
| 45 | it.each(["textbox", "searchbox", "combobox", " TextBox "])("leaves typing in a role=%s widget alone", (role) => { |
| 46 | const target = { tagName: "DIV", getAttribute: (name: string) => (name === "role" ? role : null) }; |
| 47 | expect(isSlashShortcut(key({ target }))).toBe(false); |
| 48 | }); |
| 49 | |
| 50 | it("still fires over a role that is not a text widget", () => { |
| 51 | const target = { tagName: "DIV", getAttribute: (name: string) => (name === "role" ? "button" : null) }; |
| 52 | expect(isSlashShortcut(key({ target }))).toBe(true); |
| 53 | }); |
| 54 | |
| 55 | it("looks through a shadow root to the element that received the key", () => { |
| 56 | const host = { tagName: "MY-EDITOR" }; |
| 57 | const inner = { tagName: "INPUT" }; |
| 58 | expect(isSlashShortcut(key({ target: host, composedPath: () => [inner, host] }))).toBe(false); |
| 59 | expect(isSlashShortcut(key({ target: host, composedPath: () => [host] }))).toBe(true); |
| 60 | }); |
| 61 | }); |
| 62 |