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:
co-authored by
Claude Fable 5.1
parent
07759cd664
commit
944431668d
@@ -494,7 +494,9 @@ 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` (`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.
|
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), `heights`/`snap`, `actions` slot.
|
||||||
|
|
||||||
|
`heights` gives it M3's **preset heights** — `heights="25dvh,50dvh,90dvh"`, `:heights="[25, 50, 90]"` (a bare number is `dvh`) or a JSON list; `snap` is the shorthand for those three. The sheet then takes its stop's height and opens at the stop that equals `height`, or at the first. The drag handle is the height control M3 requires beside the drag: activating it (press, Enter, Space) moves to the next stop and announces it, and from the last stop it closes the sheet; dragging settles on the nearest stop, or closes below the smallest. Fewer than two stops is no stops — use `height` for a single height.
|
||||||
|
|
||||||
### `<x-carousel>`, `<x-carousel-item>`
|
### `<x-carousel>`, `<x-carousel-item>`
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,114 @@
|
|||||||
/**
|
/**
|
||||||
* `materialBottomSheet(standard)`: the behaviour of `<x-bottom-sheet>`, spread into its x-data
|
* `materialBottomSheet(standard, presets)`: the behaviour of `<x-bottom-sheet>`, spread into its
|
||||||
* alongside `open` (entangled with Livewire, or the surrounding Alpine scope's).
|
* x-data alongside `open` (entangled with Livewire, or the surrounding Alpine scope's).
|
||||||
*
|
*
|
||||||
* A downward drag follows the pointer and, released past a quarter of the sheet's height or
|
* Without preset heights the sheet is as tall as its content allows and a downward drag follows the
|
||||||
* flicked down, closes it; otherwise it springs back. The drag starts on the handle, or anywhere
|
* pointer: released past a quarter of the sheet's height or flicked down, it closes; otherwise it
|
||||||
* on the sheet while its content is scrolled to the top, so scrolling the content still scrolls.
|
* springs back. Activating the handle closes it.
|
||||||
|
*
|
||||||
|
* With preset heights (`presets.stops`, CSS lengths from `heights`/`snap`) the sheet is the height
|
||||||
|
* of its current stop. A drag then runs that height with the pointer and settles on the nearest
|
||||||
|
* stop when it is let go — below the smallest stop, or on a downward flick, it closes instead. M3
|
||||||
|
* requires a single-pointer alternative to any drag, and names the handle as it: activating it
|
||||||
|
* moves to the next stop and announces it, and from the last stop it closes the sheet, which is
|
||||||
|
* M3's "selecting the drag handle toggles preset heights or closes the sheet".
|
||||||
|
*
|
||||||
|
* The drag starts on the handle, or anywhere on the sheet while its content is scrolled to the top,
|
||||||
|
* so scrolling the content still scrolls. The sheet's inline `--sheet-stop` is what the CSS reads
|
||||||
|
* for the current stop; only while a drag is running does this write a pixel height, with the
|
||||||
|
* transition off, so the sheet tracks the pointer rather than easing after it.
|
||||||
*/
|
*/
|
||||||
const DISMISS_FRACTION = 0.25
|
const DISMISS_FRACTION = 0.25
|
||||||
const FLICK_PX_PER_MS = 0.5
|
const FLICK_PX_PER_MS = 0.5
|
||||||
|
|
||||||
window.materialBottomSheet = (standard = false) => ({
|
/** M3's top margin for a bottom sheet, which its stops are held under. */
|
||||||
|
const TOP_MARGIN = 72
|
||||||
|
|
||||||
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
|
||||||
|
|
||||||
|
window.materialBottomSheet = (standard = false, presets = {}) => ({
|
||||||
standard,
|
standard,
|
||||||
|
stops: presets.stops ?? [],
|
||||||
|
labels: presets.labels ?? {},
|
||||||
|
start: presets.start ?? 0,
|
||||||
|
stop: presets.start ?? 0,
|
||||||
dragged: 0,
|
dragged: 0,
|
||||||
|
dragging: false,
|
||||||
|
height: 0,
|
||||||
|
announcement: '',
|
||||||
|
|
||||||
|
/** Whether this sheet has more than one height to move between. */
|
||||||
|
get preset() {
|
||||||
|
return this.stops.length > 1
|
||||||
|
},
|
||||||
|
|
||||||
|
/** What the handle does next: the last stop closes the sheet, as a handle with no stops does. */
|
||||||
|
get handleLabel() {
|
||||||
|
return this.preset && this.stop < this.stops.length - 1 ? this.labels.change : this.labels.close
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Only a running drag writes inline styles; at rest the classes and `--sheet-stop` own the box. */
|
||||||
|
get sheetStyle() {
|
||||||
|
if (!this.dragging) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.preset ? { height: `${this.height}px`, transition: 'none' } : { translate: `0 ${this.dragged}px`, transition: 'none' }
|
||||||
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.dragged = 0
|
this.dragged = 0
|
||||||
|
this.dragging = false
|
||||||
|
this.move(this.start, false)
|
||||||
this.open = typeof this.open === 'boolean' ? false : null
|
this.open = typeof this.open === 'boolean' ? false : null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** The handle's press, Enter and Space: the next stop, or the way out. */
|
||||||
|
activate() {
|
||||||
|
if (this.preset && this.stop < this.stops.length - 1) {
|
||||||
|
this.move(this.stop + 1)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.close()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Settles on a stop: the CSS variable the box is sized from, and a word for the screen reader. */
|
||||||
|
move(index, announce = true) {
|
||||||
|
if (!this.preset) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stop = index
|
||||||
|
this.$refs.sheet?.style.setProperty('--sheet-stop', this.stops[index])
|
||||||
|
|
||||||
|
if (announce) {
|
||||||
|
this.announcement = this.labels.announce?.[index] ?? ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** The tallest a sheet may be: the screen less M3's 72dp top margin. */
|
||||||
|
ceiling() {
|
||||||
|
return Math.max(0, window.innerHeight - TOP_MARGIN)
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Each stop in pixels — only the browser can say what `25dvh` is. */
|
||||||
|
sizes() {
|
||||||
|
const probe = this.$refs.probe
|
||||||
|
const ceiling = this.ceiling()
|
||||||
|
|
||||||
|
return this.stops.map((stop) => {
|
||||||
|
probe.style.height = stop
|
||||||
|
|
||||||
|
const size = probe.getBoundingClientRect().height
|
||||||
|
|
||||||
|
probe.style.removeProperty('height')
|
||||||
|
|
||||||
|
return Math.min(size, ceiling)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
dragStart(event) {
|
dragStart(event) {
|
||||||
const sheet = this.$refs.sheet
|
const sheet = this.$refs.sheet
|
||||||
const onHandle = event.target.closest('[data-drag-handle]')
|
const onHandle = event.target.closest('[data-drag-handle]')
|
||||||
@@ -27,18 +118,28 @@ window.materialBottomSheet = (standard = false) => ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preset = this.preset
|
||||||
const startY = event.clientY
|
const startY = event.clientY
|
||||||
|
const startHeight = sheet.offsetHeight
|
||||||
let lastY = startY
|
let lastY = startY
|
||||||
let lastAt = performance.now()
|
let lastAt = performance.now()
|
||||||
let velocity = 0
|
let velocity = 0
|
||||||
|
|
||||||
|
this.dragging = true
|
||||||
|
this.height = startHeight
|
||||||
|
|
||||||
const move = (moveEvent) => {
|
const move = (moveEvent) => {
|
||||||
const now = performance.now()
|
const now = performance.now()
|
||||||
|
|
||||||
velocity = (moveEvent.clientY - lastY) / Math.max(now - lastAt, 1)
|
velocity = (moveEvent.clientY - lastY) / Math.max(now - lastAt, 1)
|
||||||
lastY = moveEvent.clientY
|
lastY = moveEvent.clientY
|
||||||
lastAt = now
|
lastAt = now
|
||||||
this.dragged = Math.max(0, moveEvent.clientY - startY)
|
|
||||||
|
if (preset) {
|
||||||
|
this.height = clamp(startHeight - (moveEvent.clientY - startY), 0, this.ceiling())
|
||||||
|
} else {
|
||||||
|
this.dragged = Math.max(0, moveEvent.clientY - startY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const end = () => {
|
const end = () => {
|
||||||
@@ -46,7 +147,11 @@ window.materialBottomSheet = (standard = false) => ({
|
|||||||
window.removeEventListener('pointerup', end)
|
window.removeEventListener('pointerup', end)
|
||||||
window.removeEventListener('pointercancel', end)
|
window.removeEventListener('pointercancel', end)
|
||||||
|
|
||||||
if (this.dragged > sheet.offsetHeight * DISMISS_FRACTION || (this.dragged > 0 && velocity > FLICK_PX_PER_MS)) {
|
this.dragging = false
|
||||||
|
|
||||||
|
if (preset) {
|
||||||
|
this.release(velocity)
|
||||||
|
} else if (this.dragged > sheet.offsetHeight * DISMISS_FRACTION || (this.dragged > 0 && velocity > FLICK_PX_PER_MS)) {
|
||||||
this.close()
|
this.close()
|
||||||
} else {
|
} else {
|
||||||
this.dragged = 0
|
this.dragged = 0
|
||||||
@@ -57,4 +162,20 @@ window.materialBottomSheet = (standard = false) => ({
|
|||||||
window.addEventListener('pointerup', end)
|
window.addEventListener('pointerup', end)
|
||||||
window.addEventListener('pointercancel', end)
|
window.addEventListener('pointercancel', end)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** A preset-height drag let go: the nearest stop, or out past the smallest one. */
|
||||||
|
release(velocity) {
|
||||||
|
const sizes = this.sizes()
|
||||||
|
const smallest = Math.min(...sizes)
|
||||||
|
|
||||||
|
if (velocity > FLICK_PX_PER_MS || this.height < smallest * (1 - DISMISS_FRACTION)) {
|
||||||
|
this.close()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nearest = sizes.reduce((best, size, index) => (Math.abs(size - this.height) < Math.abs(sizes[best] - this.height) ? index : best), 0)
|
||||||
|
|
||||||
|
this.move(nearest)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,6 +15,24 @@
|
|||||||
default is `50dvh`, under a ceiling of the screen less M3's 72dp top margin. Content scrolls
|
default is `50dvh`, under a ceiling of the screen less M3's 72dp top margin. Content scrolls
|
||||||
inside.
|
inside.
|
||||||
|
|
||||||
|
`heights` gives the sheet M3's **preset heights**: a list of stops — `heights="25dvh,50dvh,90dvh"`,
|
||||||
|
`:heights="[25, 50, 90]"` (a bare number is read as `dvh`) or a JSON list — and the sheet then
|
||||||
|
takes the height of its current stop rather than sizing itself to its content. `snap` is the
|
||||||
|
shorthand for the library's three, `25dvh`, `50dvh` and `90dvh`. It opens at the stop that equals
|
||||||
|
`height`, or at the first one; fewer than two stops is no stops at all, since a single height is
|
||||||
|
what `height` already says.
|
||||||
|
|
||||||
|
With stops the drag handle is M3's height control, which is the accessibility rule behind them:
|
||||||
|
"the drag handle can be dragged **or selected** to cycle through preset heights", "any drag-only
|
||||||
|
action needs a single-pointer alternative", "Tab focuses the drag handle; Space/Enter toggles
|
||||||
|
between available heights", and "selecting the drag handle toggles preset heights **or closes the
|
||||||
|
sheet**". So activating the handle — a click, Enter or Space, since it is a button — moves to the
|
||||||
|
next stop and announces it in a live region, and from the last stop it closes the sheet, which is
|
||||||
|
also what a handle with no stops does. A drag runs the sheet's height with the pointer and
|
||||||
|
settles on the nearest stop on release, or closes it below the smallest one or on a downward
|
||||||
|
flick (docs/reference/m3/components-actions-communication-containment.md § Bottom sheets →
|
||||||
|
Behaviour, Accessibility).
|
||||||
|
|
||||||
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
|
||||||
`DragHandleVerticalPadding = 22.dp` (docs/reference/m3/components-actions-communication-containment.md § Bottom sheets → Specs). --}}
|
`DragHandleVerticalPadding = 22.dp` (docs/reference/m3/components-actions-communication-containment.md § Bottom sheets → Specs). --}}
|
||||||
@@ -23,16 +41,51 @@
|
|||||||
'title' => null,
|
'title' => null,
|
||||||
'standard' => false,
|
'standard' => false,
|
||||||
'height' => '50dvh',
|
'height' => '50dvh',
|
||||||
|
'heights' => null,
|
||||||
|
'snap' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$model = $attributes->wire('model')->value() ?: null;
|
$model = $attributes->wire('model')->value() ?: null;
|
||||||
$id = $attributes->get('id') ?? 'material-bottom-sheet-'.substr(md5($model.'|'.$title), 0, 10);
|
$id = $attributes->get('id') ?? 'material-bottom-sheet-'.substr(md5($model.'|'.$title), 0, 10);
|
||||||
|
|
||||||
|
// A list, a JSON list or a comma-separated one; a bare number is a percentage of the screen,
|
||||||
|
// which is how M3 talks about a sheet's position ("capped at 50% of screen height").
|
||||||
|
$stops = match (true) {
|
||||||
|
is_array($heights) => $heights,
|
||||||
|
is_string($heights) && str_starts_with(trim($heights), '[') => json_decode($heights, true) ?: [],
|
||||||
|
filled($heights) => explode(',', (string) $heights),
|
||||||
|
(bool) $snap => ['25dvh', '50dvh', '90dvh'],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
|
||||||
|
$stops = array_values(array_filter(array_map(
|
||||||
|
fn ($stop): string => is_numeric($stop) ? ((float) $stop).'dvh' : trim((string) $stop),
|
||||||
|
$stops,
|
||||||
|
), 'filled'));
|
||||||
|
|
||||||
|
// M3 asks for a non-drag way to change height "if multiple preset heights exist"; one stop is
|
||||||
|
// not multiple, and `height` already says where a single-height sheet opens.
|
||||||
|
$stops = count($stops) > 1 ? $stops : [];
|
||||||
|
$start = (int) (array_search($height, $stops, true) ?: 0);
|
||||||
|
|
||||||
|
$presets = \Illuminate\Support\Js::from([
|
||||||
|
'stops' => $stops,
|
||||||
|
'start' => $start,
|
||||||
|
'labels' => [
|
||||||
|
'change' => __('Change the sheet height'),
|
||||||
|
'close' => __('Close'),
|
||||||
|
'announce' => array_map(
|
||||||
|
fn (int $index): string => __('Height :position of :count', ['position' => $index + 1, 'count' => count($stops)]),
|
||||||
|
array_keys($stops),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div
|
<div
|
||||||
x-data="{
|
x-data="{
|
||||||
...materialBottomSheet({{ $standard ? 'true' : 'false' }}),
|
...materialBottomSheet({{ $standard ? 'true' : 'false' }}, {{ $presets }}),
|
||||||
@if ($model !== null) open: @entangle($attributes->wire('model')).live, @endif
|
@if ($model !== null) open: @entangle($attributes->wire('model')).live, @endif
|
||||||
}"
|
}"
|
||||||
x-on:keydown.window.escape="if (open && ! standard) close()"
|
x-on:keydown.window.escape="if (open && ! standard) close()"
|
||||||
@@ -41,6 +94,12 @@
|
|||||||
<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>
|
<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
|
@endunless
|
||||||
|
|
||||||
|
@if ($stops !== [])
|
||||||
|
{{-- A stop is a CSS length, and only the browser can say what `25dvh` is in pixels; this
|
||||||
|
measures one when a drag has to find the nearest. --}}
|
||||||
|
<div x-ref="probe" aria-hidden="true" class="pointer-events-none invisible fixed start-0 top-0 w-0"></div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<section
|
<section
|
||||||
x-cloak
|
x-cloak
|
||||||
x-show="open"
|
x-show="open"
|
||||||
@@ -52,22 +111,39 @@
|
|||||||
x-transition:leave="transition-[translate] duration-(--md-sys-motion-effects-default-duration) ease-emphasized-accelerate"
|
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-start="translate-y-0"
|
||||||
x-transition:leave-end="translate-y-full"
|
x-transition:leave-end="translate-y-full"
|
||||||
x-bind:style="dragged ? { translate: `0 ${dragged}px`, transition: 'none' } : {}"
|
x-bind:style="sheetStyle"
|
||||||
x-on:pointerdown="dragStart($event)"
|
x-on:pointerdown="dragStart($event)"
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
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: min({{ $height }}, calc(100dvh - 72px))"
|
@if ($stops === [])
|
||||||
|
style="--sheet-max-height: min({{ $height }}, calc(100dvh - 72px))"
|
||||||
|
@else
|
||||||
|
style="--sheet-stop: {{ $stops[$start] }}; --sheet-max-height: min(var(--sheet-stop), calc(100dvh - 72px))"
|
||||||
|
@endif
|
||||||
{{ $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',
|
||||||
|
// With stops the sheet is the height of its stop, not of its content, and it moves
|
||||||
|
// between them on the spatial track — anything that changes size does.
|
||||||
|
'h-(--sheet-max-height) transition-[height] duration-(--md-sys-motion-spatial-default-duration) ease-spatial-default' => $stops !== [],
|
||||||
$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 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="{{ $stops === [] ? __('Close') : __('Change the sheet height') }}"
|
||||||
|
@if ($stops !== []) x-bind:aria-label="handleLabel" @endif
|
||||||
|
x-on:click="activate()"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($stops !== [])
|
||||||
|
<span class="sr-only" aria-live="polite" x-text="announcement"></span>
|
||||||
|
@endif
|
||||||
|
|
||||||
<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">
|
||||||
@if (filled($title))
|
@if (filled($title))
|
||||||
<h2 id="{{ $id }}-title" class="mb-4 type-title-lg">{{ $title }}</h2>
|
<h2 id="{{ $id }}-title" class="mb-4 type-title-lg">{{ $title }}</h2>
|
||||||
|
|||||||
@@ -184,6 +184,18 @@
|
|||||||
</x-list>
|
</x-list>
|
||||||
</x-bottom-sheet>
|
</x-bottom-sheet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div x-data="{ open: false }">
|
||||||
|
<x-button label="Bottom sheet with preset heights" variant="tonal" x-on:click="open = true" />
|
||||||
|
<x-bottom-sheet title="Nearby places" snap>
|
||||||
|
<p class="type-body-md text-on-surface-variant">Press the drag handle, or focus it and press Enter, to move to the next height; from the last it closes. Dragging it settles on the nearest.</p>
|
||||||
|
<x-list>
|
||||||
|
<x-list-item title="Hafen" description="240 m · open until 23:00" icon="location_on" />
|
||||||
|
<x-list-item title="Stadtbibliothek" description="600 m · closes at 18:00" icon="location_on" />
|
||||||
|
<x-list-item title="Seepromenade" description="1.1 km · always open" icon="location_on" />
|
||||||
|
</x-list>
|
||||||
|
</x-bottom-sheet>
|
||||||
|
</div>
|
||||||
BLADE,
|
BLADE,
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
|
|||||||
@@ -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 () {
|
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>'))
|
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('bg-scrim/32')
|
||||||
->toContain('x-trap.inert.noscroll="open"')
|
->toContain('x-trap.inert.noscroll="open"')
|
||||||
->toContain('rounded-t-corner-xl bg-surface-container-low')
|
->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('touch-target h-1 w-8 rounded-corner-full bg-on-surface-variant ')
|
||||||
->toContain('--sheet-max-height: min(50dvh, calc(100dvh - 72px))')
|
->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, JSON.parse(')
|
||||||
->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>'))
|
->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('data-standard')
|
||||||
->not->toContain('expanded:hidden');
|
->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"');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user