<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
154 lines
6.6 KiB
PHP
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>
|