Files
livewire-material/tests/Browser/ChipsTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 889f8a8aa1
tests / lint (push) Has been cancelled
tests / feature (8.4) (push) Has been cancelled
tests / feature (8.5) (push) Has been cancelled
tests / browser (chrome, chromium) (push) Has been cancelled
tests / browser (firefox, firefox) (push) Has been cancelled
tests / browser (safari, webkit) (push) Has been cancelled
Put the showcase in the app shell, with a page per section
The showcase is now an overview and one page per section behind a
grouped navigation rail, moved between with wire:navigate, instead of
one long page under a scrolling top bar. The switch exposed a WebKit
bug in the menu: inside a focusable region, WebKit moves focus out of
the closing popover before its toggle event, so Escape no longer
returned focus to the trigger. The menu now reads it on beforetoggle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 09:30:26 +02:00

175 lines
7.1 KiB
PHP

<?php
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Livewire\Livewire;
class ChipProbe extends Component
{
/** @var list<string> */
public array $kinds = ['photos'];
/** @var list<string> */
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'
<div class="space-y-4 p-4">
<p>kinds: <span id="kinds">{{ implode(',', $kinds) }}</span></p>
<x-chip-set label="Kinds">
@foreach (['photos' => 'Photos', 'documents' => 'Documents', 'video' => 'Video'] as $value => $name)
<x-chip type="filter" :label="$name" :value="$value" wire:model.live="kinds" wire:key="kind-{{ $value }}" />
@endforeach
</x-chip-set>
<x-button label="Only video" wire:click="onlyVideo" />
<p>tags: <span id="tags">{{ implode(',', $tags) }}</span></p>
<x-chip-set label="Tags">
@foreach ($tags as $tag)
<x-chip type="input" :label="$tag" removable wire:remove="removeTag('{{ $tag }}')" wire:key="tag-{{ $tag }}" />
@endforeach
</x-chip-set>
</div>
BLADE;
}
}
function chipProbe()
{
Livewire::component('chip-probe', ChipProbe::class);
Route::middleware('web')->get('/chip-probe', fn () => Blade::render(<<<'BLADE'
<!DOCTYPE html>
<html>
<head>
<x-theme-script />
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
</head>
<body class="bg-surface">
<livewire:chip-probe />
@livewireScripts
</body>
</html>
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-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 () {
$row = "document.querySelector('#chips [data-chip-set][x-data]')";
$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'");
});