import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useState } from "react"; import { describe, expect, it, vi } from "vitest"; import { Button } from "@/components/ui/Button"; import { TextField } from "@/components/ui/Field"; import { Modal } from "@/components/ui/Modal"; import { SlideOver } from "@/components/ui/SlideOver"; function ModalHarness({ onClose = () => {} }: { onClose?: () => void }) { const [first, setFirst] = useState(""); const [second, setSecond] = useState(""); return ( {}}>Speichern } > ); } describe("Modal focus management", () => { it("names the dialog from its visible heading", () => { render(); expect(screen.getByRole("dialog", { name: "Testdialog" })).toBeInTheDocument(); }); it("moves focus into the dialog on open", () => { render(); // Not the page behind it: without this, the first Tab would start from // the top of the document. expect(document.body.contains(document.activeElement)).toBe(true); expect(screen.getByRole("dialog").contains(document.activeElement)).toBe(true); }); it("keeps Tab inside the dialog and wraps at the end", async () => { render(); const dialog = screen.getByRole("dialog"); for (let i = 0; i < 8; i++) { await userEvent.tab(); expect(dialog.contains(document.activeElement)).toBe(true); } }); it("wraps backwards on Shift+Tab", async () => { render(); const dialog = screen.getByRole("dialog"); for (let i = 0; i < 8; i++) { await userEvent.tab({ shift: true }); expect(dialog.contains(document.activeElement)).toBe(true); } }); it("closes on Escape", async () => { const onClose = vi.fn(); render(); await userEvent.keyboard("{Escape}"); expect(onClose).toHaveBeenCalled(); }); it("returns focus to the element that opened it", async () => { function Toggle() { const [open, setOpen] = useState(false); return ( <> setOpen(false)} title="Testdialog">

Inhalt

); } render(); const trigger = screen.getByRole("button", { name: "Öffnen" }); await userEvent.click(trigger); expect(screen.getByRole("dialog")).toBeInTheDocument(); await userEvent.keyboard("{Escape}"); expect(trigger).toHaveFocus(); }); }); describe("SlideOver", () => { it("marks a closed panel inert", () => { // It stays mounted for the slide transition, so without `inert` its // fields stay in the tab order behind the page — aria-hidden does not // remove them. // // Asserted on the attribute rather than by tabbing: jsdom does not // implement inert at all ("inert" in HTMLElement.prototype === false), // so a focus-based assertion here would be testing jsdom, not this // component. Browsers enforce it. const { container } = render( {}} title="Geschlossen"> ); expect(container.firstElementChild).toHaveAttribute("inert"); }); it("is not inert while open", () => { const { container } = render( {}} title="Offen"> ); expect(container.firstElementChild).not.toHaveAttribute("inert"); }); it("traps focus once open", async () => { render( <> {}} title="Offen" footer={}> ); const dialog = screen.getByRole("dialog", { name: "Offen" }); for (let i = 0; i < 6; i++) { await userEvent.tab(); expect(dialog.contains(document.activeElement)).toBe(true); } }); });