Give search M3's icon entry point and its suggestions

M3 § Search names three entry points and only the bar existed; suggestions
before the first keystroke were missing too (plan step 24, audit
docs/audits/m3-alignment/inputs.md § Missing). `trigger="icon"` is the search
icon button — search as a secondary action, one 48px button that expands into
the full-screen view at any width, since an icon button has nothing to dock
under, and takes its focus back on close. The `suggestions` slot stands where
the results do until something is typed; the live region counts whichever of
the two is on screen and names suggestions as suggestions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:40:48 +02:00
co-authored by Claude Fable 5.1
parent 2662040c24
commit 549d96b0dc
6 changed files with 230 additions and 27 deletions
+40 -5
View File
@@ -13,6 +13,15 @@
a `trailing` slot for an avatar or icon buttons in the bar. Every other attribute reaches the
`<input type="search">`.
`trigger="icon"` is M3's second entry point, the search icon button: search as a secondary
action in a toolbar or an app bar, a single 48px button that expands into the full-screen view
and hands focus to the field (so `docked` has nothing to say about it). The bar itself is the
view's header there, and closing gives the button its focus back.
`suggestions` is a slot shown in the view until the first keystroke M3's suggestions
behaviour: recent searches, popular queries. The results slot takes over once something is
typed, and whichever of the two is on screen is what the live region counts.
The bar is a combobox: the wrapper around the input carries `role="combobox"` with
`aria-expanded` and `aria-controls`, because ARIA gives a bare textbox neither. The results are
a list, and a polite live region says how many of them there are whenever they change — M3 asks
@@ -23,22 +32,26 @@
'label' => null,
'icon' => 'search',
'docked' => false,
'trigger' => 'bar',
])
@php
$model = $attributes->wire('model')->value() ?: null;
$placeholder ??= __('Search');
$id = $attributes->get('id') ?? 'material-search-'.substr(md5($model.'|'.$placeholder), 0, 10);
$trigger = $trigger === 'icon' ? 'icon' : 'bar';
$announce = [
'none' => __('No results'),
'one' => __('1 result'),
'many' => __(':count results'),
'suggestionOne' => __('1 suggestion'),
'suggestionMany' => __(':count suggestions'),
];
@endphp
<div
x-data="materialSearch({{ $docked ? 'true' : 'false' }}, @js($announce))"
x-data="materialSearch({{ $docked ? 'true' : 'false' }}, @js($announce), @js($trigger))"
x-on:keydown.escape="if (open) { $event.stopPropagation(); close(true); }"
x-on:focusout="leave($event)"
x-on:pointerdown.outside="close()"
@@ -46,16 +59,34 @@
x-bind:data-open="open ? '' : null"
x-bind:data-full-screen="fullScreen ? '' : null"
data-search
data-trigger="{{ $trigger }}"
{{ $attributes->only(['class', 'wire:key'])->class(['relative']) }}
>
<div data-search-scrim x-cloak x-show="open && ! fullScreen" x-on:pointerdown="close()" aria-hidden="true"></div>
@if ($trigger === 'icon')
<button
type="button"
x-ref="trigger"
x-show="! open"
x-on:click="expand()"
aria-label="{{ $label ?? $placeholder }}"
aria-haspopup="dialog"
aria-controls="{{ $id }}-view"
aria-expanded="false"
x-bind:aria-expanded="open.toString()"
data-search-trigger
>
<x-livewire-material::icon :name="$icon" />
</button>
@endif
<div data-search-bar role="search" x-on:click="if ($event.target === $el) $refs.input.focus()">
<span data-search-leading x-show="! fullScreen">
<x-livewire-material::icon :name="$icon" />
</span>
<button type="button" data-search-leading data-search-back x-show="fullScreen" x-cloak x-on:click="close()" aria-label="{{ __('Back') }}">
<button type="button" data-search-leading data-search-back x-show="fullScreen" x-cloak x-on:click="close(true)" aria-label="{{ __('Back') }}">
<x-livewire-material::icon name="arrow_back" />
</button>
@@ -78,7 +109,7 @@
aria-label="{{ $label ?? $placeholder }}"
x-on:focus="focused()"
x-on:click="show()"
x-on:input="show()"
x-on:input="typed($event)"
x-on:keydown.arrow-down.prevent="show(); $nextTick(() => step(1))"
data-search-input
/>
@@ -103,10 +134,14 @@
x-on:keydown.arrow-up.prevent="step(-1)"
x-on:click="choose($event)"
>
@isset($suggestions)
<div data-search-suggestions role="list" x-show="query === ''">{{ $suggestions }}</div>
@endisset
@if ($slot->hasActualContent())
<div data-search-results role="list">{{ $slot }}</div>
<div data-search-results role="list" @isset($suggestions) x-cloak x-show="query !== ''" @endisset>{{ $slot }}</div>
@elseif (isset($empty))
<div data-search-results><p class="px-4 py-3 type-body-md text-on-surface-variant">{{ $empty }}</p></div>
<div data-search-results @isset($suggestions) x-cloak x-show="query !== ''" @endisset><p class="px-4 py-3 type-body-md text-on-surface-variant">{{ $empty }}</p></div>
@endif
</div>
@@ -169,6 +169,40 @@
</div>
</div>
BLADE,
'Search entry points and suggestions' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<div x-data="{ query: '' }">
<x-search x-model="query" placeholder="Search shares">
<x-slot:suggestions>
<x-list>
<x-list-item title="Recent: holiday photos" icon="history" link="#fields" />
<x-list-item title="Recent: contract" icon="history" link="#fields" />
<x-list-item title="Shares expiring this week" icon="trending_up" link="#fields" />
</x-list>
</x-slot:suggestions>
<x-list>
<x-list-item title="holiday-photos.zip" description="248 MB" icon="folder_zip" link="#fields" x-show="'holiday-photos.zip'.includes(query.toLowerCase())" />
<x-list-item title="contract.pdf" description="1.2 MB" icon="picture_as_pdf" link="#fields" x-show="'contract.pdf'.includes(query.toLowerCase())" />
</x-list>
</x-search>
</div>
{{-- M3's search icon button: search as a secondary action, expanding into the full-screen view. --}}
<div class="flex items-center gap-2 rounded-corner-full bg-surface-container-high px-2 py-1">
<x-button icon="menu" aria-label="Open the menu" />
<span class="grow type-title-md text-on-surface">Shares</span>
<x-search trigger="icon" label="Search shares">
<x-slot:suggestions>
<x-list>
<x-list-item title="Recent: design review" icon="history" link="#fields" />
</x-list>
</x-slot:suggestions>
<x-slot:empty>No shares match.</x-slot:empty>
</x-search>
</div>
</div>
BLADE,
'A form' => <<<'BLADE'
<x-card variant="outlined" class="w-full max-w-xl">
<x-form x-on:submit.prevent="materialToast('Share created', { type: 'success' })" separator>