Open a bottom sheet at half the screen, and colour its handle

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
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:07:48 +02:00
co-authored by Claude Opus 5
parent 675cfc806a
commit 256d4f0603
3 changed files with 14 additions and 7 deletions
@@ -485,7 +485,7 @@ An M3 side sheet, bound like `<x-modal>`; `close()` in scope. Props: `title`, `s
### `<x-bottom-sheet>` ### `<x-bottom-sheet>`
An M3 bottom sheet, bound like `<x-modal>`: modal by default (scrim, inert page, drag the handle down or press Escape to close), `standard` for one that is part of the page. Props: `title`, `height` (`90dvh`), `actions` slot. An M3 bottom sheet, bound like `<x-modal>`: modal by default (scrim, inert page, drag the handle down or press Escape to close), `standard` for one that is part of the page. Props: `title`, `height` (`50dvh` — M3 caps a modal sheet's initial position at half the screen; whatever you pass is held under a ceiling of the screen less M3's 72dp top margin), `actions` slot.
### `<x-carousel>`, `<x-carousel-item>` ### `<x-carousel>`, `<x-carousel-item>`
@@ -9,7 +9,11 @@
SheetBottomTokens (androidx Compose Material 3, Apache-2.0): surface-container-low, extra-large 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 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. on a wide screen; it rises on emphasized decelerate. `title` and `actions` as on a dialog.
`height` caps it (default: 90% of the screen); content scrolls inside.
`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 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 below it, which is M3's "drag handle has an accessible 48dp hit target" and SheetDefaults.kt's
@@ -18,7 +22,7 @@
@props([ @props([
'title' => null, 'title' => null,
'standard' => false, 'standard' => false,
'height' => '90dvh', 'height' => '50dvh',
]) ])
@php @php
@@ -54,14 +58,14 @@
role="dialog" role="dialog"
@unless ($standard) aria-modal="true" @endunless @unless ($standard) aria-modal="true" @endunless
@if (filled($title)) aria-labelledby="{{ $id }}-title" @endif @if (filled($title)) aria-labelledby="{{ $id }}-title" @endif
style="--sheet-max-height: {{ $height }}" style="--sheet-max-height: min({{ $height }}, calc(100dvh - 72px))"
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->class([ {{ $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', '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'), $attributes->get('class'),
]) }} ]) }}
> >
<div class="flex shrink-0 cursor-grab justify-center py-5.5 active:cursor-grabbing" data-drag-handle> <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/40 outline-offset-4 focus-visible:outline-3 focus-visible:outline-secondary" aria-label="{{ __('Close') }}" x-on:click="close()"></button> <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>
<div x-ref="body" class="min-h-0 flex-1 overflow-y-auto px-6 pb-6"> <div x-ref="body" class="min-h-0 flex-1 overflow-y-auto px-6 pb-6">
+5 -2
View File
@@ -129,9 +129,12 @@ it('draws a modal bottom sheet with a drag handle, or a standard one without a s
->toContain('data-drag-handle') ->toContain('data-drag-handle')
->toContain('aria-modal="true"') ->toContain('aria-modal="true"')
->toContain('py-5.5') ->toContain('py-5.5')
->toContain('touch-target h-1 w-8') ->toContain('touch-target h-1 w-8 rounded-corner-full bg-on-surface-variant ')
->toContain('--sheet-max-height: min(50dvh, calc(100dvh - 72px))')
->and((string) $this->blade('<x-bottom-sheet standard>Body</x-bottom-sheet>')) ->and((string) $this->blade('<x-bottom-sheet standard>Body</x-bottom-sheet>'))
->toContain('...materialBottomSheet(true)') ->toContain('...materialBottomSheet(true)')
->not->toContain('bg-scrim/32') ->not->toContain('bg-scrim/32')
->not->toContain('aria-modal'); ->not->toContain('aria-modal')
->and((string) $this->blade('<x-bottom-sheet height="90dvh">Body</x-bottom-sheet>'))
->toContain('--sheet-max-height: min(90dvh, calc(100dvh - 72px))');
}); });