import { useRef, type ReactNode } from "react"; import { describe, it, expect } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { useFocusTrap } from "../../src/hooks/useFocusTrap"; function TrapBox({ children }: { children: ReactNode }) { const ref = useRef(null); useFocusTrap(ref); return (
{children}
); } function Harness({ trapped }: { trapped: boolean }) { return (
{trapped && ( )}
); } describe("useFocusTrap", () => { it("focuses the container itself on mount (so the dialog name is announced before its controls)", () => { render(); expect(document.activeElement).toBe(screen.getByTestId("dialog")); }); it("wraps focus to the first element when Tab is pressed on the last", () => { render(); screen.getByTestId("last").focus(); fireEvent.keyDown(screen.getByTestId("last"), { key: "Tab" }); expect(document.activeElement).toBe(screen.getByTestId("first")); }); it("wraps focus to the last element when Shift+Tab is pressed on the first", () => { render(); screen.getByTestId("first").focus(); fireEvent.keyDown(screen.getByTestId("first"), { key: "Tab", shiftKey: true }); expect(document.activeElement).toBe(screen.getByTestId("last")); }); it("wraps Shift+Tab to the last element while the container itself is focused", () => { // regression: right after open, focus sits on the container — Shift+Tab escaped behind the overlay render(); expect(document.activeElement).toBe(screen.getByTestId("dialog")); fireEvent.keyDown(screen.getByTestId("dialog"), { key: "Tab", shiftKey: true }); expect(document.activeElement).toBe(screen.getByTestId("last")); }); it("does not yank focus to on unmount when nothing was focused on open", () => { // Opened from a click that left focus on (e.g. a Safari/Firefox button or a non-focusable // row). On close we must not call body.focus() and blur whatever the user moved to next. const { rerender } = render(); expect(document.activeElement).toBe(document.body); rerender(); // trap focuses the container; restoreTo is screen.getByTestId("outside").focus(); // focus subsequently moves elsewhere rerender(); // unmount must leave focus on "outside", not reset to body expect(document.activeElement).toBe(screen.getByTestId("outside")); }); it("restores focus to the previously focused element on unmount", () => { const { rerender } = render(); screen.getByTestId("outside").focus(); expect(document.activeElement).toBe(screen.getByTestId("outside")); rerender(); expect(document.activeElement).toBe(screen.getByTestId("dialog")); rerender(); expect(document.activeElement).toBe(screen.getByTestId("outside")); }); });