Rewrite the modal (dialog) without Tailwind
Plan step 36 (containment group): <x-modal>'s class lists move into resources/css/components/modal.css, keyed on data-md-modal (with data-md-fullscreen) and data-md-modal-box/-bar/-head/-title/-subtitle/ -body/-content/-actions. Sizes stay px (560/280/48px matching DialogTokens.kt/FullScreenDialogTokens.kt and the 56px full-screen header, C-17), spacing the measurement tokens, breakpoints a literal 600px range query. The phone-only header bar and the pinned head/actions keep their existing responsive behaviour (a subtitle or an icon keeps the head on screen below 600px; only the bar's own divider-eligibility needs a computed flag, since it alone can sit beside a head that is also on screen) — reproduced with :has() against data-md-icon/ data-md-modal-subtitle wherever that is self-contained, and with an explicit data-md-modal-divider attribute where it is not. The scroll-driven dividers rename data-dialog-head/-actions -> data-md-modal-head/-actions, data-overflow-top/-bottom -> data-md-overflow-top/-bottom (resources/js/dialog.js); the dialog-dividers Alpine directive keeps its name, since it isn't a data-md-* hook. resources/css/components/dialog.css (Tailwind-era) is replaced by modal.css, imported from the Containment block of components.css; its import leaves resources/css/tailwind.css. No other component renders <x-modal> or dialog.css's rules (checked: datepicker, timepicker and search draw their own dialog-shaped surfaces independently). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9ff5a7a809
commit
3c235ecc1e
@@ -20,14 +20,16 @@
|
||||
on-surface-variant, and the `actions` slot at the end. `icon` puts a secondary-coloured icon
|
||||
above a centred title, as M3 draws a dialog with a hero icon. `fullscreen` makes a dialog that
|
||||
holds a form take the whole screen on a compact window (below `medium`, 600px — M3 uses
|
||||
full-screen dialogs "only in compact breakpoints"), with a 56px close-and-title bar clear of
|
||||
the notch. `persistent` ignores Escape and the scrim, for a dialog that must be
|
||||
answered. It opens on the fast spatial spring and closes at once, as M3's do.
|
||||
full-screen dialogs "only in compact breakpoints", FullScreenDialogTokens), with a 56px
|
||||
close-and-title bar clear of the notch (docs/audits/m3-alignment/containment.md, C-17).
|
||||
`persistent` ignores Escape and the scrim, for a dialog that must be answered. It opens on the
|
||||
fast spatial spring and closes at once, as M3's do.
|
||||
|
||||
"Dialog content generally shouldn't scroll; if it must, the title stays pinned at the top and
|
||||
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.
|
||||
§ Dialogs → Behaviour, C-06): 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.
|
||||
|
||||
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
|
||||
@@ -39,16 +41,23 @@
|
||||
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.
|
||||
is nothing to divide, and no rule. resources/css/components/modal.css draws the rules, keyed
|
||||
on `data-md-modal-divider` (only where a body exists to divide, the bar's own flag computed in
|
||||
PHP because it alone can sit beside a header that is *also* on screen) and `data-md-separator`;
|
||||
`x-dialog-dividers` (resources/js/dialog.js), on the body, watches it and the element wrapped
|
||||
around the slot, so content a Livewire render adds counts, and marks the `<dialog>` with
|
||||
`data-md-overflow-top`/`data-md-overflow-bottom`.
|
||||
|
||||
`alert` is M3's "on web, basic dialogs should have the alert dialog role": it sets
|
||||
`alert` is M3's "on web, basic dialogs should have the alert dialog role" (C-20): it sets
|
||||
`role="alertdialog"` and points `aria-describedby` at the body, for the dialog that
|
||||
interrupts to say something important. It is opt-in, because ARIA-APG keeps `alertdialog`
|
||||
for exactly that and a form dialog would over-announce with it. A `subtitle` is always the
|
||||
dialog's description. --}}
|
||||
dialog's description.
|
||||
|
||||
The root renders `data-md-modal` and `data-md-fullscreen`; its parts are `data-md-modal-*`,
|
||||
drawn by resources/css/components/modal.css: `-box`, `-bar` (the phone-only header bar),
|
||||
`-head`, `-title`, `-subtitle`, `-body`, `-content` (the resize-observed wrapper around the
|
||||
slot), `-actions`. `box-class` lands on the box. --}}
|
||||
|
||||
@props([
|
||||
'title' => null,
|
||||
@@ -91,29 +100,22 @@
|
||||
@if ($alert) role="alertdialog" @endif
|
||||
@if (filled($title)) aria-labelledby="{{ $id }}-title" @endif
|
||||
@if (filled($describedBy)) aria-describedby="{{ $describedBy }}" @endif
|
||||
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->merge(['id' => $id]) }}
|
||||
@class([
|
||||
'm-auto max-h-[calc(100dvh-3rem)] w-[calc(100vw-3rem)] max-w-[35rem] min-w-70 overflow-visible bg-transparent p-0 text-on-surface',
|
||||
'backdrop:bg-scrim/32',
|
||||
'opacity-100 scale-100 starting:opacity-0 starting:scale-95 transition-[opacity,scale] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
|
||||
'max-medium:m-0 max-medium:h-dvh max-medium:max-h-none max-medium:w-full max-medium:max-w-none max-medium:min-w-0' => $fullscreen,
|
||||
$attributes->get('class'),
|
||||
])
|
||||
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id'])->merge(array_filter([
|
||||
'id' => $id,
|
||||
'data-md-modal' => true,
|
||||
'data-md-fullscreen' => $fullscreen ? true : null,
|
||||
], fn ($value): bool => $value !== null)) }}
|
||||
>
|
||||
<div @class([
|
||||
'flex max-h-[inherit] flex-col overflow-hidden rounded-corner-xl bg-surface-container-high shadow-elevation-3',
|
||||
'max-medium:h-full max-medium:rounded-corner-none max-medium:pt-[var(--material-safe-top,env(safe-area-inset-top))]' => $fullscreen,
|
||||
$boxClass,
|
||||
])>
|
||||
<div data-md-modal-box @if (filled($boxClass)) class="{{ $boxClass }}" @endif>
|
||||
@if ($fullscreen)
|
||||
<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"
|
||||
data-md-modal-bar
|
||||
@if ($body && $barAboveBody) data-md-modal-divider @if ($separator) data-md-separator @endif @endif
|
||||
>
|
||||
<x-livewire-material::button icon="close" :tooltip="__('Close')" x-on:click="close()" />
|
||||
|
||||
@if (filled($title))
|
||||
<span class="truncate type-title-lg">{{ $title }}</span>
|
||||
<span data-md-modal-bar-title>{{ $title }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@@ -121,45 +123,33 @@
|
||||
@if ($header)
|
||||
{{-- A full-screen dialog's bar names it on a phone, so only the subtitle stays there. --}}
|
||||
<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])
|
||||
data-md-modal-head
|
||||
@if ($body) data-md-modal-divider @if ($separator) data-md-separator @endif @endif
|
||||
>
|
||||
@if ($icon)
|
||||
<x-livewire-material::icon :name="$icon" class="mx-auto mb-4 size-6 text-secondary" />
|
||||
<x-livewire-material::icon :name="$icon" size="24" />
|
||||
@endif
|
||||
|
||||
@if (filled($title))
|
||||
<h2 id="{{ $id }}-title" @class(['type-headline-sm', 'max-medium:hidden' => $fullscreen && ! $icon])>{{ $title }}</h2>
|
||||
<h2 id="{{ $id }}-title" data-md-modal-title>{{ $title }}</h2>
|
||||
@endif
|
||||
|
||||
@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>
|
||||
<p id="{{ $id }}-subtitle" data-md-modal-subtitle>{{ $subtitle }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($body)
|
||||
<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,
|
||||
'pt-6' => ! $header,
|
||||
'pb-2' => isset($actions),
|
||||
'pb-6' => ! isset($actions),
|
||||
])>
|
||||
<div data-dialog-content>{{ $slot }}</div>
|
||||
<div id="{{ $id }}-body" data-md-modal-body x-dialog-dividers>
|
||||
<div data-md-modal-content>{{ $slot }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@isset($actions)
|
||||
<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,
|
||||
])
|
||||
data-md-modal-actions
|
||||
@if ($body) data-md-modal-divider @if ($separator) data-md-separator @endif @endif
|
||||
>
|
||||
{{ $actions }}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user