{{-- 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 ``: `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. `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's wrapper is the drag target and M3's 48dp hit area: the button's own visual bar is 32×4px, and `md-touch-target` (foundation/interaction.css) extends it, while the wrapper's own 22px top/bottom padding (`SheetDefaults.kt`'s `DragHandleVerticalPadding`) gives the row its M3-specified height regardless — 4 + 22 + 22 = 48px (C-01). The button renders the shared `md-focus-ring` and `md-touch-target` classes rather than a hand-rolled ring: unlike the datepicker's day, its own visible bar is not a smaller indicator drawn inside a bigger box the classes cannot reach — the box the classes draw is the button's whole hit area. The root renders `data-md-bottom-sheet` (data-md-open, data-md-standard); the parts are `data-md-bottom-sheet-scrim`, `-panel` (data-md-preset while stops exist), `-handle` (the drag target), `-grip` (the 32×4px bar), `-announce`, `-body`, `-title`, `-actions` — drawn by resources/css/components/bottom-sheet.css. `` renders this component for its compact presentation. --}} @props([ 'title' => null, 'standard' => false, 'height' => '50dvh', 'heights' => null, 'snap' => false, ]) @php $model = $attributes->wire('model')->value() ?: null; $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
@unless ($standard) @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. --}} @endif