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
77 lines
3.2 KiB
CSS
77 lines
3.2 KiB
CSS
/*
|
|
* `<x-modal>`'s dividers: the rule under the pinned header and the rule over the pinned actions.
|
|
*
|
|
* M3 lists a divider in the anatomy of both dialogs, 1dp high (docs/reference/m3/
|
|
* components-actions-communication-containment.md § Dialogs → Anatomy, Specs), in outline-variant
|
|
* (DividerTokens: Color = OutlineVariant, Thickness = 1dp, androidx Compose Material 3,
|
|
* Apache-2.0). A scrolling dialog keeps its title and buttons pinned and scrolls only what is
|
|
* between them (§ Dialogs → Behaviour), and a rule there only says something while content is
|
|
* hidden on its far side: the one under the header shows once the body is scrolled away from its
|
|
* top, the one over the actions while more of the body is below. resources/js/dialog.js marks the
|
|
* `<dialog>` with `data-overflow-top` and `data-overflow-bottom`; a row with `data-separator` (the
|
|
* `separator` prop) draws its rule whatever the scroll.
|
|
*
|
|
* Each rule is a pseudo-element laid over the edge of its own row's padding, as Material Web's
|
|
* dialog lays its dividers at the bottom of the headline and the top of the actions
|
|
* (material-components/material-web, dialog/internal/_dialog.scss): it takes no room, so showing
|
|
* it moves nothing, and it lies outside the body's scrollport, so no content scrolls over it. The
|
|
* rule is full-bleed, the width of the dialog. Only the head and actions of a dialog that has a
|
|
* body carry the hooks (resources/views/components/modal.blade.php).
|
|
*
|
|
* The child combinators tie a mark to its own dialog's rows, never to those of a dialog opened
|
|
* from inside its body. The rule fades on the effects-fast token, which reduced motion sets to
|
|
* zero (resources/css/tokens/motion.css), so there it simply appears.
|
|
*/
|
|
|
|
@layer components {
|
|
[data-dialog-head],
|
|
[data-dialog-actions] {
|
|
position: relative;
|
|
}
|
|
|
|
[data-dialog-head]::after,
|
|
[data-dialog-actions]::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset-inline: 0;
|
|
height: 1px;
|
|
background-color: var(--md-sys-color-outline-variant);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-dialog-head]::after {
|
|
bottom: 0;
|
|
}
|
|
|
|
[data-dialog-actions]::before {
|
|
top: 0;
|
|
}
|
|
|
|
dialog[data-overflow-top] > * > [data-dialog-head]::after,
|
|
dialog[data-overflow-bottom] > * > [data-dialog-actions]::before,
|
|
[data-dialog-head][data-separator]::after,
|
|
[data-dialog-actions][data-separator]::before {
|
|
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;
|
|
}
|
|
}
|