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:
@@ -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>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user