Filter a menu from a field at the top of it
Plan step 22, actions.md § Missing (Menus as a filtering surface): M3's menus page describes a menu that embeds a text field and filters its options as you type, and nothing in the library did that. `<x-menu filter>` renders the field, sticky above the list, and hides the rows the query leaves out — client-side over the items already rendered, so nothing is fetched and a `wire:click` stays where it was. The field keeps the focus and the arrow keys move a highlight it names through `aria-activedescendant`, the APG combobox keyboard `<x-choices searchable>` already uses; Enter chooses the highlighted row, and a query that leaves nothing says so. The list becomes a `role="menu"` inside the popover, because a text field is not something a menu may contain. 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
3f651c5c08
commit
88c46001fb
@@ -176,6 +176,37 @@ it('marks a submenu item with a chevron instead of a tick', function () {
|
||||
->not->toContain($chevron);
|
||||
});
|
||||
|
||||
it('embeds a text field that filters the list, as a combobox over the menu', function () {
|
||||
$html = (string) $this->blade('<x-menu label="Assign to" filter="Find a person"><x-slot:trigger><button>x</button></x-slot:trigger><x-menu-item label="Ada" /></x-menu>');
|
||||
|
||||
preg_match('/id="(material-menu-[a-z0-9]+)"/', $html, $id);
|
||||
|
||||
expect($id)->not->toBeEmpty()
|
||||
->and($html)
|
||||
->toContain('data-menu-filter')
|
||||
->toContain('x-ref="filter"')
|
||||
->toContain('role="combobox"')
|
||||
->toContain('aria-autocomplete="list"')
|
||||
->toContain("aria-controls=\"{$id[1]}-list\"")
|
||||
->toContain('aria-label="Find a person"')
|
||||
->toContain('placeholder="Find a person"')
|
||||
->toContain('x-on:input="refine()"')
|
||||
->toContain('x-on:keydown.stop="search($event)"')
|
||||
->toContain('Nothing matches')
|
||||
// A text field is not something a `role="menu"` may hold, so the list moves inside it.
|
||||
->toContain("<div id=\"{$id[1]}-list\" role=\"menu\" aria-label=\"Assign to\"")
|
||||
->toMatch('/<div\s[^>]*popover="auto"(?![^>]*role="menu")/');
|
||||
});
|
||||
|
||||
it('names a bare filter field, and leaves a plain menu alone', function () {
|
||||
expect((string) $this->blade('<x-menu filter><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>'))
|
||||
->toContain('aria-label="Filter"')
|
||||
->and((string) $this->blade('<x-menu label="Share"><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>'))
|
||||
->not->toContain('data-menu-filter')
|
||||
->not->toContain('x-ref="filter"')
|
||||
->toMatch('/<div\s[^>]*popover="auto"[^>]*role="menu"/');
|
||||
});
|
||||
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user