Files
livewire-material/resources/views/components/search.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 4141ebe7b0 Draw the search without Tailwind
<x-search> renders data-md-search with data-md-trigger and binds
data-md-open and data-md-full-screen; its parts become data-md-search-*
and the empty row data-md-search-empty. search.css moves into
material.components on px, spacing and state tokens (plan step 36).
search.js and the showcase layout follow the renamed hooks.

Browser tests cover the polite result count as it changes, the icon
entry point expanding full screen and returning focus, and suggestions
swapping to results on the first key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 15:04:50 +02:00

154 lines
6.6 KiB
PHP

{{-- M3's search: a search bar that opens into a search view with the results.
Bind the input like any other (`wire:model.live.debounce.300ms="query"`) and render the
results in the slot, from a Livewire property or computed property that follows the query;
`empty` is shown instead when the slot renders nothing (say, "No shares match"). Results are
usually `<x-list-item>`s with a `link`, or buttons: choosing one closes the view.
Docked under the bar from `medium` over a scrim, full screen below it with a back arrow
(resources/css/components/search.css); `docked` keeps it docked at every width. The bar is
never wider than M3's 720px and grows to that width while it is focused: set `--search-width`
on a wrapper for a narrower resting bar (M3's own is 360px). `placeholder` ("Search"),
`label` (the input's name when it differs from the placeholder), leading `icon` (`search`), and
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
for both (docs/reference/m3/components-navigation-selection-inputs.md § Search).
The root renders `data-md-search` with `data-md-trigger`, and `data-md-open` and
`data-md-full-screen` while they hold; the parts are `data-md-search-*`, drawn by
resources/css/components/search.css. `class` and `wire:key` land on the root. --}}
@props([
'placeholder' => null,
'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), @js($trigger))"
x-on:keydown.escape="if (open) { $event.stopPropagation(); close(true); }"
x-on:focusout="leave($event)"
x-on:pointerdown.outside="close()"
x-trap.noscroll="fullScreen"
x-bind:data-md-open="open ? '' : null"
x-bind:data-md-full-screen="fullScreen ? '' : null"
data-md-search
data-md-trigger="{{ $trigger }}"
{{ $attributes->only(['class', 'style', 'wire:key']) }}
>
<div data-md-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-md-search-trigger
>
<x-livewire-material::icon :name="$icon" />
</button>
@endif
<div data-md-search-bar role="search" x-on:click="if ($event.target === $el) $refs.input.focus()">
<span data-md-search-leading x-show="! fullScreen">
<x-livewire-material::icon :name="$icon" />
</span>
<button type="button" data-md-search-leading data-md-search-back x-show="fullScreen" x-cloak x-on:click="close(true)" aria-label="{{ __('Back') }}">
<x-livewire-material::icon name="arrow_back" />
</button>
<span
data-md-search-field
role="combobox"
aria-haspopup="dialog"
aria-controls="{{ $id }}-view"
aria-expanded="false"
x-bind:aria-expanded="open.toString()"
>
<input
{{ $attributes->except(['class', 'wire:key', 'id', 'placeholder', 'type']) }}
x-ref="input"
id="{{ $id }}"
type="search"
autocomplete="off"
enterkeyhint="search"
placeholder="{{ $placeholder }}"
aria-label="{{ $label ?? $placeholder }}"
x-on:focus="focused()"
x-on:click="show()"
x-on:input="typed($event)"
x-on:keydown.arrow-down.prevent="show(); $nextTick(() => step(1))"
data-md-search-input
/>
</span>
<button type="button" data-md-search-clear x-on:click="clear()" aria-label="{{ __('Clear') }}">
<x-livewire-material::icon name="close" />
</button>
@isset($trailing)
<span data-md-search-trailing>{{ $trailing }}</span>
@endisset
</div>
<div
x-ref="view"
id="{{ $id }}-view"
data-md-search-view
x-cloak
x-show="open"
x-on:keydown.arrow-down.prevent="step(1)"
x-on:keydown.arrow-up.prevent="step(-1)"
x-on:click="choose($event)"
>
@isset($suggestions)
<div data-md-search-suggestions role="list" x-show="query === ''">{{ $suggestions }}</div>
@endisset
@if ($slot->hasActualContent())
<div data-md-search-results role="list" @isset($suggestions) x-cloak x-show="query !== ''" @endisset>{{ $slot }}</div>
@elseif (isset($empty))
<div data-md-search-results @isset($suggestions) x-cloak x-show="query !== ''" @endisset><p data-md-search-empty>{{ $empty }}</p></div>
@endif
</div>
<span data-md-search-status class="md-visually-hidden" aria-live="polite" aria-atomic="true" x-text="announcement"></span>
</div>