// @vitest-environment jsdom import { afterEach, describe, expect, it } from "vitest"; import { act, cleanup, render } from "@testing-library/react"; import { useRef, useState } from "react"; import { useFocusKeeper } from "./useFocusKeeper"; afterEach(cleanup); const flush = () => act(() => new Promise((r) => setTimeout(r, 0))); function Rows({ nav = false }: { nav?: boolean }) { const ref = useRef(null); useFocusKeeper(ref); const [rows, setRows] = useState(["a", "b", "c"]); return (

Invites

{rows.map((r, i) => (
))}
); } const btn = (name: string) => [...document.querySelectorAll("button")].find((b) => b.textContent === name)!; describe("useFocusKeeper", () => { it("keeps the focus in the removed row's slot — the next row takes it", async () => { render(); btn("approve b").focus(); act(() => btn("approve b").click()); await flush(); expect(document.activeElement).toBe(btn("approve c")); }); it("falls back to what followed the list once the last row is gone", async () => { render(); btn("approve c").focus(); act(() => btn("approve c").click()); await flush(); expect(document.activeElement).toBe(btn("after the list")); }); // A page swapped for its loading fallback leaves nothing focusable in the slot. The walk up used to // run past the container and hand the focus to the first control of the whole document. it("never puts the focus outside its own container", async () => { function Swapped() { const ref = useRef(null); useFocusKeeper(ref); const [loaded, setLoaded] = useState(true); return ( <>
{loaded ? :

Loading…

}
); } render(); btn("go").focus(); act(() => btn("go").click()); await flush(); expect(document.activeElement).toBe(document.body); }); it("leaves a keyboard list's rows to listNav", async () => { render(); btn("approve b").focus(); act(() => btn("approve b").click()); await flush(); expect(document.activeElement).toBe(document.body); }); });