Open a submenu from a menu item
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. `<x-menu-item submenu>` 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
d74fa250ee
commit
25091ebf21
@@ -117,3 +117,51 @@ it('adds icon-class to the leading icon, over its own colour but not over disabl
|
||||
->and((string) $this->blade('<x-menu-item label="Next" icon-right="chevron_right" icon-class="text-sport-run" />'))
|
||||
->not->toContain('text-sport-run');
|
||||
});
|
||||
|
||||
it('opens a submenu beside the item that holds it', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-menu-item label="Send to" icon="send" submenu>
|
||||
<x-menu-item label="A person" icon="person" />
|
||||
</x-menu-item>
|
||||
BLADE);
|
||||
|
||||
preg_match('/anchor-name: (--material-submenu-([a-z0-9]+))/', $html, $anchor);
|
||||
|
||||
expect($anchor)->not->toBeEmpty()
|
||||
->and($html)
|
||||
->toContain('x-data="materialSubmenu"')
|
||||
->toContain('x-on:keydown.right.prevent.stop="open(\'first\')"')
|
||||
->toContain('x-on:pointerenter="hover($event)"')
|
||||
->toContain('aria-haspopup="menu"')
|
||||
->toContain('aria-expanded="false"')
|
||||
->toContain("aria-controls=\"material-submenu-{$anchor[2]}\"")
|
||||
->toContain("id=\"material-submenu-{$anchor[2]}\"")
|
||||
->toContain("position-anchor: {$anchor[1]}")
|
||||
->toContain('data-submenu')
|
||||
->toContain('aria-label="Send to"')
|
||||
->toContain('[position-area:inline-end_span-block-end]')
|
||||
->toContain('[position-try-fallbacks:flip-inline]')
|
||||
// Opening a submenu chooses nothing, so the menu around it stays where it was.
|
||||
->toContain('data-keep-open')
|
||||
->toContain('A person');
|
||||
});
|
||||
|
||||
it('marks a submenu item with a chevron instead of a tick', function () {
|
||||
$chevron = trim((string) file_get_contents(__DIR__.'/../../../resources/svg/symbols/outlined-20/chevron_right.svg'));
|
||||
$chevron = substr($chevron, (int) strpos($chevron, '><') + 1);
|
||||
|
||||
expect((string) $this->blade('<x-menu-item label="Export as" submenu><x-menu-item label="ZIP" /></x-menu-item>'))
|
||||
->toContain($chevron)
|
||||
->toContain('rtl:-scale-x-100')
|
||||
->and((string) $this->blade('<x-menu-item label="Export as" icon-right="download" submenu><x-menu-item label="ZIP" /></x-menu-item>'))
|
||||
->not->toContain($chevron);
|
||||
});
|
||||
|
||||
it('tells a vibrant menu apart, so the submenus inside it take the same container', function () {
|
||||
expect((string) $this->blade('<x-menu vibrant><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>'))
|
||||
->toContain('data-menu')
|
||||
->toContain('data-vibrant')
|
||||
->and((string) $this->blade('<x-menu><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>'))
|
||||
->toContain('data-menu')
|
||||
->not->toContain('data-vibrant');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user