From 25091ebf211f4985d0e72d2d1be1438ebb7d5cf4 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 06:31:56 +0200 Subject: [PATCH] Open a submenu from a menu item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step 22, actions.md § Missing (Submenus): M3's Expressive vertical menu specifies submenus and the Left/Right keys that open and close them, and the library had neither. `` puts its slot in a second popover on the item's end, flipping to the start where there is no room. `materialSubmenu` reuses the menu button pattern one level in: the item is its own trigger, so the anchor move and the button lookup are overridden away; Right, Enter and Space open it, Left and Escape close it and return the focus, and a fine pointer resting on the item opens it after a moment. `items()` now stops at the popover it belongs to, so the arrows never walk between a menu and an open submenu. The menu publishes its container as custom properties so a vibrant menu's submenus are vibrant too. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../livewire-material-development/SKILL.md | 13 +- resources/css/components/menu.css | 27 +- resources/js/menu.js | 494 +++++++++++------- .../views/components/menu-item.blade.php | 56 +- resources/views/components/menu.blade.php | 2 + .../views/showcase/sections/menus.blade.php | 27 +- tests/Feature/Components/MenuTest.php | 48 ++ 7 files changed, 462 insertions(+), 205 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 02d87a4f..00c3bc2b 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -261,7 +261,18 @@ M3's plain tooltip, standalone around any trigger: ` ``` -``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`. ``: `label`, `icon`, `icon-class` (classes for the leading icon; a colour there paints it, a selected item's too, but not a disabled one's — `icon-class="text-sport-run"`), `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`, ticked at its end unless it has an `icon-right`), `current` (for a menu of places: marks the page you are on with `aria-current="page"` in secondary-container, never a checked choice), `badge` (`true` for a dot, or a count, at the end of the row), `disabled`, `keep-open`. Choosing an item closes the menu unless `keep-open`; a second press on the menu button closes it too. An open menu stays open while the Livewire component around it renders, a `keep-open` item's own `wire:click` included. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab; a `disabled` item keeps its place in that order, as M3 asks, but cannot be activated. A menu longer than the window scrolls. +``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`. ``: `label`, `icon`, `icon-class` (classes for the leading icon; a colour there paints it, a selected item's too, but not a disabled one's — `icon-class="text-sport-run"`), `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`, ticked at its end unless it has an `icon-right`), `current` (for a menu of places: marks the page you are on with `aria-current="page"` in secondary-container, never a checked choice), `badge` (`true` for a dot, or a count, at the end of the row), `disabled`, `keep-open`, `submenu`. Choosing an item closes the menu unless `keep-open`; a second press on the menu button closes it too. An open menu stays open while the Livewire component around it renders, a `keep-open` item's own `wire:click` included. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab; a `disabled` item keeps its place in that order, as M3 asks, but cannot be activated. A menu longer than the window scrolls. + +`submenu` makes an item a menu of its own — the slot holds the nested ``s instead of a label, and they open beside it, on its end, flipping to its start where the window has no room: + +```blade + + + + +``` + +The item says so with `aria-haspopup="menu"`, `aria-expanded` and a chevron; Right, Enter or Space open it on its first item, Left or Escape close it and come back, and on a fine pointer resting on the item opens it. Choosing anything inside closes the whole menu. Arrows stay inside the list they are in. M3 calls submenus a large-screen pattern — on a phone give the menu `sheet-at-compact`, or keep the list flat. ### `` diff --git a/resources/css/components/menu.css b/resources/css/components/menu.css index e03404d0..5226b775 100644 --- a/resources/css/components/menu.css +++ b/resources/css/components/menu.css @@ -1,7 +1,7 @@ /* - * The exposed dropdown menu — the list a field drops open. + * The menus: the exposed dropdown a field drops open, and what `` cannot say in utilities. * - * Two lists wear it, so a form reads as one family: + * Two lists wear the dropdown, so a form reads as one family: * * - ``'s: the native