import test from "node:test"; import assert from "node:assert/strict"; import { setupDom, teardownDom } from "./_dom.js"; import { focusables, roving, trap } from "../internals/focus.js"; test.beforeEach(() => setupDom()); test.after(() => teardownDom()); function box(html) { const n = document.createElement("div"); n.innerHTML = html; // test fixture, not kit code document.body.appendChild(n); return n; } const tab = (node, shift = false) => node.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Tab", shiftKey: shift, bubbles: true, cancelable: true })); const arrow = (node, key) => node.dispatchEvent(new window.KeyboardEvent("keydown", { key, bubbles: true, cancelable: true })); test("focusables skips disabled and hidden controls", () => { const n = box(` cd`); assert.deepEqual(focusables(n).map((x) => x.textContent || x.tagName), ["a", "c"]); }); test("focus moves into the container on trap", () => { const n = box(""); trap(n); assert.equal(document.activeElement.textContent, "a"); }); test("Tab wraps from last back to first", () => { const n = box(""); trap(n); n.querySelectorAll("button")[1].focus(); tab(n); assert.equal(document.activeElement.textContent, "a"); }); test("Shift+Tab wraps from first back to last", () => { const n = box(""); trap(n); tab(n, true); assert.equal(document.activeElement.textContent, "b"); }); test("focus that escaped the container is pulled back", () => { const outside = box(""); const n = box(""); trap(n); outside.querySelector("button").focus(); tab(n); assert.ok(n.contains(document.activeElement)); }); test("release puts focus back where it came from", () => { // Dropping focus to on close silently ends keyboard navigation. const opener = box("").querySelector("button"); opener.focus(); const n = box(""); const release = trap(n); assert.equal(document.activeElement.textContent, "a"); release(); assert.equal(document.activeElement, opener); }); test("release can decline to restore", () => { const opener = box("").querySelector("button"); opener.focus(); const n = box(""); trap(n)({ restore: false }); assert.notEqual(document.activeElement, opener); }); test("an empty container traps without throwing", () => { const n = box("

nothing focusable

"); assert.doesNotThrow(() => trap(n)); assert.doesNotThrow(() => tab(n)); }); test("roving gives the group one tab stop", () => { const n = box(""); roving(n); const tabindexes = [...n.querySelectorAll("button")].map((b) => b.getAttribute("tabindex")); assert.deepEqual(tabindexes, ["0", "-1", "-1"]); }); test("arrows move within the group and wrap", () => { const n = box(""); roving(n); n.querySelector("button").focus(); arrow(n, "ArrowRight"); assert.equal(document.activeElement.textContent, "b"); arrow(n, "ArrowRight"); arrow(n, "ArrowRight"); assert.equal(document.activeElement.textContent, "a", "wraps past the end"); arrow(n, "ArrowLeft"); assert.equal(document.activeElement.textContent, "c", "wraps before the start"); }); test("Home and End jump to the edges", () => { const n = box(""); roving(n); n.querySelector("button").focus(); arrow(n, "End"); assert.equal(document.activeElement.textContent, "c"); arrow(n, "Home"); assert.equal(document.activeElement.textContent, "a"); }); test("a vertical group uses up and down", () => { const n = box(""); roving(n, { orientation: "vertical" }); n.querySelector("button").focus(); arrow(n, "ArrowDown"); assert.equal(document.activeElement.textContent, "b"); arrow(n, "ArrowRight"); assert.equal(document.activeElement.textContent, "b", "the cross axis is ignored"); }); test("arrow keys outside the group are left alone", () => { const n = box(""); roving(n); document.body.focus(); assert.doesNotThrow(() => arrow(n, "ArrowRight")); }); test("a modal trap pulls back focus that left the card, and only the top modal acts", () => { const behind = box(""); const lower = box(""), upper = box(""); const a = trap(lower, { modal: true }), b = trap(upper, { modal: true }); behind.querySelector("button").focus(); tab(document.body); assert.equal(document.activeElement.textContent, "upper"); b(); a(); }); test("Shift+Tab from the card itself (clicked text) goes to the last control, not out", () => { const n = box(""); n.tabIndex = -1; const rel = trap(n, { modal: true }); n.focus(); tab(document.body, true); assert.equal(document.activeElement.textContent, "b"); rel(); });