Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/modal/modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,60 @@ describe("<Modal />", () => {
expect(closeButton).toHaveFocus();
});

it("should not apply a focus ring to the dialog container", async () => {
vi.useFakeTimers();

// Simulate the race where document.activeElement is the dialog root at
// the moment afterEnter fires (HeadlessUI's transient focus state).
const dialogContainer = document.createElement("div");
dialogContainer.setAttribute("role", "dialog");
const activeElementSpy = vi
.spyOn(document, "activeElement", "get")
.mockReturnValue(dialogContainer);

const { rerender } = render(
<Modal open={false} onClose={() => {}}>
<Modal.Header>Lorem ipsum</Modal.Header>
<Modal.Body>
<p>Laboriosam autem non et nisi.</p>
</Modal.Body>
<Modal.Footer>
<Button block size="large">
Submit
</Button>
</Modal.Footer>
</Modal>,
);

rerender(
<Modal open={true} onClose={() => {}}>
<Modal.Header>Lorem ipsum</Modal.Header>
<Modal.Body>
<p>Laboriosam autem non et nisi.</p>
</Modal.Body>
<Modal.Footer>
<Button block size="large">
Submit
</Button>
</Modal.Footer>
</Modal>,
);

act(() => {
vi.runAllTimers();
});
act(() => {
vi.runAllTimers();
});
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(async () => {});

activeElementSpy.mockRestore();

expect(dialogContainer.style.outline).toBe("");
expect(dialogContainer.style.outlineOffset).toBe("");
});

it("should show a focus ring on the initially focused element after the modal opens", async () => {
// headlessui only fires afterEnter when `show` transitions false → true (not on
// initial mount). Use fake timers so requestAnimationFrame callbacks — which
Expand Down
12 changes: 11 additions & 1 deletion src/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,18 @@ const Modal = ({
}

const showFocusRingOnInitialFocus = () => {
// Guard against container elements (body, html, the dialog root) that
// HeadlessUI may temporarily focus during its enter transition. Those
// elements are fixed inset-0, so an outline on them renders as blue
// lines at the viewport edges.
const el = document.activeElement as HTMLElement | null;
if (!el) return;
if (
!el ||
el === document.body ||
el === document.documentElement ||
el.getAttribute("role") === "dialog"
)
return;

el.style.outline = "3px solid var(--focus-outline-color)";
el.style.outlineOffset = "0px";
Expand Down
Loading