Plan step 19, containment.md C-19 and C-09. A modal sheet opened at 90% of the viewport where M3 caps the initial position at 50% and lets the person pull it up; `height` now defaults to `50dvh` and whatever is passed is held under a ceiling of the screen less M3's 72dp top margin. The drag handle is `on-surface-variant` undiluted, as SheetBottomTokens' DockedDragHandleColor and the component's own header comment already said. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
84 lines
4.4 KiB
PHP
84 lines
4.4 KiB
PHP
{{-- An M3 bottom sheet: secondary content or actions anchored to the bottom of the screen.
|
||
|
||
Modal by default: over a scrim, the page inert and still, dismissed by the scrim, Escape, or a
|
||
downward drag on its handle (or anywhere on the sheet when its content is scrolled to the top).
|
||
`standard` makes it part of the page instead — no scrim, nothing inert, and it stays until closed.
|
||
The open state works as for `<x-drawer>`: `wire:model` (a flag or an id, written back `false` or
|
||
`null` on close) or `open` in the surrounding Alpine scope; `close()` is in scope inside it.
|
||
|
||
SheetBottomTokens (androidx Compose Material 3, Apache-2.0): surface-container-low, extra-large
|
||
top corners, elevation 1, a 32×4px drag handle in on-surface-variant; 640px wide at most, centred
|
||
on a wide screen; it rises on emphasized decelerate. `title` and `actions` as on a dialog.
|
||
|
||
`height` is where it opens, and M3 caps a modal sheet's initial position at half the screen —
|
||
"if content exceeds that, it can be pulled to full screen and scrolled internally" — so the
|
||
default is `50dvh`, under a ceiling of the screen less M3's 72dp top margin. Content scrolls
|
||
inside.
|
||
|
||
The handle is drawn 32×4px and pressed 48×48: `touch-target` on the button and 22px above and
|
||
below it, which is M3's "drag handle has an accessible 48dp hit target" and SheetDefaults.kt's
|
||
`DragHandleVerticalPadding = 22.dp` (docs/reference/m3/components-actions-communication-containment.md § Bottom sheets → Specs). --}}
|
||
|
||
@props([
|
||
'title' => null,
|
||
'standard' => false,
|
||
'height' => '50dvh',
|
||
])
|
||
|
||
@php
|
||
$model = $attributes->wire('model')->value() ?: null;
|
||
$id = $attributes->get('id') ?? 'material-bottom-sheet-'.substr(md5($model.'|'.$title), 0, 10);
|
||
@endphp
|
||
|
||
<div
|
||
x-data="{
|
||
...materialBottomSheet({{ $standard ? 'true' : 'false' }}),
|
||
@if ($model !== null) open: @entangle($attributes->wire('model')).live, @endif
|
||
}"
|
||
x-on:keydown.window.escape="if (open && ! standard) close()"
|
||
>
|
||
@unless ($standard)
|
||
<div x-cloak x-show="open" x-transition.opacity.duration.200ms x-on:click="close()" class="fixed inset-0 z-40 bg-scrim/32" aria-hidden="true"></div>
|
||
@endunless
|
||
|
||
<section
|
||
x-cloak
|
||
x-show="open"
|
||
x-ref="sheet"
|
||
@unless ($standard) x-trap.inert.noscroll="open" @endunless
|
||
x-transition:enter="transition-[translate] duration-(--md-sys-motion-spatial-default-duration) ease-emphasized-decelerate"
|
||
x-transition:enter-start="translate-y-full"
|
||
x-transition:enter-end="translate-y-0"
|
||
x-transition:leave="transition-[translate] duration-(--md-sys-motion-effects-default-duration) ease-emphasized-accelerate"
|
||
x-transition:leave-start="translate-y-0"
|
||
x-transition:leave-end="translate-y-full"
|
||
x-bind:style="dragged ? { translate: `0 ${dragged}px`, transition: 'none' } : {}"
|
||
x-on:pointerdown="dragStart($event)"
|
||
id="{{ $id }}"
|
||
role="dialog"
|
||
@unless ($standard) aria-modal="true" @endunless
|
||
@if (filled($title)) aria-labelledby="{{ $id }}-title" @endif
|
||
style="--sheet-max-height: min({{ $height }}, calc(100dvh - 72px))"
|
||
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->class([
|
||
'fixed inset-x-0 bottom-0 z-50 mx-auto flex max-h-(--sheet-max-height) w-full max-w-160 touch-pan-y flex-col rounded-t-corner-xl bg-surface-container-low pb-[var(--material-safe-bottom,env(safe-area-inset-bottom))] text-on-surface shadow-elevation-1',
|
||
$attributes->get('class'),
|
||
]) }}
|
||
>
|
||
<div class="flex shrink-0 cursor-grab justify-center py-5.5 active:cursor-grabbing" data-drag-handle>
|
||
<button type="button" class="touch-target h-1 w-8 rounded-corner-full bg-on-surface-variant outline-offset-4 focus-visible:outline-3 focus-visible:outline-secondary" aria-label="{{ __('Close') }}" x-on:click="close()"></button>
|
||
</div>
|
||
|
||
<div x-ref="body" class="min-h-0 flex-1 overflow-y-auto px-6 pb-6">
|
||
@if (filled($title))
|
||
<h2 id="{{ $id }}-title" class="mb-4 type-title-lg">{{ $title }}</h2>
|
||
@endif
|
||
|
||
{{ $slot }}
|
||
</div>
|
||
|
||
@isset($actions)
|
||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pb-6">{{ $actions }}</div>
|
||
@endisset
|
||
</section>
|
||
</div>
|