From ff16b12b3b749f1a18b8e657716e280e5bc61e3a Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 11:04:40 +0200 Subject: [PATCH] Draw M3's focus ring on a dialog's scrolling body Chrome makes a scroll container with nothing focusable inside a keyboard stop, and showModal() gives it the dialog's first focus, which showed the browser's own ring. The body now takes M3's 3px secondary indicator, inset so the dialog's rounded, overflow-hidden box does not clip it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components/dialog.css | 18 ++++++++++++++++++ resources/views/components/modal.blade.php | 2 +- tests/Feature/Components/OverlayTest.php | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/resources/css/components/dialog.css b/resources/css/components/dialog.css index ff85919e..74f2d82c 100644 --- a/resources/css/components/dialog.css +++ b/resources/css/components/dialog.css @@ -56,3 +56,21 @@ opacity: 1; } } + +/* + * The scrolling body is a keyboard stop of its own when nothing inside it can take focus: Chrome + * makes a scroll container focusable so the keyboard can scroll it, and `showModal()` hands it the + * dialog's first focus. It then showed the browser's own ring. It takes M3's focus indicator + * instead — 3px in secondary (tokens/state.css) — drawn inside its edge, because the dialog's + * rounded, overflow-hidden box would clip a ring drawn 2px outside. + */ +@layer components { + [data-dialog-body] { + outline: none; + } + + [data-dialog-body]:focus-visible { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: -3px; + } +} diff --git a/resources/views/components/modal.blade.php b/resources/views/components/modal.blade.php index ec150aad..bb28feeb 100644 --- a/resources/views/components/modal.blade.php +++ b/resources/views/components/modal.blade.php @@ -139,7 +139,7 @@ @endif @if ($body) -
$header, 'max-medium:pt-4' => $header && $barAboveBody, diff --git a/tests/Feature/Components/OverlayTest.php b/tests/Feature/Components/OverlayTest.php index 9c048aa8..3f78f736 100644 --- a/tests/Feature/Components/OverlayTest.php +++ b/tests/Feature/Components/OverlayTest.php @@ -63,7 +63,7 @@ it('pins a dialog\'s headline and actions and scrolls only the body between them ->toContain('overflow-hidden rounded-corner-xl bg-surface-container-high shadow-elevation-3') ->not->toContain('overflow-y-auto rounded-corner-xl') ->toMatch('//') - ->toMatch('/
\s*
Body<\/div>\s*<\/div>/') + ->toMatch('/
\s*
Body<\/div>\s*<\/div>/') ->toMatch('//'); });