refactor(e10): move the platform components into components/ui/ and enforce the boundary

The app HAS a platform layer -- AnchoredPopover (22 importers, with
arrows/typeahead/Escape/anchor-flip), QueryState (21), Modal (17),
PageShell, DataTable, Tabs, Tooltip and ~26 more -- but it sat flat among
~160 feature components with nothing marking it as one. Several of those
files describe themselves as 'the one place' or 'canonical' and were
still bypassed by hand-rolled markup, because a layer nobody can see is a
layer nobody uses; three separate inventory passes of this codebase
misread it the same way.

Moves the 33 domain-free modules into components/ui/, rewrites the
imports in 89 files, and makes the boundary a lint rule rather than a
convention: inside ui/ a one-level-up import (a feature component) and
any lib/api slice are errors. The rule was verified to fire on both and
to leave generic lib/ helpers alone. README.md states which side of the
line a component belongs on and why.

No behaviour change: 3x tsc, eslint, knip, prettier and 687 vitest tests
all green; no backend file touched.
This commit is contained in:
2026-09-05 16:58:29 +02:00
parent 31fa554b87
commit 5bfcf5d83a
138 changed files with 262 additions and 189 deletions
@@ -0,0 +1,191 @@
// @vitest-environment jsdom
//
// Modal: focus management (FE-9). This was DEFERRED from the E4 deep-review because it's
// jsdom-untestable as written — `focusablesIn` filters on `el.offsetParent !== null`, and jsdom
// returns null for EVERY element (it never lays out), so the trap would see zero focusables and do
// nothing. We stub `HTMLElement.prototype.offsetParent` to report "visible" (non-null) for any
// connected node, which is exactly what a real browser reports for the shown dialog controls; the
// separate no-stub describe below documents the raw-jsdom reality (empty → the guard no-ops).
//
// The overlay/history machinery (useBackToClose) is exercised in history.dom.test.ts, so it's mocked
// out here to isolate the focus-trap behaviour from its module-global history side-effects.
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render, screen, act, cleanup } from "@testing-library/react";
vi.mock("react-i18next", async (importOriginal) => {
const actual = await importOriginal<typeof import("react-i18next")>();
return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
});
// Isolate the focus trap from the Back-to-close history stack (tested in history.dom.test.ts).
vi.mock("../../lib/history", () => ({ useBackToClose: () => {} }));
import Modal from "./Modal";
// Fire a keydown on window (where Modal installs its handler), inside act.
const press = (key: string, shiftKey = false) =>
act(() => {
window.dispatchEvent(
new KeyboardEvent("keydown", { key, shiftKey, bubbles: true, cancelable: true }),
);
});
const panelEl = () => document.querySelector<HTMLElement>('[tabindex="-1"]')!;
const closeBtn = () => screen.getByRole("button", { name: "common.close" });
// A modal with three extra focusable controls; DOM order inside the panel is [close, A, input, B].
function Body() {
return (
<>
<button>alpha</button>
<input aria-label="field" />
<button>beta</button>
</>
);
}
describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
let origDesc: PropertyDescriptor | undefined;
beforeEach(() => {
origDesc = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetParent");
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
configurable: true,
get(this: HTMLElement) {
return this.isConnected ? document.body : null;
},
});
});
afterEach(() => {
cleanup();
if (origDesc) Object.defineProperty(HTMLElement.prototype, "offsetParent", origDesc);
else delete (HTMLElement.prototype as unknown as Record<string, unknown>).offsetParent;
});
it("moves initial focus to the first control on open", () => {
render(
<Modal title="t" onClose={() => {}}>
<Body />
</Modal>,
);
expect(document.activeElement).toBe(closeBtn());
});
it("Tab from the last control wraps to the first", () => {
render(
<Modal title="t" onClose={() => {}}>
<Body />
</Modal>,
);
screen.getByText("beta").focus(); // last
press("Tab");
expect(document.activeElement).toBe(closeBtn());
});
it("Shift+Tab from the first control wraps to the last", () => {
render(
<Modal title="t" onClose={() => {}}>
<Body />
</Modal>,
);
closeBtn().focus(); // first
press("Tab", true);
expect(document.activeElement).toBe(screen.getByText("beta"));
});
it("Tab pulls focus back inside when it has escaped to the page behind", () => {
const outside = document.createElement("button");
document.body.appendChild(outside);
render(
<Modal title="t" onClose={() => {}}>
<Body />
</Modal>,
);
outside.focus(); // focus escaped the dialog
expect(document.activeElement).toBe(outside);
press("Tab");
expect(document.activeElement).toBe(closeBtn());
document.body.removeChild(outside);
});
it("Shift+Tab from the panel container itself goes to the last control (no leak to the background)", () => {
render(
<Modal title="t" onClose={() => {}}>
<Body />
</Modal>,
);
panelEl().focus(); // active === panel (tabIndex=-1, reached by clicking dead space)
press("Tab", true);
expect(document.activeElement).toBe(screen.getByText("beta"));
});
it("does not trap a Tab in the middle — only the edges wrap", () => {
render(
<Modal title="t" onClose={() => {}}>
<Body />
</Modal>,
);
const mid = screen.getByLabelText("field");
mid.focus();
press("Tab"); // interior: native Tab handles it, the trap must not force-move
expect(document.activeElement).toBe(mid);
});
it("Escape calls onClose", () => {
const onClose = vi.fn();
render(
<Modal title="t" onClose={onClose}>
<Body />
</Modal>,
);
press("Escape");
expect(onClose).toHaveBeenCalledTimes(1);
});
it("Escape closes only the topmost modal", () => {
const outerClose = vi.fn();
const innerClose = vi.fn();
render(
<Modal title="outer" onClose={outerClose}>
<button>outer-body</button>
</Modal>,
);
render(
<Modal title="inner" onClose={innerClose}>
<button>inner-body</button>
</Modal>,
);
press("Escape");
expect(innerClose).toHaveBeenCalledTimes(1);
expect(outerClose).not.toHaveBeenCalled();
});
it("restores focus to the trigger on unmount", () => {
const trigger = document.createElement("button");
document.body.appendChild(trigger);
trigger.focus(); // captured as restoreTo during Modal's first render
const { unmount } = render(
<Modal title="t" onClose={() => {}}>
<Body />
</Modal>,
);
expect(document.activeElement).not.toBe(trigger); // focus moved into the dialog
unmount();
expect(document.activeElement).toBe(trigger);
document.body.removeChild(trigger);
});
});
describe("Modal focus trap (raw jsdom — every offsetParent null)", () => {
afterEach(cleanup);
it("with no visible focusables: initial focus falls to the panel and Tab is a safe no-op", () => {
render(
<Modal title="t" onClose={() => {}}>
<Body />
</Modal>,
);
// focusablesIn() returns [] in raw jsdom, so the initializer focuses the panel itself.
expect(document.activeElement).toBe(panelEl());
expect(() => press("Tab")).not.toThrow();
expect(document.activeElement).toBe(panelEl());
});
});