feat(design): dialogs open by purpose and keep their commit row pinned (E28 S3f1)

Modal takes a required initialFocus (field / close / action), a field that
loads late takes the focus while it still rests on the close button, and
ModalFooter portals a dialog's commit row under the scrolling body from any
depth. The header and footer fades follow the body's scroll timeline, and the
viewport's interactive-widget=resizes-content keeps the footer above an
on-screen keyboard.
This commit is contained in:
2026-09-27 01:33:37 +02:00
parent 4c63d6660d
commit 3ef2c38eb4
34 changed files with 505 additions and 113 deletions
+21 -21
View File
@@ -66,7 +66,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
it("moves initial focus to the first control on open", () => {
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -75,7 +75,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
it("Tab from the last control wraps to the first", () => {
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -86,7 +86,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
it("Shift+Tab from the first control wraps to the last", () => {
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -99,7 +99,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
// takes Escape first, but deferring the Tab trap to it let Shift+Tab walk out behind the scrim.
it("keeps trapping while only a hover card is open above it", () => {
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -117,7 +117,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
// The dialog's toast host sits beside the panel; trapped to the panel, a toast's Undo or dismiss
// could not be reached from the keyboard at all.
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -143,7 +143,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
const outside = document.createElement("button");
document.body.appendChild(outside);
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -156,7 +156,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
it("Shift+Tab from the panel container itself goes to the last control (no leak to the background)", () => {
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -167,7 +167,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
it("does not trap a Tab in the middle — only the edges wrap", () => {
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -180,7 +180,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
it("Escape calls onClose", () => {
const onClose = vi.fn();
render(
<Modal title="t" onClose={onClose}>
<Modal initialFocus="close" title="t" onClose={onClose}>
<Body />
</Modal>,
);
@@ -192,12 +192,12 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
const outerClose = vi.fn();
const innerClose = vi.fn();
render(
<Modal title="outer" onClose={outerClose}>
<Modal initialFocus="close" title="outer" onClose={outerClose}>
<button>outer-body</button>
</Modal>,
);
render(
<Modal title="inner" onClose={innerClose}>
<Modal initialFocus="close" title="inner" onClose={innerClose}>
<button>inner-body</button>
</Modal>,
);
@@ -211,7 +211,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
document.body.appendChild(trigger);
trigger.focus(); // captured as restoreTo during Modal's first render
const { unmount } = render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -230,7 +230,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
document.body.appendChild(trigger);
trigger.focus();
const { unmount } = render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -256,10 +256,10 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
function Nested() {
const [open, setOpen] = useState(false);
return (
<Modal title="outer" onClose={() => {}}>
<Modal initialFocus="close" title="outer" onClose={() => {}}>
<button onClick={() => setOpen(true)}>open inner</button>
{open && (
<Modal title="inner" onClose={() => {}}>
<Modal initialFocus="close" title="inner" onClose={() => {}}>
<button>inner-body</button>
</Modal>
)}
@@ -291,11 +291,11 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
return (
<>
{lower && (
<Modal title="lower" onClose={() => {}}>
<Modal initialFocus="close" title="lower" onClose={() => {}}>
<button onClick={() => setLower(false)}>close lower</button>
</Modal>
)}
<Modal title="upper" onClose={() => {}}>
<Modal initialFocus="close" title="upper" onClose={() => {}}>
<button>upper-body</button>
</Modal>
</>
@@ -319,7 +319,7 @@ describe("Modal focus trap (offsetParent stubbed 'visible')", () => {
document.body.append(trigger, elsewhere);
trigger.focus();
const { unmount } = render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -336,7 +336,7 @@ describe("Modal focus trap (raw jsdom — every offsetParent null)", () => {
it("with no visible focusables: initial focus falls to the panel and Tab is a safe no-op", () => {
render(
<Modal title="t" onClose={() => {}}>
<Modal initialFocus="close" title="t" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -357,7 +357,7 @@ describe("Modal — toasts raised while it is open", () => {
// The app's Toaster is in a stacking context under this body-level portal: a failed share or
// rename was drawn under the scrim, and clicking it landed on the backdrop and closed the dialog.
render(
<Modal title="Share" onClose={() => {}}>
<Modal initialFocus="close" title="Share" onClose={() => {}}>
<Body />
</Modal>,
);
@@ -390,7 +390,7 @@ describe("Modal — an Escape a layer above it already took", () => {
const onClose = vi.fn();
const listClosed = vi.fn();
render(
<Modal title="Share" onClose={onClose}>
<Modal initialFocus="close" title="Share" onClose={onClose}>
<List onClosed={listClosed} />
</Modal>,
);