<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
120 lines
5.3 KiB
PHP
120 lines
5.3 KiB
PHP
<?php
|
|
|
|
it('draws a search bar whose combobox controls the view', function () {
|
|
$html = (string) $this->blade('<x-search id="find" placeholder="Search shares" wire:model.live.debounce.300ms="query" />');
|
|
|
|
expect($html)
|
|
->toContain('data-md-search')
|
|
->toContain('data-md-trigger="bar"')
|
|
->toContain('x-bind:data-md-open="open ? \'\' : null"')
|
|
->toContain('x-bind:data-md-full-screen="fullScreen ? \'\' : null"')
|
|
->toContain('role="search"')
|
|
->toContain('type="search"')
|
|
->toContain('placeholder="Search shares"')
|
|
->toContain('aria-label="Search shares"')
|
|
->toContain('role="combobox"')
|
|
->toContain('aria-haspopup="dialog"')
|
|
->toContain('aria-controls="find-view"')
|
|
->toContain('aria-expanded="false"')
|
|
->toContain('wire:model.live.debounce.300ms="query"')
|
|
->toContain('id="find-view"')
|
|
->toContain('data-md-search-clear')
|
|
->toContain('data-md-search-scrim')
|
|
->toContain('aria-label="Back"')
|
|
->toContain('materialSearch(false,')
|
|
->not->toContain('data-md-search-results')
|
|
->not->toContain('class="relative"');
|
|
|
|
// The state belongs to the combobox, never to the bare textbox ARIA gives it to.
|
|
expect($html)->toMatch('/<span\s+data-md-search-field[^>]*aria-expanded/s');
|
|
expect($html)->not->toMatch('/<input[^>]*aria-expanded/s');
|
|
});
|
|
|
|
it('announces how many results there are, politely', function () {
|
|
$html = (string) $this->blade('<x-search />');
|
|
|
|
expect($html)
|
|
->toContain('data-md-search-status class="md-visually-hidden"')
|
|
->toContain('aria-live="polite"')
|
|
->toContain('aria-atomic="true"')
|
|
->toContain('No results')
|
|
->toContain('1 result')
|
|
->toContain(':count results');
|
|
});
|
|
|
|
it('shows the results as a list, or what to say when there are none', function () {
|
|
expect((string) $this->blade('<x-search><a href="/shares/1">holiday.zip</a></x-search>'))
|
|
->toContain('data-md-search-results role="list"')
|
|
->toContain('<a href="/shares/1">holiday.zip</a>')
|
|
->and((string) $this->blade('<x-search><x-slot:empty>No shares match.</x-slot:empty></x-search>'))
|
|
->toContain('<p data-md-search-empty>No shares match.</p>')
|
|
->not->toContain('data-md-search-results role="list"');
|
|
});
|
|
|
|
it('offers M3\'s search-icon entry point', function () {
|
|
$html = (string) $this->blade('<x-search id="find" trigger="icon" label="Search shares" />');
|
|
|
|
expect($html)
|
|
->toContain('data-md-trigger="icon"')
|
|
->toContain('data-md-search-trigger')
|
|
->toContain('x-ref="trigger"')
|
|
->toContain('x-on:click="expand()"')
|
|
->toContain('aria-label="Search shares"')
|
|
->toContain('aria-haspopup="dialog"')
|
|
->toContain('aria-controls="find-view"')
|
|
->toContain('materialSearch(false,')
|
|
->toContain("'icon'")
|
|
// The bar is still there: it becomes the expanded view's header.
|
|
->toContain('data-md-search-bar');
|
|
|
|
expect((string) $this->blade('<x-search />'))
|
|
->toContain('data-md-trigger="bar"')
|
|
->not->toContain('data-md-search-trigger');
|
|
});
|
|
|
|
it('shows suggestions until the first keystroke, then the results', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-search>
|
|
<a href="/shares/1">holiday.zip</a>
|
|
<x-slot:suggestions><a href="/recent">Recent shares</a></x-slot:suggestions>
|
|
</x-search>
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('data-md-search-suggestions role="list"')
|
|
->toContain('x-show="query === \'\'"')
|
|
->toContain('Recent shares')
|
|
->toContain('x-show="query !== \'\'"')
|
|
->toContain('holiday.zip')
|
|
->toContain('1 suggestion')
|
|
->toContain(':count suggestions');
|
|
|
|
// Without the slot the results stand alone and are never hidden.
|
|
expect((string) $this->blade('<x-search><a href="/shares/1">holiday.zip</a></x-search>'))
|
|
->not->toContain('data-md-search-suggestions')
|
|
->not->toContain('x-show="query !== \'\'"');
|
|
});
|
|
|
|
it('names the input, trails the bar, and stays docked on request', function () {
|
|
$html = (string) $this->blade('<x-search label="Search your shares" icon="travel_explore" docked><x-slot:trailing><button>AR</button></x-slot:trailing></x-search>');
|
|
|
|
expect($html)
|
|
->toContain('placeholder="Search"')
|
|
->toContain('aria-label="Search your shares"')
|
|
->toContain('data-md-search-trailing')
|
|
->toContain('<button>AR</button>')
|
|
->toContain('materialSearch(true,');
|
|
});
|
|
|
|
it('draws the bar and the view from its stylesheet, at M3\'s widths', function () {
|
|
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/search.css');
|
|
|
|
expect($css)->toContain('@layer material.components')
|
|
->toMatch('/\\[data-md-search\\] \\{\\s*--search-height: 56px;[^}]*--search-open-width: 720px;/')
|
|
->toContain('[data-md-search][data-md-full-screen] [data-md-search-view] {')
|
|
->toContain("[data-md-search][data-md-trigger='icon']:not([data-md-open]) [data-md-search-bar] {")
|
|
->not->toMatch('/\\d(?:\\.\\d+)?rem/')
|
|
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/search.css';")
|
|
->and((string) file_get_contents(__DIR__.'/../../../resources/css/tailwind.css'))->not->toContain('search.css');
|
|
});
|