Files
beacon-web/tests/hooks/useFocusTrap.test.tsx
MrAlders0n 39641b6339 ui: footer version from package.json plus a batch of interaction fixes
- footer read 'v0.1' forever; it's injected from package.json at build
  time now
- region picker shows 'Failed to load' instead of an eternal Loading
  when /iatas errors
- selecting text in a modal and releasing over the backdrop no longer
  closes it (close requires the press to start on the backdrop)
- Escape in a dropdown inside the mobile filter sheet closes just the
  dropdown, not the whole sheet
- Shift+Tab right after a modal opens wraps to the last control instead
  of escaping the focus trap
- formatCount rolls 999,999 over to 1M instead of printing 1000k
- TABS list moved to lib/constants so AppShell only exports components
2026-06-10 07:03:16 -04:00

83 lines
3.3 KiB
TypeScript

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<HTMLDivElement>(null);
useFocusTrap(ref);
return (
<div ref={ref} tabIndex={-1} data-testid="dialog">
{children}
</div>
);
}
function Harness({ trapped }: { trapped: boolean }) {
return (
<div>
<button data-testid="outside">outside</button>
{trapped && (
<TrapBox>
<button data-testid="first">first</button>
<button data-testid="last">last</button>
</TrapBox>
)}
</div>
);
}
describe("useFocusTrap", () => {
it("focuses the container itself on mount (so the dialog name is announced before its controls)", () => {
render(<Harness trapped={true} />);
expect(document.activeElement).toBe(screen.getByTestId("dialog"));
});
it("wraps focus to the first element when Tab is pressed on the last", () => {
render(<Harness trapped={true} />);
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(<Harness trapped={true} />);
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(<Harness trapped={true} />);
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 <body> on unmount when nothing was focused on open", () => {
// Opened from a click that left focus on <body> (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(<Harness trapped={false} />);
expect(document.activeElement).toBe(document.body);
rerender(<Harness trapped={true} />); // trap focuses the container; restoreTo is <body>
screen.getByTestId("outside").focus(); // focus subsequently moves elsewhere
rerender(<Harness trapped={false} />); // 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(<Harness trapped={false} />);
screen.getByTestId("outside").focus();
expect(document.activeElement).toBe(screen.getByTestId("outside"));
rerender(<Harness trapped={true} />);
expect(document.activeElement).toBe(screen.getByTestId("dialog"));
rerender(<Harness trapped={false} />);
expect(document.activeElement).toBe(screen.getByTestId("outside"));
});
});