Give a bottom sheet M3's preset heights

Plan step 23, containment.md § Missing ("Bottom sheets: preset heights
and the handle's cycle-through-heights behaviour"). `heights` (or `snap`,
which is 25/50/90dvh) lists the stops a sheet moves between; it then
takes its stop's height and opens at the stop that equals `height`, or at
the first. Fewer than two stops is no stops, since M3 only requires the
non-drag alternative "if multiple preset heights exist".

That alternative is the drag handle, which M3 names a button: activating
it moves to the next stop and announces it in a live region, and from the
last stop it closes the sheet — M3's "selecting the drag handle toggles
preset heights or closes the sheet", and the same thing a handle with no
stops has always done. A drag now runs the sheet's height with the
pointer and settles on the nearest stop, or closes below the smallest one
or on a downward flick.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:42:37 +02:00
co-authored by Claude Fable 5.1
parent 07759cd664
commit 944431668d
5 changed files with 252 additions and 15 deletions
+28 -2
View File
@@ -127,7 +127,7 @@ it('slides a side sheet in from either edge, and is a pane from expanded when as
it('draws a modal bottom sheet with a drag handle, or a standard one without a scrim', function () {
expect((string) $this->blade('<x-bottom-sheet title="Share via">Body</x-bottom-sheet>'))
->toContain('...materialBottomSheet(false)')
->toContain('...materialBottomSheet(false, JSON.parse(')
->toContain('bg-scrim/32')
->toContain('x-trap.inert.noscroll="open"')
->toContain('rounded-t-corner-xl bg-surface-container-low')
@@ -137,7 +137,7 @@ it('draws a modal bottom sheet with a drag handle, or a standard one without a s
->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>'))
->toContain('...materialBottomSheet(true)')
->toContain('...materialBottomSheet(true, JSON.parse(')
->not->toContain('bg-scrim/32')
->not->toContain('aria-modal')
->and((string) $this->blade('<x-bottom-sheet height="90dvh">Body</x-bottom-sheet>'))
@@ -167,3 +167,29 @@ it('is M3\'s standard side sheet from expanded, and the modal one below', functi
->not->toContain('data-standard')
->not->toContain('expanded:hidden');
});
it('gives a bottom sheet M3\'s preset heights, cycled from the drag handle', function () {
$html = (string) $this->blade('<x-bottom-sheet title="Share via" snap>Body</x-bottom-sheet>');
expect($html)
->toContain('--sheet-stop: 50dvh; --sheet-max-height: min(var(--sheet-stop), calc(100dvh - 72px))')
->toContain('h-(--sheet-max-height) transition-[height]')
->toContain('x-ref="probe"')
->toContain('aria-label="Change the sheet height"')
->toContain('x-bind:aria-label="handleLabel"')
->toContain('x-on:click="activate()"')
->toContain('<span class="sr-only" aria-live="polite" x-text="announcement"></span>')
->toContain('25dvh')
->toContain('90dvh')
->toContain('Height 2 of 3')
->and((string) $this->blade('<x-bottom-sheet heights="30dvh,60dvh" height="60dvh">Body</x-bottom-sheet>'))
->toContain('--sheet-stop: 60dvh;')
->toContain('30dvh')
->and((string) $this->blade('<x-bottom-sheet :heights="[25, 50, 90]">Body</x-bottom-sheet>'))
->toContain('--sheet-stop: 50dvh;')
->and((string) $this->blade('<x-bottom-sheet heights="50dvh">Body</x-bottom-sheet>'))
->toContain('--sheet-max-height: min(50dvh, calc(100dvh - 72px))')
->not->toContain('--sheet-stop')
->not->toContain('sr-only')
->toContain('aria-label="Close"');
});