<x-checkbox> renders data-md-checkbox, its box, tick and dash, and its row, text and errors as data-md-* attributes; the 18px tick takes a size prop and a label-less box md-touch-target (plan step 36). selection.css moves into material.components as the row, text and state layer the three controls share, and each control's drawing goes to its own file: checkbox.css, radio.css, toggle.css, on the state tokens. The radio and the switch take the renamed shared hooks now and lose their layout classes in their own commits. field.js follows data-md-indeterminate. Renaming the switch's data-handle also stops selection.css matching the slider's handles, which it drew 24px above the track. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
90 lines
4.3 KiB
PHP
90 lines
4.3 KiB
PHP
{{-- One choice from a few, each with room for a line of explanation: M3's radio buttons.
|
|
|
|
A `<fieldset>` whose legend is the question, so a screen reader reads the group's name with each
|
|
option, and native radios sharing a name, so the arrow keys move between them. `options` as
|
|
`['id' => …, 'name' => …, 'hint' => …]` (`'disabled' => true` greys one out), `option-value`,
|
|
`option-label` and `option-hint` to read other keys, `inline` to lay them in a row from `medium`
|
|
(600px, M3's compact/medium boundary — compact always stacks), and
|
|
`value` for the option chosen when the page loads (with `wire:model`, Livewire sets it).
|
|
Errors are read from the bag under the `wire:model` name
|
|
(resources/css/components/radio.css).
|
|
|
|
M3 stacks radio buttons vertically and cautions against a row at any width, so `inline` is for
|
|
a group of two or three short labels that would waste a column, not the shape to reach for.
|
|
It also asks for five options or fewer and for one of them to be chosen when the page loads:
|
|
give the group a `value` (or a bound property that already holds one). --}}
|
|
|
|
@props([
|
|
'label' => null,
|
|
'hint' => null,
|
|
'options' => [],
|
|
'optionValue' => 'id',
|
|
'optionLabel' => 'name',
|
|
'optionHint' => 'hint',
|
|
'inline' => false,
|
|
'value' => null,
|
|
])
|
|
|
|
@php
|
|
$model = $attributes->whereStartsWith('wire:model')->first();
|
|
$name = $attributes->get('name') ?? $model ?? 'radio-'.substr(md5($label.'|'.json_encode($options)), 0, 12);
|
|
$id = 'radio-'.substr(md5($name.'|'.$label), 0, 12);
|
|
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
|
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
|
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
|
@endphp
|
|
|
|
<fieldset
|
|
{{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}
|
|
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
|
>
|
|
@if (filled($label))
|
|
<legend class="mb-3 type-label-lg text-on-surface-variant">{{ $label }}</legend>
|
|
@endif
|
|
|
|
<div @class(['grid gap-4', 'medium:flex medium:flex-wrap medium:gap-x-6' => $inline])>
|
|
@foreach ($options as $option)
|
|
@php($optionHintText = data_get($option, $optionHint))
|
|
|
|
<label data-md-selection-row @if ($messages !== []) data-md-invalid @endif @class([
|
|
'flex gap-4',
|
|
'items-start' => filled($optionHintText),
|
|
'items-center' => blank($optionHintText),
|
|
])>
|
|
<span data-md-radio-button @class([
|
|
'mt-0.5' => filled($optionHintText),
|
|
'touch-target' => blank(data_get($option, $optionLabel)) && blank($optionHintText),
|
|
])>
|
|
<input
|
|
{{ $attributes->except(['class', 'wire:key', 'name', 'id']) }}
|
|
type="radio"
|
|
name="{{ $name }}"
|
|
value="{{ data_get($option, $optionValue) }}"
|
|
@disabled(data_get($option, 'disabled'))
|
|
@checked($value !== null && (string) $value === (string) data_get($option, $optionValue))
|
|
@if ($messages !== []) aria-invalid="true" @endif
|
|
/>
|
|
<span data-md-radio-dot></span>
|
|
</span>
|
|
|
|
<span class="min-w-0">
|
|
<span class="block type-body-lg text-on-surface" data-md-selection-label>{{ data_get($option, $optionLabel) }}</span>
|
|
@if (filled($optionHintText))
|
|
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $optionHintText }}</span>
|
|
@endif
|
|
</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
|
|
@if ($messages !== [])
|
|
<div id="{{ $id }}-support" class="mt-2 type-body-sm text-error" role="alert">
|
|
@foreach ($messages as $message)
|
|
<p>{{ $message }}</p>
|
|
@endforeach
|
|
</div>
|
|
@elseif (filled($hint))
|
|
<p id="{{ $id }}-support" class="mt-2 type-body-sm text-on-surface-variant">{{ $hint }}</p>
|
|
@endif
|
|
</fieldset>
|