Plan step 36. <x-button> renders data-md-button with its variant, colour, size, shape, icon-button width, selected state and compact FAB as data-md-* attributes, and passes the glyph's size to <x-icon>. button.css draws Button*Tokens' geometry per size, the colour roles through custom properties that the disabled treatment replaces, the state layer, focus ring and 48px target, and the compact FAB below 600px. Its corner rules carry no specificity (:where), so a group's stylesheet reshapes the buttons inside it with any selector; the split button no longer passes an empty corners prop, which is gone. data-icon-button becomes data-md-icon-button, also in groups.css, toolbar.css and theme-toggle's own button (navigation group), and the AppBarTest assertions follow the new hooks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
177 lines
8.0 KiB
PHP
177 lines
8.0 KiB
PHP
<?php
|
|
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
|
|
it('groups buttons with the spacing of their size', function () {
|
|
expect((string) $this->blade('<x-button-group label="Views" size="md"><x-button label="Day" /></x-button-group>'))
|
|
->toContain('role="group"')
|
|
->toContain('aria-label="Views"')
|
|
->toContain('data-button-group="standard"')
|
|
->toContain('data-size="md"')
|
|
->toContain('gap-2')
|
|
->and((string) $this->blade('<x-button-group><x-button label="Day" /></x-button-group>'))
|
|
->toContain('gap-3');
|
|
});
|
|
|
|
it('connects buttons 2px apart', function () {
|
|
expect((string) $this->blade('<x-button-group connected><x-button label="Day" /></x-button-group>'))
|
|
->toContain('data-button-group="connected"')
|
|
->toContain('gap-0.5');
|
|
});
|
|
|
|
it('squares a group, and a choice drawn as one', function () {
|
|
// M3 lists "Default shape | Round, square" as a button-group configuration; the shape covers
|
|
// every button in the group, so it need not be written on each one.
|
|
expect((string) $this->blade('<x-button-group shape="square"><x-button label="Day" /></x-button-group>'))
|
|
->toContain('data-shape="square"')
|
|
->and((string) $this->blade('<x-button-group connected shape="square"><x-button label="Day" /></x-button-group>'))
|
|
->toContain('data-button-group="connected"')
|
|
->toContain('data-shape="square"')
|
|
->and((string) $this->blade('<x-button-group><x-button label="Day" /></x-button-group>'))
|
|
->toContain('data-shape="round"')
|
|
->and((string) $this->blade('<x-group shape="square" name="x" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))
|
|
->toContain('data-shape="square"')
|
|
->and((string) $this->blade('<x-group name="x" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))
|
|
->toContain('data-shape="round"');
|
|
});
|
|
|
|
it('takes over aria-pressed for a group that carries a selection', function () {
|
|
$html = (string) $this->blade('<x-button-group connected selection="single" required label="View"><x-button label="Day" value="day" :selected="true" /></x-button-group>');
|
|
|
|
expect($html)
|
|
->toContain('data-selection="single"')
|
|
->toContain('data-selection-required')
|
|
->toContain('multiple: false')
|
|
->toContain('required: true')
|
|
->toContain('value: null')
|
|
->toContain('x-on:click="press($event)"')
|
|
// No model: the group takes the state from the buttons' own aria-pressed and goes on.
|
|
->toContain('x-modelable="value"')
|
|
->and((string) $this->blade('<x-button-group connected selection="multi"><x-button label="Bold" value="bold" :selected="false" /></x-button-group>'))
|
|
->toContain('multiple: true')
|
|
->toContain('required: false')
|
|
->toContain('value: []')
|
|
->and((string) $this->blade('<x-button-group connected><x-button label="Day" /></x-button-group>'))
|
|
->not->toContain('data-selection')
|
|
->not->toContain('press($event)');
|
|
});
|
|
|
|
it('entangles a selection group with the property it binds, and keeps wire:model off the div', function () {
|
|
$component = new class extends Component
|
|
{
|
|
public string $view = 'week';
|
|
|
|
public function render(): string
|
|
{
|
|
return <<<'BLADE'
|
|
<div>
|
|
<x-button-group connected selection="single" required wire:model.live="view" label="View">
|
|
<x-button label="Day" value="day" :selected="$view === 'day'" />
|
|
<x-button label="Week" value="week" :selected="$view === 'week'" />
|
|
</x-button-group>
|
|
</div>
|
|
BLADE;
|
|
}
|
|
};
|
|
|
|
$html = Livewire::test($component)->assertSet('view', 'week')->html();
|
|
|
|
expect($html)
|
|
->toContain('data-selection="single"')
|
|
->toMatch('/value: window\.Livewire\.find\(/')
|
|
->and(substr_count($html, 'wire:model.live="view"'))->toBe(0);
|
|
});
|
|
|
|
it('never wraps a group onto a second line', function () {
|
|
expect((string) $this->blade('<x-button-group><x-button label="Day" /></x-button-group>'))
|
|
->not->toContain('flex-wrap');
|
|
});
|
|
|
|
it('marks icon buttons, which keep their width when a group is pressed', function () {
|
|
expect((string) $this->blade('<x-button icon="format_bold" aria-label="Bold" />'))->toContain('data-md-icon-button')
|
|
->and((string) $this->blade('<x-button label="Bold" />'))->not->toContain('data-md-icon-button');
|
|
});
|
|
|
|
it('draws a choice as connected radios', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-group label="Theme" name="theme" x-model="theme" :options="[
|
|
['id' => 'light', 'name' => 'Light', 'icon' => 'light_mode'],
|
|
['id' => 'dark', 'name' => 'Dark', 'disabled' => true],
|
|
]" />
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('<legend class="mb-2 type-label-lg text-on-surface-variant">Theme</legend>')
|
|
->toContain('data-button-group="connected"')
|
|
->toContain('type="radio"')
|
|
->toContain('name="theme"')
|
|
->toContain('x-model="theme"')
|
|
->toContain('value="light"')
|
|
->toContain('has-checked:bg-secondary has-checked:text-on-secondary')
|
|
->toContain('disabled')
|
|
->and(substr_count($html, '<svg'))->toBe(1);
|
|
});
|
|
|
|
it('keeps a 48px target and a 48px minimum width on the smallest connected segments', function () {
|
|
$options = [['id' => 'a', 'name' => 'A']];
|
|
|
|
expect((string) $this->blade('<x-group size="sm" name="x" :$options />', ['options' => $options]))
|
|
->toContain('touch-target min-w-12')
|
|
->and((string) $this->blade('<x-group size="md" name="x" :$options />', ['options' => $options]))
|
|
->not->toContain('touch-target')
|
|
->toContain('min-w-0');
|
|
});
|
|
|
|
it('draws several choices as checkboxes', function () {
|
|
expect((string) $this->blade('<x-group name="days" multiple variant="outlined" :options="[[\'id\' => \'mon\', \'name\' => \'Mon\']]" />'))
|
|
->toContain('type="checkbox"')
|
|
->toContain('name="days[]"')
|
|
->toContain('has-checked:bg-inverse-surface');
|
|
});
|
|
|
|
it('binds to a Livewire property and shows its validation message', function () {
|
|
$component = new class extends Component
|
|
{
|
|
public string $theme = 'dark';
|
|
|
|
public function save(): void
|
|
{
|
|
$this->validate(['theme' => 'in:light'], ['theme.in' => 'Pick light.']);
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
return <<<'BLADE'
|
|
<div>
|
|
<x-group wire:model="theme" hint="How it looks" :options="[['id' => 'light', 'name' => 'Light'], ['id' => 'dark', 'name' => 'Dark']]" />
|
|
</div>
|
|
BLADE;
|
|
}
|
|
};
|
|
|
|
Livewire::test($component)
|
|
->assertSeeHtml('wire:model="theme"')
|
|
->assertSee('How it looks')
|
|
->set('theme', 'light')
|
|
->assertSet('theme', 'light')
|
|
->set('theme', 'dark')
|
|
->call('save')
|
|
->assertSee('Pick light.')
|
|
->assertDontSee('How it looks');
|
|
});
|
|
|
|
it('adds hint-class to the hint, which a validation message still replaces', function () {
|
|
$options = [['id' => 'flat', 'name' => 'Flat'], ['id' => 'hilly', 'name' => 'Hilly']];
|
|
|
|
expect((string) $this->blade('<x-group wire:model="terrain" hint="No elevation data here" hint-class="text-warning" :$options />', ['options' => $options]))
|
|
->toContain('<p class="mt-1 type-body-sm [:where(&)]:text-on-surface-variant text-warning">No elevation data here</p>')
|
|
->and((string) $this->blade('<x-group wire:model="terrain" hint="No elevation data here" :$options />', ['options' => $options]))
|
|
->toContain('<p class="mt-1 type-body-sm text-on-surface-variant">No elevation data here</p>');
|
|
|
|
expect((string) $this->withViewErrors(['terrain' => 'Pick a terrain.'])->blade('<x-group wire:model="terrain" hint="No elevation data here" hint-class="text-warning" :$options />', ['options' => $options]))
|
|
->toContain('<p class="mt-1 type-body-sm text-error">Pick a terrain.</p>')
|
|
->not->toContain('No elevation data here')
|
|
->not->toContain('text-warning');
|
|
});
|