*/ public array $kinds = ['photos']; /** @var list */ public array $tags = ['php', 'js', 'css']; public function onlyVideo(): void { $this->kinds = ['video']; } public function removeTag(string $tag): void { $this->tags = array_values(array_diff($this->tags, [$tag])); } public function render(): string { return <<<'BLADE'

kinds: {{ implode(',', $kinds) }}

@foreach (['photos' => 'Photos', 'documents' => 'Documents', 'video' => 'Video'] as $value => $name) @endforeach

tags: {{ implode(',', $tags) }}

@foreach ($tags as $tag) @endforeach
BLADE; } } function chipProbe() { Livewire::component('chip-probe', ChipProbe::class); Route::middleware('web')->get('/chip-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/chip-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } function chipShowcase() { return visit('/material/chips')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } /** * The script for a showcase filter chip's checkbox, found by its value. */ function filterInput(string $value): string { return "document.querySelector('#chips input[type=\"checkbox\"][value=\"{$value}\"]')"; } it('toggles a filter chip with a click and with Space, and grows its check in', function () { $check = fn (string $value): string => filterInput($value).".parentElement.querySelector('[data-md-chip-check]').parentElement.getBoundingClientRect().width"; $page = chipShowcase()->assertNoJavaScriptErrors(); $page->click('#chips label:has-text("Documents")') ->assertScript(filterInput('documents').'.checked') ->assertScript("Math.round({$check('documents')}) === 18") ->assertScript('getComputedStyle('.filterInput('documents').".parentElement).backgroundColor !== 'rgba(0, 0, 0, 0)'") ->assertScript("window.eval(\"Alpine.\$data(document.querySelector('#chips input[value=documents]')).kinds.join(',')\") === 'photos,documents'"); // A key press first, so the browser is in keyboard modality; focused directly, not tabbed to, // because WebKit leaves form controls out of the Tab order unless full keyboard access is on. $page->keys('#content', 'Tab'); $page->script(filterInput('archives').'.focus()'); $page->keys(':focus', 'Space') ->assertScript(filterInput('archives').'.checked') ->assertScript("Math.round({$check('archives')}) === 18"); $page->keys(':focus', 'Space') ->assertScript('! '.filterInput('archives').'.checked') ->assertScript("Math.round({$check('archives')}) === 0"); }); it('binds filter chips to a Livewire array, and a server change reaches them after a morph', function () { $input = fn (string $value): string => "document.querySelector('input[value=\"{$value}\"]')"; $page = chipProbe()->assertNoJavaScriptErrors(); $page->assertScript("{$input('photos')}.checked && ! {$input('documents')}.checked"); $page->click('label:has-text("Documents")') ->assertSeeIn('#kinds', 'photos,documents'); $page->click('button:has-text("Only video")') ->assertSeeIn('#kinds', 'video') ->assertScript("! {$input('photos')}.checked && ! {$input('documents')}.checked && {$input('video')}.checked") ->assertScript("getComputedStyle({$input('video')}.parentElement).backgroundColor !== 'rgba(0, 0, 0, 0)'") ->assertScript("getComputedStyle({$input('photos')}.parentElement).backgroundColor === 'rgba(0, 0, 0, 0)'"); }); it('removes a Livewire input chip with its button and with Delete, focusing the chip after it', function () { $page = chipProbe(); $page->click('button[aria-label="Remove js"]') ->assertSeeIn('#tags', 'php,css') ->assertScript("document.querySelector('button[aria-label=\"Remove js\"]') === null"); $page->script("document.querySelector('button[aria-label=\"Remove php\"]').focus()"); $page->keys(':focus', 'Delete') ->assertScript("document.querySelector('#tags').textContent === 'css'") ->assertScript("document.querySelector('button[aria-label=\"Remove php\"]') === null") ->assertScript("document.activeElement.getAttribute('aria-label') === 'Remove css'"); }); it('removes an Alpine input chip with Backspace, focusing the chip before it', function () { $remove = fn (string $who): string => "document.querySelector('#chips button[aria-label=\"Remove {$who}@example.com\"]')"; $page = chipShowcase(); // Named in the browser: the chips come from x-for, so the server never knew their labels. $page->assertScript("{$remove('ben')} !== null"); $page->script("{$remove('ben')}.focus()"); $page->keys(':focus', 'Backspace') ->assertScript("{$remove('ben')} === null") ->assertScript("{$remove('anna')} !== null && {$remove('chiara')} !== null") ->assertScript("document.activeElement.getAttribute('aria-label') === 'Remove anna@example.com'"); }); it('scrolls a chip set sideways, fading the edge it can still scroll towards', function () { // The scrolling row, inside the component that also holds its scroll buttons. $row = "document.querySelector('#chips [x-data=\"materialChipSet\"] > [data-chip-set]')"; $page = chipShowcase(); $page->assertScript("{$row}.scrollWidth > {$row}.clientWidth") ->assertScript("{$row}.hasAttribute('data-scroll-end') && ! {$row}.hasAttribute('data-scroll-start')") ->assertScript("getComputedStyle({$row}).flexWrap === 'nowrap'"); $page->script("{$row}.querySelector('input[value=\"unopened\"]').focus()"); $page->assertScript("Math.abs({$row}.scrollLeft) > 0") ->assertScript("{$row}.hasAttribute('data-scroll-start')") ->assertScript("getComputedStyle({$row}).getPropertyValue('--chip-fade-start').trim() === '1.5rem'"); }); it('draws the chip 32px tall and catches presses over 48px, the chip and its remove button alike', function () { $page = chipProbe(); $page->assertScript("document.querySelector('[data-md-chip=\"input\"]').getBoundingClientRect().height === 32") // The remove button is an 18px icon whose target reaches 15px past it on every side. ->assertScript("(() => { const button = document.querySelector('button[aria-label=\"Remove css\"]'); const box = button.getBoundingClientRect(); const after = getComputedStyle(button, '::after'); return box.width === 18 && box.height === 18 && after.position === 'absolute' && after.top === '-15px' && after.left === '-15px'; })()"); // A press 22px out from the icon's centre, over the chip's 48px strip, still removes the chip. $page->script("(() => { const button = document.querySelector('button[aria-label=\"Remove css\"]'); const box = button.getBoundingClientRect(); document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2 + 22).click(); })()"); $page->assertSeeIn('#tags', 'php,js') ->assertScript("document.querySelector('button[aria-label=\"Remove css\"]') === null"); }); it('draws a selected filter chip without its outline, and puts an unselected one\'s label 16px in', function () { $input = fn (string $value): string => "document.querySelector('input[value=\"{$value}\"]')"; $page = chipProbe(); $page->assertScript("getComputedStyle({$input('photos')}.parentElement).borderTopColor === 'rgba(0, 0, 0, 0)'") ->assertScript("getComputedStyle({$input('documents')}.parentElement).borderTopWidth === '1px'") ->assertScript("getComputedStyle({$input('documents')}.parentElement).borderTopColor !== 'rgba(0, 0, 0, 0)'") // 1px border, 7px padding, the empty check's 8px margin: Compose's 16px before the label. ->assertScript("Math.round({$input('documents')}.parentElement.querySelector('[data-md-chip-label]').getBoundingClientRect().left - {$input('documents')}.parentElement.getBoundingClientRect().left) === 16"); });