Divide a dialog's scrolling body from its pinned header and actions
Plan step 23 (containment), containment.md § Missing: "no divider pinned between a scrolling body and the header/actions". Every dialog now draws a 1px outline-variant rule under the header once the body is scrolled away from its top, and over the actions while more of it is below; a body that fits shows neither. The rules are pseudo-elements in the rows' own padding (dialog.css), so showing one moves nothing, and x-dialog-dividers (dialog.js) marks the wire:ignore.self <dialog>, which a morph leaves alone, watching scroll and a ResizeObserver on the body and a wrapper around the slot. The M3 gaps are split around the rules (8/8 under the header, 8/16 over the actions). A full-screen dialog's rule sits under its phone bar, and its action bar's always-on border follows the scroll too. `separator` now means "draw both rules always" instead of rendering two <x-divider> elements. The fade uses the effects-fast token, zero under reduced motion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
12cdeaaf67
commit
cbc0fa76d2
@@ -28,7 +28,21 @@
|
||||
the buttons at the bottom" (docs/reference/m3/components-actions-communication-containment.md
|
||||
§ Dialogs → Behaviour): the header and the action row are their own rows of the flex column
|
||||
and only the body between them scrolls, each with the 24dp padding the box used to carry.
|
||||
`separator` draws M3's divider under the pinned header and above the pinned actions.
|
||||
|
||||
Between that body and the rows pinned around it go M3's optional dividers (the 1dp "Divider"
|
||||
in both dialogs' anatomy, § Dialogs → Anatomy, Specs), each only while content is hidden on
|
||||
its far side: the rule under the header once the body is scrolled away from its top, the rule
|
||||
over the actions while more of the body is below, so a body that fits draws neither. On a
|
||||
phone a full-screen dialog's top rule lies under its close-and-title bar, unless a subtitle
|
||||
or an icon keeps the header there, and its action bar's rule follows the scroll like any
|
||||
other. `separator` draws both rules whatever the scroll. The gaps M3 gives are split around
|
||||
the rules — 8px above and 8px below the top one (16dp title to body), 8px above and 16px below
|
||||
the bottom one (24dp body to actions) — so the scrolling body keeps room inside its edges for
|
||||
a floating label or a focus ring, and a rule that appears moves nothing. Without a body there
|
||||
is nothing to divide, and no rule. resources/css/components/dialog.css draws the rules;
|
||||
`x-dialog-dividers` (resources/js/dialog.js), on the body, says which of them show, watching
|
||||
the body and the one element wrapped around the slot, so content a Livewire render adds
|
||||
counts.
|
||||
|
||||
`alert` is M3's "on web, basic dialogs should have the alert dialog role": it sets
|
||||
`role="alertdialog"` and points `aria-describedby` at the body, for the dialog that
|
||||
@@ -51,9 +65,12 @@
|
||||
$model = $attributes->wire('model')->value() ?: null;
|
||||
$id = $attributes->get('id') ?? 'material-dialog-'.substr(md5($model.'|'.$title), 0, 10);
|
||||
$header = filled($title) || filled($subtitle) || $icon;
|
||||
$body = $slot->isNotEmpty();
|
||||
// On a phone a full-screen dialog's bar names it, and the header hides unless an icon or a subtitle keeps it.
|
||||
$barAboveBody = $fullscreen && (! $header || (! $icon && blank($subtitle)));
|
||||
$describedBy = implode(' ', array_filter([
|
||||
filled($subtitle) ? $id.'-subtitle' : null,
|
||||
$alert && $slot->isNotEmpty() ? $id.'-body' : null,
|
||||
$alert && $body ? $id.'-body' : null,
|
||||
]));
|
||||
@endphp
|
||||
|
||||
@@ -89,7 +106,10 @@
|
||||
$boxClass,
|
||||
])>
|
||||
@if ($fullscreen)
|
||||
<div class="flex h-14 shrink-0 items-center gap-1 px-1 medium:hidden">
|
||||
<div
|
||||
@if ($body && $barAboveBody) data-dialog-head @if ($separator) data-separator @endif @endif
|
||||
class="flex h-14 shrink-0 items-center gap-1 px-1 medium:hidden"
|
||||
>
|
||||
<x-livewire-material::button icon="close" :tooltip="__('Close')" x-on:click="close()" />
|
||||
|
||||
@if (filled($title))
|
||||
@@ -100,7 +120,10 @@
|
||||
|
||||
@if ($header)
|
||||
{{-- A full-screen dialog's bar names it on a phone, so only the subtitle stays there. --}}
|
||||
<div @class(['shrink-0 px-6 pt-6', 'max-medium:hidden' => $fullscreen && ! $icon && blank($subtitle), 'text-center' => $icon])>
|
||||
<div
|
||||
@if ($body) data-dialog-head @if ($separator) data-separator @endif @endif
|
||||
@class(['shrink-0 px-6 pt-6', 'pb-2' => $body, 'max-medium:hidden' => $fullscreen && ! $icon && blank($subtitle), 'text-center' => $icon])
|
||||
>
|
||||
@if ($icon)
|
||||
<x-livewire-material::icon :name="$icon" class="mx-auto mb-4 size-6 text-secondary" />
|
||||
@endif
|
||||
@@ -112,33 +135,32 @@
|
||||
@if (filled($subtitle))
|
||||
<p id="{{ $id }}-subtitle" @class(['type-body-md text-on-surface-variant', 'mt-4' => filled($title) || $icon, 'max-medium:mt-0' => $fullscreen && ! $icon])>{{ $subtitle }}</p>
|
||||
@endif
|
||||
|
||||
@if ($separator)
|
||||
<x-livewire-material::divider class="mt-4" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($slot->isNotEmpty())
|
||||
<div id="{{ $id }}-body" @class([
|
||||
@if ($body)
|
||||
<div id="{{ $id }}-body" x-dialog-dividers @class([
|
||||
'min-h-0 flex-1 overflow-y-auto px-6 type-body-md text-on-surface-variant',
|
||||
'pt-4' => $header,
|
||||
'pt-2' => $header,
|
||||
'max-medium:pt-4' => $header && $barAboveBody,
|
||||
'pt-6' => ! $header,
|
||||
'pb-2' => isset($actions),
|
||||
'pb-6' => ! isset($actions),
|
||||
])>
|
||||
{{ $slot }}
|
||||
<div data-dialog-content>{{ $slot }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@isset($actions)
|
||||
@if ($separator)
|
||||
<x-livewire-material::divider class="shrink-0" />
|
||||
@endif
|
||||
|
||||
<div @class([
|
||||
'flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pt-6 pb-6',
|
||||
'max-medium:min-h-14 max-medium:border-t max-medium:border-outline-variant max-medium:pt-2 max-medium:pb-2' => $fullscreen,
|
||||
])>
|
||||
<div
|
||||
@if ($body) data-dialog-actions @if ($separator) data-separator @endif @endif
|
||||
@class([
|
||||
'flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pb-6',
|
||||
'pt-4' => $body,
|
||||
'pt-6' => ! $body,
|
||||
'max-medium:min-h-14 max-medium:pt-2 max-medium:pb-2' => $fullscreen,
|
||||
])
|
||||
>
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
Reference in New Issue
Block a user