From d86392fd471937edaff67fa4bed85a319b49dfda Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 22:02:16 +0200 Subject: [PATCH] Keep a nested dialog's head out of the full-screen phone rules Plan step 36 review of 3c235ecc. The rules that hide a full-screen dialog's head and title below 600px, zero its subtitle's margin and shorten its action bar matched descendants of the full-screen dialog, so a basic confirm opened from inside its body lost its title and its padding on a phone. They now go through > [data-md-modal-box] >, as the divider marks already did (old dialog.css: "the child combinators tie a mark to its own dialog's rows"), and the icon and subtitle checks look only at the head's own children. The computed data-md-modal-divider flag stays. It is correct (the old view's data-dialog-head placement, one for one), and though a :has() chain on the box could say when the bar owns the rule, the flag keeps bar, head and actions on one hook the view already has the facts for. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components/modal.css | 16 ++++++++++------ tests/Feature/Components/OverlayTest.php | 10 +++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/resources/css/components/modal.css b/resources/css/components/modal.css index 6f169341..23749268 100644 --- a/resources/css/components/modal.css +++ b/resources/css/components/modal.css @@ -26,6 +26,10 @@ * the head already owns); `data-md-separator` (the `separator` prop) draws the rule whatever the * scroll. The rule is a pseudo-element over the row's own edge, so showing it moves nothing. * + * Every rule that reads the full-screen flag or the head's icon and subtitle goes through + * `> [data-md-modal-box] >`, as the divider marks do: a basic dialog opened from inside a + * full-screen dialog's body is a descendant of it, and must keep its own title and padding. + * * `[data-md-modal-body]:focus-visible` is Chrome's own focusable scroll container catching the * dialog's first focus when nothing inside can take it — M3's 3px secondary indicator, drawn * inside the edge because the box's rounded, overflow-hidden corner would clip one drawn outside. @@ -128,14 +132,14 @@ padding-bottom: var(--md-sys-measurement-space100); } - [data-md-modal-head]:has([data-md-icon]) { + [data-md-modal-head]:has(> [data-md-icon]) { text-align: center; } /* On a phone the bar already names a plain dialog; the head hides unless an icon or a subtitle keeps it there (the bar cannot hold either). */ @media (width < 600px) { - [data-md-modal][data-md-fullscreen] [data-md-modal-head]:not(:has([data-md-icon])):not(:has([data-md-modal-subtitle])) { + [data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon], > [data-md-modal-subtitle])) { display: none; } } @@ -152,7 +156,7 @@ } @media (width < 600px) { - [data-md-modal][data-md-fullscreen]:not(:has([data-md-modal-head] [data-md-icon])) [data-md-modal-title] { + [data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon])) > [data-md-modal-title] { display: none; } } @@ -169,7 +173,7 @@ } @media (width < 600px) { - [data-md-modal][data-md-fullscreen]:not(:has([data-md-modal-head] [data-md-icon])) [data-md-modal-subtitle] { + [data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon])) > [data-md-modal-subtitle] { margin-top: 0; } } @@ -197,7 +201,7 @@ } @media (width < 600px) { - [data-md-modal][data-md-fullscreen] [data-md-modal-head]:not(:has([data-md-icon])):not(:has([data-md-modal-subtitle])) ~ [data-md-modal-body] { + [data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon], > [data-md-modal-subtitle])) ~ [data-md-modal-body] { padding-top: var(--md-sys-measurement-space200); } } @@ -223,7 +227,7 @@ } @media (width < 600px) { - [data-md-modal][data-md-fullscreen] [data-md-modal-actions] { + [data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-actions] { min-height: var(--md-sys-measurement-space700); padding-top: var(--md-sys-measurement-space100); padding-bottom: var(--md-sys-measurement-space100); diff --git a/tests/Feature/Components/OverlayTest.php b/tests/Feature/Components/OverlayTest.php index 5849f45a..debf4307 100644 --- a/tests/Feature/Components/OverlayTest.php +++ b/tests/Feature/Components/OverlayTest.php @@ -59,9 +59,13 @@ it('keeps a full-screen dialog\'s subtitle on a phone, where its bar carries the // The markup is the same at every width; modal.css (ContainmentStylesheetsTest) decides which // of the bar/head/title/subtitle show below 600px, keyed on the icon and the subtitle. expect(File::get(__DIR__.'/../../../resources/css/components/modal.css')) - ->toContain('[data-md-modal][data-md-fullscreen] [data-md-modal-head]:not(:has([data-md-icon])):not(:has([data-md-modal-subtitle]))') - ->toContain('[data-md-modal][data-md-fullscreen]:not(:has([data-md-modal-head] [data-md-icon])) [data-md-modal-title]') - ->toContain('[data-md-modal][data-md-fullscreen]:not(:has([data-md-modal-head] [data-md-icon])) [data-md-modal-subtitle]'); + ->toContain('[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon], > [data-md-modal-subtitle]))') + ->toContain('[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon])) > [data-md-modal-title]') + ->toContain('[data-md-modal][data-md-fullscreen] > [data-md-modal-box] > [data-md-modal-head]:not(:has(> [data-md-icon])) > [data-md-modal-subtitle]'); + + // Each through its own box, so a basic dialog opened inside a full-screen one's body keeps its + // title and padding on a phone. + expect(File::get(__DIR__.'/../../../resources/css/components/modal.css'))->not->toMatch('/\[data-md-fullscreen\](?::[^ ]+)? \[/'); }); it('pins a dialog\'s headline and actions and scrolls only the body between them', function () {