The side sheet, the bottom sheet's panel and the docked search's scrim and view kept `display` alive through their exit with `transition-behavior: allow-discrete`. Firefox does not transition `display` (Chrome 117 and Safari 18 do), and `x-show` sets `display: none` in the frame the exit starts, so in Firefox the sheets vanished instead of sliding out and the search view and scrim vanished instead of fading. The docked search had a second problem in every engine: neither the view nor the scrim had a closed state to transition to, so where `display` was held (Chrome, Safari) the view stood at full opacity for its duration and then disappeared. Each element now carries `x-transition:enter`/`:leave="md-transition"`, the approach the two sheet scrims already took (renamed from `md-scrim-transition` to one name for all of them). The class only switches Alpine to CSS-transition mode, so `x-show` holds `display` for the element's computed transition-duration before hiding it, in every engine, and a reopen during the exit cancels the pending hide; nothing styles it. `display` and `allow-discrete` leave the transitions so Chrome and Safari do not hold a second time. Alpine reads the first `transition-duration` listed, which is the closing slide or fade in each list (the preset panel lists translate before height). The search view now closes back into the bar (opacity 0, `scale: 1 0.9`, the reverse of its `@starting-style` entry) and its scrim fades out on close and when the search turns full screen. Under reduced motion the durations are zero and every one of them closes at once. Four browser tests sample each exit mid-way in the page, in the same round trip as the close: the sheets part of the way to their closed offset, the search scrim and view part of the way faded, each still displayed, then `display: none`. All four fail on main in Firefox (the two search tests in Chrome too) and pass in Chrome, Firefox and Safari. OverlayTest pins the drawer's new transition and the view's markup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
156 lines
7.9 KiB
PHP
156 lines
7.9 KiB
PHP
{{-- An M3 side sheet: detail or controls that slide in from the edge over a scrim; on a phone it
|
|
is the whole screen.
|
|
|
|
The open state is the Livewire property in `wire:model` (entangled live, so a detail held in
|
|
the URL follows a close) — a flag or an id — and closing writes back `false` or `null`.
|
|
Without `wire:model` it reads and writes `open` in the Alpine scope around it. `close()` is in
|
|
scope for anything inside the sheet, so it can draw its own close button.
|
|
|
|
As a sheet it is modal: the page inert and still (`x-trap.inert.noscroll`), and it enters on
|
|
emphasized decelerate rather than a spring — a sheet anchored to the edge that overshot would
|
|
open a gap. M3's modal side sheet (docs/reference/m3/components-actions-communication-
|
|
containment.md § Side sheets; Compose has no side-sheet token file): surface-container-low, a
|
|
large corner on its inner edge, elevation 1; `side` `end` (the default) or `start`, mirrored
|
|
in a right-to-left document; `width` from `medium`, 400px (M3's 400dp cap) unless it says
|
|
otherwise — a compact window gets the full-bleed sheet.
|
|
|
|
As a **standard** side sheet (`standard`, from `expanded`) it is M3's other variant: co-planar
|
|
with the content rather than over it — no scrim, no focus trap, nothing inert, 0dp elevation
|
|
(material-components-android's `SideSheet.md`: "standard side sheet elevation = 0dp,
|
|
coplanar"), `surface` rather than surface-container-low, no corner, and an outline-variant
|
|
divider down its inner edge in place of the scrim. It sits in the page flow beside the
|
|
content, spans the window's height and scrolls on its own; below `expanded` (840px) it is the
|
|
modal sheet — M3 calls the standard sheet "supplementary surfaces mainly for medium to
|
|
expanded breakpoints" and the modal one "preferred at compact breakpoints", and the switch
|
|
sits at `expanded` rather than `medium` because M3 also caps a side sheet at 400dp and a
|
|
600px window has too little left beside one.
|
|
|
|
For the second pane of a list-detail layout use `<x-list-detail>` instead (step 35's canonical
|
|
layout); the two are not the same thing — a pane shows what the list beside it selected, a
|
|
standard sheet is supplementary content (filters, details, a list of actions) beside the
|
|
primary content, divided from it by a rule rather than being part of its selection.
|
|
|
|
M3 **requires** a close affordance on a side sheet — without one nobody can predict the
|
|
sheet's open/close flow or tell whether it is transient or permanent
|
|
(docs/reference/m3/components-actions-communication-containment.md § Side sheets →
|
|
Accessibility, C-04) — so `with-close-button` is on by default, and
|
|
`:with-close-button="false"` is ignored where nothing else closes the sheet: Escape off, the
|
|
scrim off, or `standard`, which has neither from `expanded`.
|
|
|
|
maryUI's API, kept: `title`, `subtitle`, `separator`, `with-close-button`, `close-on-escape`,
|
|
`without-backdrop-close`, `right` (ignored; use `side`), and an `actions` slot.
|
|
|
|
The root renders `data-md-drawer`, `data-md-open` and `data-md-standard`; the sheet is
|
|
`data-md-drawer-sheet` with `data-md-side`, its parts `data-md-drawer-head`/-head-row/
|
|
-heading/-title/-subtitle/-close/-body/-actions — drawn by resources/css/components/
|
|
drawer.css, which imports button.css and divider.css for what the view renders. --}}
|
|
|
|
@props([
|
|
'title' => null,
|
|
'subtitle' => null,
|
|
'separator' => false,
|
|
'side' => 'end',
|
|
'right' => true,
|
|
'withCloseButton' => true,
|
|
'closeOnEscape' => true,
|
|
'withoutBackdropClose' => false,
|
|
'width' => '400px',
|
|
'standard' => false,
|
|
])
|
|
|
|
@php
|
|
$model = $attributes->wire('model')->value() ?: null;
|
|
$id = $attributes->get('id') ?? 'material-sheet-'.substr(md5($model.'|'.$title), 0, 10);
|
|
$side = $side === 'start' ? 'start' : 'end';
|
|
$standard = (bool) $standard;
|
|
|
|
// The side-sheet specs table caps the sheet at 400dp, which is where `width` already starts;
|
|
// a wider one is the modal sheet's to take, not the co-planar standard sheet's.
|
|
$sheetWidth = $standard ? "min({$width}, 400px)" : $width;
|
|
|
|
// M3 requires a close affordance; the prop can only ever add one, never take away the last
|
|
// way out of the sheet. A standard sheet keeps no scrim and no trap from `expanded`, and
|
|
// Escape leaves it open there, so it always draws one.
|
|
$closeButton = $withCloseButton || ! $closeOnEscape || $withoutBackdropClose || $standard;
|
|
@endphp
|
|
|
|
<div
|
|
x-data="{
|
|
@if ($model !== null) open: @entangle($attributes->wire('model')).live, @endif
|
|
wide: false,
|
|
close() { this.open = typeof this.open === 'boolean' ? false : null; },
|
|
@if ($standard)
|
|
init() {
|
|
const query = window.matchMedia('(width >= 840px)');
|
|
this.wide = query.matches;
|
|
query.addEventListener('change', (event) => this.wide = event.matches);
|
|
},
|
|
@endif
|
|
}"
|
|
@if ($closeOnEscape) x-on:keydown.window.escape="if (open && ! wide) close()" @endif
|
|
x-bind:data-md-open="open ? '' : null"
|
|
x-bind:data-md-drawer-collapsed="(! open && wide) ? '' : null"
|
|
data-md-drawer
|
|
@if ($standard) data-md-standard @endif
|
|
>
|
|
{{-- `md-transition` is a class name only to turn on Alpine's CSS transition: `x-show` then keeps
|
|
the scrim and the sheet displayed for their computed transition-duration (drawer.css's
|
|
tokens) before hiding them, so they fade and slide out rather than vanish — Firefox does not
|
|
transition `display`, even with `allow-discrete`. Both stages, so reopening during the exit
|
|
cancels the pending hide. Nothing styles the class. --}}
|
|
<div data-md-drawer-scrim x-cloak x-show="open" x-transition:enter="md-transition" x-transition:leave="md-transition" @if (! $withoutBackdropClose) x-on:click="close()" @endif aria-hidden="true"></div>
|
|
|
|
<aside
|
|
x-cloak
|
|
x-show="open"
|
|
x-transition:enter="md-transition"
|
|
x-transition:leave="md-transition"
|
|
x-trap.inert.noscroll="open && ! wide"
|
|
x-bind:role="wide ? 'region' : 'dialog'"
|
|
x-bind:aria-modal="wide ? null : 'true'"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
@if (filled($title)) aria-labelledby="{{ $id }}-title" @endif
|
|
id="{{ $id }}"
|
|
data-md-drawer-sheet
|
|
data-md-side="{{ $side }}"
|
|
style="--sheet-width: {{ $sheetWidth }}"
|
|
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id']) }}
|
|
>
|
|
@if (filled($title) || $closeButton)
|
|
<div data-md-drawer-head>
|
|
<div data-md-drawer-head-row>
|
|
<div data-md-drawer-heading>
|
|
@if (filled($title))
|
|
<h2 id="{{ $id }}-title" data-md-drawer-title>{{ $title }}</h2>
|
|
@endif
|
|
|
|
@if (filled($subtitle))
|
|
<p data-md-drawer-subtitle>{{ $subtitle }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($closeButton)
|
|
<span data-md-drawer-close>
|
|
<x-livewire-material::button icon="close" :tooltip-left="__('Close')" x-on:click="close()" />
|
|
</span>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($separator)
|
|
<x-livewire-material::divider />
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<div data-md-drawer-body>{{ $slot }}</div>
|
|
|
|
@isset($actions)
|
|
{{-- The side-sheet specs table: bottom actions 72dp tall, 16dp above them, 24dp below
|
|
(the sheet's own padding), aligned **left** — a dialog's are trailing-aligned, and
|
|
M3 specifies the two differently (C-10). --}}
|
|
<div data-md-drawer-actions>{{ $actions }}</div>
|
|
@endisset
|
|
</aside>
|
|
</div>
|