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); }); });