157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
import { createRoot } from "solid-js";
|
|
import { describe, expect, it } from "vitest";
|
|
import { createFocusTrap } from "../../src/utilities/focus-trap/create-focus-trap";
|
|
|
|
describe("createFocusTrap", () => {
|
|
it("focuses the first focusable element when activated", () => {
|
|
const container = document.createElement("div");
|
|
const button1 = document.createElement("button");
|
|
const button2 = document.createElement("button");
|
|
button1.textContent = "First";
|
|
button2.textContent = "Second";
|
|
container.appendChild(button1);
|
|
container.appendChild(button2);
|
|
document.body.appendChild(container);
|
|
|
|
createRoot((dispose) => {
|
|
const trap = createFocusTrap(() => container);
|
|
trap.activate();
|
|
expect(document.activeElement).toBe(button1);
|
|
trap.deactivate();
|
|
dispose();
|
|
});
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
it("does nothing when container is null", () => {
|
|
createRoot((dispose) => {
|
|
const trap = createFocusTrap(() => null);
|
|
// Should not throw
|
|
expect(() => trap.activate()).not.toThrow();
|
|
dispose();
|
|
});
|
|
});
|
|
|
|
it("returns focus to previously focused element on deactivate", () => {
|
|
const outside = document.createElement("button");
|
|
outside.textContent = "Outside";
|
|
document.body.appendChild(outside);
|
|
outside.focus();
|
|
|
|
const container = document.createElement("div");
|
|
const inner = document.createElement("button");
|
|
inner.textContent = "Inner";
|
|
container.appendChild(inner);
|
|
document.body.appendChild(container);
|
|
|
|
createRoot((dispose) => {
|
|
const trap = createFocusTrap(() => container);
|
|
trap.activate();
|
|
expect(document.activeElement).toBe(inner);
|
|
trap.deactivate();
|
|
expect(document.activeElement).toBe(outside);
|
|
dispose();
|
|
});
|
|
|
|
document.body.removeChild(outside);
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
it("wraps Tab forward from last focusable to first", () => {
|
|
const container = document.createElement("div");
|
|
const button1 = document.createElement("button");
|
|
const button2 = document.createElement("button");
|
|
button1.textContent = "First";
|
|
button2.textContent = "Second";
|
|
container.appendChild(button1);
|
|
container.appendChild(button2);
|
|
document.body.appendChild(container);
|
|
|
|
createRoot((dispose) => {
|
|
const trap = createFocusTrap(() => container);
|
|
trap.activate();
|
|
button2.focus();
|
|
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
document.dispatchEvent(event);
|
|
expect(document.activeElement).toBe(button1);
|
|
trap.deactivate();
|
|
dispose();
|
|
});
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
it("wraps Shift+Tab backward from first focusable to last", () => {
|
|
const container = document.createElement("div");
|
|
const button1 = document.createElement("button");
|
|
const button2 = document.createElement("button");
|
|
button1.textContent = "First";
|
|
button2.textContent = "Second";
|
|
container.appendChild(button1);
|
|
container.appendChild(button2);
|
|
document.body.appendChild(container);
|
|
|
|
createRoot((dispose) => {
|
|
const trap = createFocusTrap(() => container);
|
|
trap.activate();
|
|
// button1 is already focused after activate
|
|
const event = new KeyboardEvent("keydown", {
|
|
key: "Tab",
|
|
shiftKey: true,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
});
|
|
document.dispatchEvent(event);
|
|
expect(document.activeElement).toBe(button2);
|
|
trap.deactivate();
|
|
dispose();
|
|
});
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
it("does not throw when container has no focusable elements", () => {
|
|
const container = document.createElement("div");
|
|
container.textContent = "No focusable children";
|
|
document.body.appendChild(container);
|
|
|
|
createRoot((dispose) => {
|
|
const trap = createFocusTrap(() => container);
|
|
trap.activate();
|
|
// Tab should be prevented but not throw
|
|
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
expect(() => document.dispatchEvent(event)).not.toThrow();
|
|
trap.deactivate();
|
|
dispose();
|
|
});
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
it("second activate() is a no-op — does not attach duplicate listeners", () => {
|
|
const container = document.createElement("div");
|
|
const button = document.createElement("button");
|
|
container.appendChild(button);
|
|
document.body.appendChild(container);
|
|
|
|
createRoot((dispose) => {
|
|
const trap = createFocusTrap(() => container);
|
|
trap.activate();
|
|
trap.activate(); // second call should be ignored
|
|
button.focus();
|
|
// Tab from the only element — Tab wraps back to itself (first === last)
|
|
// What matters is it doesn't fire twice causing erratic behaviour
|
|
expect(() => {
|
|
document.dispatchEvent(
|
|
new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true }),
|
|
);
|
|
}).not.toThrow();
|
|
trap.deactivate();
|
|
dispose();
|
|
});
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
});
|