M3 § Checkbox, Behaviour asks that from expanded (840px) related checkboxes be gathered into a contained region rather than left as one long column. That is a rule about the page around the control, not about the control, so it is written down rather than built: the component's header and its SKILL.md entry now say it, and the showcase lays a group out in an `expanded:grid-cols-2` card (plan step 24, audit docs/audits/m3-alignment/inputs.md § Missing). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
133 lines
6.1 KiB
PHP
133 lines
6.1 KiB
PHP
<?php
|
|
|
|
it('draws a checkbox whose row is its label', function () {
|
|
$html = (string) $this->blade('<x-checkbox id="notify" label="Notify me" hint="On every download" wire:model="notify" class="mt-2" />');
|
|
|
|
expect($html)
|
|
->toContain('<div class="min-w-0 mt-2">')
|
|
->toContain('<label for="notify" data-selection')
|
|
->toContain('data-checkbox')
|
|
->toContain('type="checkbox"')
|
|
->toContain('wire:model="notify"')
|
|
->toContain('data-check')
|
|
->toContain('data-mixed')
|
|
->toContain('Notify me')
|
|
->toContain('On every download')
|
|
->not->toContain('data-indeterminate');
|
|
});
|
|
|
|
it('fills the box with an 18px tick from the 20 optical cut', function () {
|
|
expect((string) $this->blade('<x-checkbox label="Notify me" />'))
|
|
->toContain((string) $this->blade('<x-icon name="check" class="size-4.5" optical="20" data-check />'))
|
|
->toContain((string) $this->blade('<x-icon name="remove" class="size-4.5" optical="20" data-mixed />'));
|
|
});
|
|
|
|
it('marks a checkbox that is partly ticked', function () {
|
|
expect((string) $this->blade('<x-checkbox label="Select all" indeterminate />'))->toContain('data-indeterminate');
|
|
});
|
|
|
|
it('gives a selection control without a label a 48px target', function () {
|
|
expect((string) $this->blade('<x-checkbox aria-label="Select all" />'))->toContain('touch-target')
|
|
->and((string) $this->blade('<x-toggle aria-label="Public link" />'))->toContain('touch-target')
|
|
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'\']]" />'))->toContain('touch-target');
|
|
|
|
// A labelled control is pressed anywhere along its row, so it needs no extra target.
|
|
expect((string) $this->blade('<x-checkbox label="Notify me" />'))->not->toContain('touch-target')
|
|
->and((string) $this->blade('<x-toggle label="Public link" />'))->not->toContain('touch-target')
|
|
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))->not->toContain('touch-target');
|
|
});
|
|
|
|
it('leaves a checkbox group to the caller and says how M3 wants it laid out', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<fieldset>
|
|
<legend>Tell me about</legend>
|
|
<div class="grid gap-4 expanded:grid-cols-2">
|
|
<x-checkbox label="Downloads" name="notify[]" value="downloads" />
|
|
<x-checkbox label="Comments" name="notify[]" value="comments" />
|
|
</div>
|
|
</fieldset>
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('class="grid gap-4 expanded:grid-cols-2"')
|
|
->toContain('Downloads')
|
|
->toContain('Comments')
|
|
->and(substr_count($html, 'data-checkbox'))->toBe(2);
|
|
|
|
// The rule is guidance, not markup, so the component's header has to carry it.
|
|
expect(file_get_contents(__DIR__.'/../../../resources/views/components/checkbox.blade.php'))
|
|
->toContain('expanded')
|
|
->toContain('contained region');
|
|
});
|
|
|
|
it('puts a checkbox at the end of its row on request', function () {
|
|
expect((string) $this->blade('<x-checkbox label="Show" right />'))->toContain('flex-row-reverse justify-between');
|
|
});
|
|
|
|
it('shows a checkbox\'s errors', function () {
|
|
$html = (string) $this->withViewErrors(['terms' => ['Accept the terms to continue.']])
|
|
->blade('<x-checkbox id="terms" label="I accept the terms" wire:model="terms" />');
|
|
|
|
expect($html)
|
|
->toContain('data-invalid')
|
|
->toContain('aria-invalid="true"')
|
|
->toContain('aria-describedby="terms-support"')
|
|
->toContain('Accept the terms to continue.');
|
|
});
|
|
|
|
it('draws radio buttons in a fieldset named by its question', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-radio label="Who can open it" wire:model.live="audience" :options="[
|
|
['id' => 'anyone', 'name' => 'Anyone with the link'],
|
|
['id' => 'password', 'name' => 'Anyone with the password', 'hint' => 'Share it separately'],
|
|
['id' => 'team', 'name' => 'My team', 'disabled' => true],
|
|
]" />
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('<fieldset')
|
|
->toContain('Who can open it</legend>')
|
|
->and(substr_count($html, 'type="radio"'))->toBe(3)
|
|
->and(substr_count($html, 'name="audience"'))->toBe(3)
|
|
->and(substr_count($html, 'wire:model.live="audience"'))->toBe(3)
|
|
->and($html)->toContain('value="password"')
|
|
->toContain('Share it separately')
|
|
->toMatch('/value="team"\s+disabled/');
|
|
});
|
|
|
|
it('names unbound radios after their group and checks the given value', function () {
|
|
$html = (string) $this->blade('<x-radio name="theme" value="dark" inline :options="[[\'id\' => \'light\', \'name\' => \'Light\'], [\'id\' => \'dark\', \'name\' => \'Dark\']]" />');
|
|
|
|
expect($html)
|
|
->toContain('medium:flex')
|
|
->and(substr_count($html, 'name="theme"'))->toBe(2)
|
|
->and($html)->toMatch('/value="dark"\s+checked/')
|
|
->not->toMatch('/value="light"\s+checked/');
|
|
});
|
|
|
|
it('shows the errors of a radio group', function () {
|
|
expect((string) $this->withViewErrors(['audience' => ['Choose who can open it.']])
|
|
->blade('<x-radio wire:model="audience" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))
|
|
->toContain('data-invalid')
|
|
->toContain('Choose who can open it.');
|
|
});
|
|
|
|
it('draws a switch on a checkbox with the switch role', function () {
|
|
$html = (string) $this->blade('<x-toggle id="public" label="Public link" wire:model.live="public" right />');
|
|
|
|
expect($html)
|
|
->toContain('<label for="public" data-selection')
|
|
->toContain('data-switch')
|
|
->toContain('role="switch"')
|
|
->toContain('type="checkbox"')
|
|
->toContain('wire:model.live="public"')
|
|
->toContain('data-handle')
|
|
->toContain('flex-row-reverse')
|
|
->not->toContain('data-icons');
|
|
});
|
|
|
|
it('puts icons on a switch\'s handle', function () {
|
|
expect((string) $this->blade('<x-toggle label="Wi-Fi" icons />'))->toContain('data-icons="both"')->toContain('data-on')->toContain('data-off')
|
|
->and((string) $this->blade('<x-toggle label="Wi-Fi" icons="selected" />'))->toContain('data-icons="selected"');
|
|
});
|