Files
livewire-material/resources/views/components/drawer.blade.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 9dd527f303 Rewrite the drawer (side sheet) without Tailwind, remove pane
Plan step 36 (containment group): <x-drawer>'s class lists move into
resources/css/components/drawer.css, keyed on data-md-drawer (with
data-md-open, data-md-standard) and data-md-drawer-scrim/-sheet
(data-md-side)/-head/-head-row/-heading/-title/-subtitle/-close/-body/
-actions. The modal sheet slides in on the emphasized-decelerate
easing and out on emphasized-accelerate (an asymmetric CSS transition
expressed by giving the open-state selector its own
transition-duration/-timing-function, which the "after-change style"
rule picks up); the sheet stays x-show-driven, with @starting-style +
transition-behavior: allow-discrete taking the place of x-transition's
enter/leave classes, matching resources/css/components/search.css's
already-rewritten scrim/view. A standard sheet drops the scrim and
trap from 840px and only fades in place (an in-flow box does not
travel); data-md-drawer-collapsed (from the view's `wide` Alpine
state) takes it out of the layout while closed there.

<x-drawer pane> is removed: step 35's <x-list-detail> is M3's form of
that job (the plan's decision, docs/plans/material-3-alignment.md
"Decisions"). Removed the prop, its CSS classes and its tests
(OverlayTest's two pane-only tests deleted, the others' pane cases
dropped); pointed each mention at <x-list-detail> in the material-3
guideline, the material-3-design and livewire-material-development
skills, and added a line to UPGRADE.md's 2.0.0 notes. No other living
file named `<x-drawer pane>` (grepped views, tests, README, boost,
workbench excluding build output); docs/plans and docs/audits keep
their history unchanged.

Imported from the Containment block of components.css, importing
button.css and divider.css for what the view renders.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 21:07:04 +02:00

149 lines
7.2 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 (NavigationDrawerTokens.kt-adjacent side-sheet sources;
docs/reference/m3/components-actions-communication-containment.md § Side sheets): surface-
container-low, a large corner on its inner edge, elevation 1; `side` `end` (the default) or
`start`; `width` from `medium` (a caller's `class` would race the sheet's own — 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' => '25rem',
'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}, 25rem)" : $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 >= 52.5rem)');
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
>
<div data-md-drawer-scrim x-cloak x-show="open" @if (! $withoutBackdropClose) x-on:click="close()" @endif aria-hidden="true"></div>
<aside
x-cloak
x-show="open"
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>