// @vitest-environment jsdom import { beforeEach, describe, expect, it } from "vitest"; import { ensureFocusWithin, focusFirst, focusLast, getFocusableElements, isElementDisabled, isElementHidden, isFocusable, wrapFocus, } from "../src/utils/focusManagement"; function renderFocusFixture(): HTMLElement { document.body.innerHTML = `
No href Catalogue
Programmatic only
Zero tab index
Editable
`; return document.getElementById("fixture") as HTMLElement; } describe("focusManagement", () => { beforeEach(() => { document.body.innerHTML = ""; }); it("returns keyboard-focusable descendants in browser tab order", () => { const fixture = renderFocusFixture(); expect(getFocusableElements(fixture).map((element) => element.id)).toEqual([ "positive-two", "positive-five", "link", "button", "input", "zero", "editable", ]); }); it("filters hidden, disabled, and programmatic-only elements by default", () => { const fixture = renderFocusFixture(); expect(isElementDisabled(document.getElementById("disabled") as HTMLElement)).toBe(true); expect(isElementDisabled(document.getElementById("aria-disabled") as HTMLElement)).toBe(true); expect(isElementHidden(document.getElementById("aria-hidden-child") as HTMLElement)).toBe(true); expect(isElementHidden(document.getElementById("inert-child") as HTMLElement)).toBe(true); expect(isElementHidden(document.getElementById("display-none-child") as HTMLElement)).toBe(true); expect(isFocusable(document.getElementById("nohref"))).toBe(false); expect(isFocusable(document.getElementById("hidden-input"))).toBe(false); expect(isFocusable(document.getElementById("negative"))).toBe(false); expect(isFocusable(document.getElementById("negative"), { includeNegativeTabIndex: true })).toBe( true, ); expect(getFocusableElements(fixture)).not.toContain(document.getElementById("disabled")); expect(getFocusableElements(fixture)).not.toContain(document.getElementById("aria-disabled")); expect(getFocusableElements(fixture)).not.toContain(document.getElementById("negative")); }); it("can include a programmatically focusable container after tabbable children", () => { document.body.innerHTML = `
`; const panel = document.getElementById("panel") as HTMLElement; expect(getFocusableElements(panel).map((element) => element.id)).toEqual(["inside"]); expect( getFocusableElements(panel, { includeContainer: true, includeNegativeTabIndex: true, }).map((element) => element.id), ).toEqual(["inside", "panel"]); }); it("moves focus to first and last focusable controls", () => { document.body.innerHTML = `
`; const trap = document.getElementById("trap") as HTMLElement; expect(focusFirst(trap)).toBe(true); expect(document.activeElement?.id).toBe("first"); expect(focusLast(trap)).toBe(true); expect(document.activeElement?.id).toBe("last"); }); it("falls back to the container when explicitly requested", () => { document.body.innerHTML = `
`; const empty = document.getElementById("empty") as HTMLElement; expect(focusFirst(empty)).toBe(false); expect( ensureFocusWithin(empty, { includeContainer: true, includeNegativeTabIndex: true, }), ).toBe(true); expect(document.activeElement?.id).toBe("empty"); }); it("wraps tab focus at the beginning and end of a trapped region", () => { document.body.innerHTML = `
`; const trap = document.getElementById("trap") as HTMLElement; const first = document.getElementById("first") as HTMLButtonElement; const last = document.getElementById("last") as HTMLButtonElement; const outside = document.getElementById("outside") as HTMLButtonElement; last.focus(); const forwardEvent = new KeyboardEvent("keydown", { key: "Tab", cancelable: true, }); expect(wrapFocus(trap, forwardEvent)).toBe(true); expect(forwardEvent.defaultPrevented).toBe(true); expect(document.activeElement).toBe(first); first.focus(); const backwardEvent = new KeyboardEvent("keydown", { key: "Tab", shiftKey: true, cancelable: true, }); expect(wrapFocus(trap, backwardEvent)).toBe(true); expect(backwardEvent.defaultPrevented).toBe(true); expect(document.activeElement).toBe(last); outside.focus(); const outsideEvent = new KeyboardEvent("keydown", { key: "Tab", cancelable: true, }); expect(wrapFocus(trap, outsideEvent)).toBe(true); expect(outsideEvent.defaultPrevented).toBe(true); expect(document.activeElement).toBe(first); }); });