Open a menu in a bottom sheet on a compact window

Plan step 22, item 8 (actions.md § Missing, "Adaptive menu → bottom sheet
at compact"): M3 says "at compact breakpoints, consider swapping a menu for
a bottom sheet", and nothing connected <x-menu> to <x-bottom-sheet>.

<x-menu sheet-at-compact> draws its slot twice, in the popover and in a
modal <x-bottom-sheet> teleported to <body>, and below `medium`
(upTo('medium')) the trigger opens the sheet. The trigger says
aria-haspopup="dialog" there and "menu" from medium, aria-expanded in
both. Items keep their roles and the APG keyboard in the sheet; choosing
one, Escape or Tab close it and return focus to the trigger; a submenu
opens in place under its item; `filter` works in both; a resize across
600px closes whichever is open. The sheet has a fixed id that menu.js
makes unique and keeps as its wire:key, and the lists are keyed, so a
Livewire render patches an open sheet instead of swapping it.

bottom-sheet.blade.php is used as is. The filter now finds its field and
empty row per list instead of through x-refs.

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 11:30:55 +02:00
co-authored by Claude Opus 5
parent 12cdeaaf67
commit deb1442dfb
7 changed files with 648 additions and 51 deletions
@@ -21,6 +21,14 @@
pointer resting on the item opens it. Choosing anything inside closes the whole menu, as it
would from the outer list.
In the bottom sheet of an `<x-menu sheet-at-compact>` on a compact window the same markup opens
in place instead: the list stands under the item, inset, and the chevron turns to point down
at it (menu.css) — a sheet has no room beside an item, and M3 calls submenus "best suited to
large screens" (docs/reference/m3/components-actions-communication-containment.md § Menus →
Behaviour). It is never shown as a popover there, so menu.js only flips `aria-expanded`, and
the keyboard stays the one above: Right, Enter or Space open it, Left or Escape close it and
come back to the item, and Escape closes nothing more.
`icon-class` is for an icon whose colour means something of its own, a sport's glyph in the
sport's colour (`icon-class="text-sport-run"`). A colour there paints the icon, a selected
item's too: the icon's own colour then carries no specificity, because which of two colour
@@ -141,7 +149,7 @@
<x-livewire-material::icon :name="$iconRight" optical="20" :class="'size-5 '.$iconInk" />
@elseif ($submenu)
{{-- M3's submenu marker: it points the way the list opens, and turns over in an RTL page. --}}
<x-livewire-material::icon name="chevron_right" optical="20" :class="'size-5 rtl:-scale-x-100 '.$iconInk" />
<x-livewire-material::icon name="chevron_right" optical="20" data-submenu-chevron :class="'size-5 rtl:-scale-x-100 '.$iconInk" />
@elseif ($selected === true)
{{-- The third cue M3 recommends, so a chosen item is not told by colour and shape alone. --}}
<x-livewire-material::icon name="check" optical="20" :class="'size-5 '.$iconInk" />