Files
livewire-material/resources/views/components/drawer.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 1e580c573e Resolve the package's own components through its namespace
Package views now write <x-livewire-material::button>, so a configured
prefix (which Blade spells <x-m::button>) and an application component
of the same name can no longer break or shadow them. The showcase
rewrites its examples to the configured prefix. Adds the README, and
waits for the adaptive rail to hear a resize before the navigation
tests press its menu button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 09:05:24 +02:00

138 lines
6.4 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. With `pane`, from `xl` it is a list-detail pane beside the list instead.
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: surface-container-low, a large corner on its inner edge,
elevation 1; `side` `end` (the default) or `start`; `width` from `sm` (a caller's `w-*` would
race the sheet's own).
As a pane (`pane`, from `xl`) nothing is covered: the page renders the drawer after its list in
an `xl:flex xl:items-start xl:gap-6` row, the drawer sticks under the top of the viewport, the
list stays usable and another row swaps what it shows — no scrim, no trap, no inert page. While
closed it takes no room. `pane-width` sizes the pane (the sheet's width by default). The body is
a size container, so its contents lay out by the room the sheet or pane actually has (`@md:`),
never by the viewport.
maryUI's API, kept: `title`, `subtitle`, `separator`, `with-close-button`, `close-on-escape`,
`without-backdrop-close`, `right` (ignored; use `side`), and an `actions` slot. --}}
@props([
'title' => null,
'subtitle' => null,
'separator' => false,
'side' => 'end',
'right' => true,
'withCloseButton' => false,
'closeOnEscape' => true,
'withoutBackdropClose' => false,
'width' => '25rem',
'pane' => false,
'paneWidth' => null,
])
@php
$model = $attributes->wire('model')->value() ?: null;
$id = $attributes->get('id') ?? 'material-sheet-'.substr(md5($model.'|'.$title), 0, 10);
$start = $side === 'start';
@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 ($pane)
init() {
const query = window.matchMedia('(min-width: 80rem)');
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
data-sheet="{{ $id }}"
@if ($pane)
x-bind:class="! open && 'xl:hidden'"
class="xl:sticky xl:top-[calc(env(safe-area-inset-top)+1.25rem)] xl:shrink-0 xl:self-start"
data-pane
@endif
>
<div
x-cloak
x-show="open"
x-transition.opacity.duration.200ms
@if (! $withoutBackdropClose) x-on:click="close()" @endif
@class(['fixed inset-0 z-40 bg-scrim/32', 'xl:hidden' => $pane])
aria-hidden="true"
></div>
<aside
x-cloak
x-show="open"
x-trap.inert.noscroll="open && ! wide"
x-transition:enter="transition-[translate,opacity] duration-(--md-sys-motion-spatial-default-duration) ease-emphasized-decelerate"
x-transition:enter-start="{{ $pane ? ($start ? 'max-xl:-translate-x-full xl:opacity-0' : 'max-xl:translate-x-full xl:opacity-0') : ($start ? '-translate-x-full' : 'translate-x-full') }}"
x-transition:enter-end="translate-x-0 opacity-100"
x-transition:leave="transition-[translate,opacity] duration-(--md-sys-motion-effects-default-duration) ease-emphasized-accelerate"
x-transition:leave-start="translate-x-0 opacity-100"
x-transition:leave-end="{{ $pane ? ($start ? 'max-xl:-translate-x-full xl:opacity-0' : 'max-xl:translate-x-full xl:opacity-0') : ($start ? '-translate-x-full' : 'translate-x-full') }}"
id="{{ $id }}"
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
style="--sheet-width: {{ $width }}; --pane-width: {{ $paneWidth ?? $width }}"
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->class([
'fixed top-[env(safe-area-inset-top)] bottom-0 z-50 flex w-full flex-col overflow-y-auto bg-surface-container-low p-6 text-on-surface shadow-elevation-1',
'end-0 sm:rounded-s-corner-lg' => ! $start,
'start-0 sm:rounded-e-corner-lg' => $start,
'sm:w-(--sheet-width) sm:max-w-[calc(100vw-4rem)]',
'xl:relative xl:top-0 xl:z-auto xl:max-h-[calc(100dvh-2.5rem-env(safe-area-inset-top))] xl:w-(--pane-width) xl:max-w-none xl:rounded-corner-lg xl:bg-surface-container xl:shadow-none' => $pane,
$attributes->get('class'),
]) }}
>
@if (filled($title) || $withCloseButton)
<div class="mb-4">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
@if (filled($title))
<h2 id="{{ $id }}-title" class="type-title-lg">{{ $title }}</h2>
@endif
@if (filled($subtitle))
<p class="mt-1 type-body-md text-on-surface-variant">{{ $subtitle }}</p>
@endif
</div>
@if ($withCloseButton)
<span class="-me-3 -mt-2 inline-flex shrink-0">
<x-livewire-material::button icon="close" :tooltip-left="__('Close')" x-on:click="close()" />
</span>
@endif
</div>
@if ($separator)
<x-livewire-material::divider class="mt-4" />
@endif
</div>
@endif
<div class="@container flex-1">
{{ $slot }}
</div>
@isset($actions)
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2 pt-6">
{{ $actions }}
</div>
@endisset
</aside>
</div>