Draw the switch without Tailwind
<x-toggle> renders data-md-toggle, its row (data-md-right), track, handle and icons as data-md-* attributes; the handle's icons take size 16 and a label-less track md-touch-target (plan step 36). toggle.css gains the root; a browser test pins the handle's 16px/24px sizes and 16px/36px centres off and on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
8747163fce
commit
b42069503d
@@ -11,8 +11,10 @@
|
|||||||
* state layer is a 40px circle around the handle. Its icons are 16px, on-primary-container when on.
|
* state layer is a 40px circle around the handle. Its icons are 16px, on-primary-container when on.
|
||||||
* Disabled, the track is on-surface at 12% and the handle on-surface at 38% (surface when on).
|
* Disabled, the track is on-surface at 12% and the handle on-surface at 38% (surface when on).
|
||||||
*
|
*
|
||||||
* [data-md-switch] the track; `data-md-icons` = "both" or "selected"
|
* [data-md-toggle] the root; `class` and `style` land here
|
||||||
* input, [data-md-switch-handle] (with [data-md-switch-on] and [data-md-switch-off] icons)
|
* [data-md-selection-row] data-md-right puts the switch at the end of the row
|
||||||
|
* [data-md-switch] the track; `data-md-icons` = "both" or "selected"
|
||||||
|
* input, [data-md-switch-handle] (with [data-md-switch-on] and [data-md-switch-off] icons)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
@@ -21,6 +23,10 @@
|
|||||||
@import './icon.css';
|
@import './icon.css';
|
||||||
|
|
||||||
@layer material.components {
|
@layer material.components {
|
||||||
|
[data-md-toggle] {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* The positions below are the handle's centres less half its size, measured inside the track's
|
/* The positions below are the handle's centres less half its size, measured inside the track's
|
||||||
2px outline. */
|
2px outline. */
|
||||||
[data-md-switch] {
|
[data-md-switch] {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
cross when off; `icons="selected"` only the check. Every other attribute reaches the `<input>`
|
cross when off; `icons="selected"` only the check. Every other attribute reaches the `<input>`
|
||||||
(resources/css/components/toggle.css).
|
(resources/css/components/toggle.css).
|
||||||
|
|
||||||
Without a label the row is only the 52×32 track, so the track carries `touch-target` and catches
|
Without a label the row is only the 52×32 track, so the track carries `md-touch-target` and
|
||||||
presses over M3's 48px minimum. --}}
|
catches presses over M3's 48px minimum. `class` and `style` land on the root, `data-md-toggle`. --}}
|
||||||
|
|
||||||
@props([
|
@props([
|
||||||
'label' => null,
|
'label' => null,
|
||||||
@@ -23,35 +23,30 @@
|
|||||||
$icons = $icons === 'selected' ? 'selected' : ($icons ? 'both' : null);
|
$icons = $icons === 'selected' ? 'selected' : ($icons ? 'both' : null);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
|
<div data-md-toggle {{ $attributes->only(['class', 'style', 'wire:key']) }}>
|
||||||
<label for="{{ $id }}" data-md-selection-row @class([
|
<label for="{{ $id }}" data-md-selection-row @if ($right) data-md-right @endif>
|
||||||
'flex gap-4',
|
<span data-md-switch @if ($icons) data-md-icons="{{ $icons }}" @endif @if (blank($label) && blank($hint)) class="md-touch-target" @endif>
|
||||||
'items-start' => filled($hint),
|
|
||||||
'items-center' => blank($hint),
|
|
||||||
'flex-row-reverse justify-between' => $right,
|
|
||||||
])>
|
|
||||||
<span data-md-switch @if ($icons) data-md-icons="{{ $icons }}" @endif @class(['touch-target' => blank($label) && blank($hint)])>
|
|
||||||
<input
|
<input
|
||||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
{{ $attributes->except(['class', 'style', 'id', 'wire:key']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
role="switch"
|
role="switch"
|
||||||
/>
|
/>
|
||||||
<span data-md-switch-handle>
|
<span data-md-switch-handle>
|
||||||
@if ($icons)
|
@if ($icons)
|
||||||
<x-livewire-material::icon name="check" class="size-4" optical="20" data-md-switch-on />
|
<x-livewire-material::icon name="check" size="16" data-md-switch-on />
|
||||||
<x-livewire-material::icon name="close" class="size-4" optical="20" data-md-switch-off />
|
<x-livewire-material::icon name="close" size="16" data-md-switch-off />
|
||||||
@endif
|
@endif
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@if (filled($label) || filled($hint))
|
@if (filled($label) || filled($hint))
|
||||||
<span class="min-w-0">
|
<span data-md-selection-text>
|
||||||
@if (filled($label))
|
@if (filled($label))
|
||||||
<span class="block type-body-lg text-on-surface" data-md-selection-label>{{ $label }}</span>
|
<span data-md-selection-label>{{ $label }}</span>
|
||||||
@endif
|
@endif
|
||||||
@if (filled($hint))
|
@if (filled($hint))
|
||||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
<span data-md-selection-hint>{{ $hint }}</span>
|
||||||
@endif
|
@endif
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -312,3 +312,14 @@ it('lets a selection control without a label be pressed at the edge of its 48px
|
|||||||
->assertScript($pressAtEdge('#bare-switch', '0', '22'))
|
->assertScript($pressAtEdge('#bare-switch', '0', '22'))
|
||||||
->assertScript($pressAtEdge('input[name=\"bare-radio\"]', '0', '-22'));
|
->assertScript($pressAtEdge('input[name=\"bare-radio\"]', '0', '-22'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('draws the switch\'s handle at SwitchTokens\' sizes and centres, off and on', function () {
|
||||||
|
$handle = "(() => { const track = document.querySelector('[data-md-switch]:has(#public)').getBoundingClientRect(); const handle = document.querySelector('[data-md-switch]:has(#public) [data-md-switch-handle]').getBoundingClientRect(); return [track.width, track.height, handle.width, Math.round(handle.left + handle.width / 2 - track.left), Math.round(handle.top + handle.height / 2 - track.top)].join(); })()";
|
||||||
|
|
||||||
|
$page = fieldProbe()->assertScript("{$handle} === '52,32,16,16,16'");
|
||||||
|
|
||||||
|
$page->keys('#public', 'Space')
|
||||||
|
->assertSeeIn('#public-state', 'true')
|
||||||
|
->wait(0.7)
|
||||||
|
->assertScript("{$handle} === '52,32,24,36,16'");
|
||||||
|
});
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ it('marks a checkbox that is partly ticked', function () {
|
|||||||
|
|
||||||
it('gives a selection control without a label a 48px target', function () {
|
it('gives a selection control without a label a 48px target', function () {
|
||||||
expect((string) $this->blade('<x-checkbox aria-label="Select all" />'))->toMatch('/<span data-md-checkbox-box\s+class="md-touch-target"\s*>/')
|
expect((string) $this->blade('<x-checkbox aria-label="Select all" />'))->toMatch('/<span data-md-checkbox-box\s+class="md-touch-target"\s*>/')
|
||||||
->and((string) $this->blade('<x-toggle aria-label="Public link" />'))->toContain('touch-target')
|
->and((string) $this->blade('<x-toggle aria-label="Public link" />'))->toMatch('/<span data-md-switch\\s+class="md-touch-target"\\s*>/')
|
||||||
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'\']]" />'))->toMatch('/<span data-md-radio-button\\s+class="md-touch-target"\\s*>/');
|
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'\']]" />'))->toMatch('/<span data-md-radio-button\\s+class="md-touch-target"\\s*>/');
|
||||||
|
|
||||||
// A labelled control is pressed anywhere along its row, so it needs no extra 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('md-touch-target')
|
expect((string) $this->blade('<x-checkbox label="Notify me" />'))->not->toContain('md-touch-target')
|
||||||
->and((string) $this->blade('<x-toggle label="Public link" />'))->not->toContain('touch-target')
|
->and((string) $this->blade('<x-toggle label="Public link" />'))->not->toContain('md-touch-target')
|
||||||
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))->not->toContain('md-touch-target');
|
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))->not->toContain('md-touch-target');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,18 +124,20 @@ 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 />');
|
$html = (string) $this->blade('<x-toggle id="public" label="Public link" wire:model.live="public" right />');
|
||||||
|
|
||||||
expect($html)
|
expect($html)
|
||||||
->toContain('<label for="public" data-md-selection-row')
|
->toMatch('/<div data-md-toggle\s*>/')
|
||||||
|
->toMatch('/<label for="public" data-md-selection-row\\s+data-md-right\\s*>/')
|
||||||
->toContain('data-md-switch')
|
->toContain('data-md-switch')
|
||||||
->toContain('role="switch"')
|
->toContain('role="switch"')
|
||||||
->toContain('type="checkbox"')
|
->toContain('type="checkbox"')
|
||||||
->toContain('wire:model.live="public"')
|
->toContain('wire:model.live="public"')
|
||||||
->toContain('data-md-switch-handle')
|
->toContain('data-md-switch-handle')
|
||||||
->toContain('flex-row-reverse')
|
->not->toContain('class=')
|
||||||
->not->toContain('data-md-icons');
|
->not->toContain('data-md-icons');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('puts icons on a switch\'s handle', function () {
|
it('puts icons on a switch\'s handle', function () {
|
||||||
expect((string) $this->blade('<x-toggle label="Wi-Fi" icons />'))->toContain('data-md-icons="both"')->toContain('data-md-switch-on')->toContain('data-md-switch-off')
|
expect((string) $this->blade('<x-toggle label="Wi-Fi" icons />'))->toContain('data-md-icons="both"')->toContain('data-md-switch-on')->toContain('data-md-switch-off')
|
||||||
|
->and((string) $this->blade('<x-toggle label="Wi-Fi" icons />'))->toContain('--md-icon-size: 16px')
|
||||||
->and((string) $this->blade('<x-toggle label="Wi-Fi" icons="selected" />'))->toContain('data-md-icons="selected"');
|
->and((string) $this->blade('<x-toggle label="Wi-Fi" icons="selected" />'))->toContain('data-md-icons="selected"');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,3 +164,10 @@ it('lays the radio group out from its stylesheet, inline only from 600px', funct
|
|||||||
->and((string) $this->blade('<x-radio name="size" class="mt-4" style="order: 1" :options="[[\'id\' => \'s\', \'name\' => \'S\']]" />'))
|
->and((string) $this->blade('<x-radio name="size" class="mt-4" style="order: 1" :options="[[\'id\' => \'s\', \'name\' => \'S\']]" />'))
|
||||||
->toMatch('/data-md-radio\\s+class="mt-4" style="order: 1"/');
|
->toMatch('/data-md-radio\\s+class="mt-4" style="order: 1"/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('draws the switch from its stylesheet', function () {
|
||||||
|
expect((string) file_get_contents(__DIR__.'/../../../resources/css/components/toggle.css'))
|
||||||
|
->toContain("@import './selection.css';")
|
||||||
|
->toMatch('/\\[data-md-switch\\] \\{[^}]*width: 52px;[^}]*height: 32px;/')
|
||||||
|
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/toggle.css';");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user