{{-- An M3 Expressive menu: a list of actions that opens from a trigger. Props, slots, `filter` and `sheet-at-compact` are documented in SKILL.md; this is the mechanics behind them. The list is a `popover="auto"` in the top layer, placed by CSS anchor positioning at `position`, flipping when there is no room. The anchor name is rendered on the wrapper around the trigger slot, the only element the server can name, and resources/js/menu.js moves it onto the menu button itself: a trigger that is `position: fixed` (`` on a phone) leaves the wrapper behind as an empty box where the page put it, and the menu opened there. The id and the anchor name are new with every render. The popover carries a `wire:key`, which a Livewire morph matches it by before the id, so a render of the component around an open menu patches it in place — still open, focus and listeners kept — instead of swapping in a closed copy; menu.js then writes the menu button's ARIA attributes again. The key goes through an attribute bag: Livewire compiles a `wire:key` written in a template into the key of the loop iteration around it, which would give every child component after the menu the same key. For `sheet-at-compact`: the slot is written once and drawn twice, in the popover and in the sheet, so a Livewire render patches both copies and a chosen item shows chosen in either. The sheet is teleported to the end of ``, so a menu in a sticky app bar or a toolbar still covers the whole window rather than only that bar's own stacking context. Since the items exist twice, an `id` of the caller's or a nested Livewire component among them would exist twice too: keep those out of a `sheet-at-compact` menu. The container is Expressive's standard menu (surface-container-low, 16px corner, elevation 2), or `vibrant` in tertiary-container — StandardMenuTokens and VibrantMenuTokens from androidx Compose Material 3 (Apache-2.0). A menu longer than the window scrolls at 288px. Drawn by resources/css/components/menu.css, which also draws the dropdown a form's own lists wear — ``'s exposed picker and ``'s listbox — so a menu and a form's own dropdowns read as one family (`[data-md-field-menu]`, `[data-md-field-option]`). --}} @props([ 'label' => null, 'position' => 'bottom-start', 'vibrant' => false, 'filter' => false, 'sheetAtCompact' => false, ]) @php $position = in_array($position, ['bottom-start', 'bottom-end', 'top-start', 'top-end'], true) ? $position : 'bottom-start'; $key = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10)); $anchor = "--material-menu-{$key}"; $filtering = $filter !== false && $filter !== null && $filter !== ''; $filterLabel = is_string($filter) && filled($filter) ? $filter : __('Filter'); // A dialog needs a name; the menu's own label is the one it has. $sheet = (bool) $sheetAtCompact; $sheetLabel = filled($label) ? $label : __('Menu'); @endphp
{{ $trigger }}
'material-menu']) }} id="material-menu-{{ $key }}" popover="auto" @unless ($filtering) role="menu" @endunless data-md-menu-popover data-md-popover-exit data-md-position="{{ $position }}" @if ($vibrant) data-md-vibrant @endif @if ($label && ! $filtering) aria-label="{{ $label }}" @endif tabindex="-1" style="position-anchor: {{ $anchor }}" x-on:keydown="navigate($event)" x-on:click="activate($event)" > @if ($filtering) @include('livewire-material::partials.menu-filter', ['idSuffix' => '', 'wireKey' => null, 'delegated' => false]) @else {{ $slot }} @endif
@if ($sheet) {{-- The compact presentation. The host is the element menu.js knows the sheet by, and its listeners run in this menu's scope; the one inside it only lends the bottom sheet the `open` it reads, which in this scope is the name of a method. Everything inside the sheet is in the sheet's scope, where `close` and `activate` are the sheet's, so nothing in there calls the menu by name. A Livewire morph matches an element by its `wire:key`, or else by its id, and swaps in a fresh copy where the two differ — which would take the focus out of an open sheet. The lists carry keys of their own, as the popover does. The bottom sheet cannot carry one: a `wire:key` given to a Blade component becomes the key of the loop around it, for every Livewire component after it. So the sheet is rendered with one fixed id, the same in every render, and menu.js gives each sheet a unique id of its own while keeping the fixed one as its `wire:key`. --}} @endif