Plan step 36 review. 52fd65a3 replaced the md-visually-hidden class on
field's spoken counter, search's result count and a bound filter chip's
native checkbox with hooks carrying copies of its clip, on the reading
that a package view writes no class but the interaction classes. The
brief and step 34 let a view write the text classes too, and one shared
source beats three copies: the class is back, the copies and the
data-md-field-live and data-md-chip-native hooks are gone
(data-md-search-status predates them and stays). chip.css keeps its
tooltip.css import.
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>
|