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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 11:04:40 +02:00
co-authored by Claude Opus 5
parent 8a2d8c0327
commit ff16b12b3b
3 changed files with 20 additions and 2 deletions
+18
View File
@@ -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;
}
}
+1 -1
View File
@@ -139,7 +139,7 @@
@endif
@if ($body)
<div id="{{ $id }}-body" x-dialog-dividers @class([
<div id="{{ $id }}-body" data-dialog-body x-dialog-dividers @class([
'min-h-0 flex-1 overflow-y-auto px-6 type-body-md text-on-surface-variant',
'pt-2' => $header,
'max-medium:pt-4' => $header && $barAboveBody,
+1 -1
View File
@@ -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('/<div\s+data-dialog-head\s+class="shrink-0 px-6 pt-6 pb-2"\s*>/')
->toMatch('/<div id="[^"]+-body" x-dialog-dividers class="min-h-0 flex-1 overflow-y-auto px-6 type-body-md text-on-surface-variant pt-2 pb-2">\s*<div data-dialog-content>Body<\/div>\s*<\/div>/')
->toMatch('/<div id="[^"]+-body" data-dialog-body x-dialog-dividers class="min-h-0 flex-1 overflow-y-auto px-6 type-body-md text-on-surface-variant pt-2 pb-2">\s*<div data-dialog-content>Body<\/div>\s*<\/div>/')
->toMatch('/<div\s+data-dialog-actions\s+class="flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pb-6 pt-4"\s*>/');
});