Plan step 36. <x-group> renders data-md-group with its size, variant, shape, multiple and inline, a data-md-button-group row and data-md-group-segment labels over its radios or checkboxes. group.css draws the segments as M3's buttons with the toggle button's colours from :has(:checked), the disabled treatment from :has(:disabled), the focus ring from :has(:focus-visible) and the 48px target at xs and sm. The connected shapes move into button-group.css (the corner scale, square ends, the pressed and selected inner corners), which group.css imports; <x-button-group> adopts them in its own rewrite. The group's render tests move to GroupTest; AppBarTest (navigation group) follows the new legend and row hooks of theme-toggle's rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
107 lines
4.9 KiB
PHP
107 lines
4.9 KiB
PHP
{{-- A choice of a few options as an M3 Expressive connected button group — the successor of the
|
|
segmented button.
|
|
|
|
<x-group label="Theme" wire:model.live="theme" :options="[
|
|
['id' => 'light', 'name' => 'Light', 'icon' => 'light_mode'],
|
|
['id' => 'dark', 'name' => 'Dark', 'icon' => 'dark_mode'],
|
|
]" />
|
|
|
|
Native radios under the segments (checkboxes with `multiple`), so `wire:model` and `x-model`
|
|
bind as on any input, the arrow keys move the choice, and a screen reader announces a group.
|
|
The chosen segment rounds fully and takes the selected colour; `variant` is `tonal` (the
|
|
default), `filled` or `outlined`, as for toggle buttons. The segments share the row unless
|
|
`inline`. An option with `'disabled' => true` greys its own segment. At `xs` and `sm` a
|
|
segment keeps a 48px target and a 48px minimum width, which M3 asks for by name and tells
|
|
you never to reduce. Corners move on the spatial spring and colours on the effects one.
|
|
|
|
`shape` is M3's "Default shape | Round, square" configuration: `square` squares the two ends
|
|
of the group to the corner its inner edges take (M3's square table, 4/8/8/16/20dp by size),
|
|
and the chosen segment still rounds fully, so selection reads the same either way.
|
|
|
|
ReStride's props, kept: `label`, `hint`, `hint-class`, `name` (needed with `x-model`, which
|
|
names no property), `options`, `option-value`, `option-label`; plus `option-icon`, `size`,
|
|
`variant`, `shape`, `multiple`, `inline`. A validation message for the bound property replaces
|
|
the hint.
|
|
|
|
`hint-class` adds classes to the hint, as on `<x-field>`: a colour there paints it
|
|
(`hint-class="md-ink-warning"` for a hint that warns), because a caller's class outranks the
|
|
package's layer.
|
|
|
|
Drawn by resources/css/components/group.css, with the connected shapes of
|
|
resources/css/components/button-group.css. --}}
|
|
|
|
@props([
|
|
'label' => null,
|
|
'hint' => null,
|
|
'hintClass' => null,
|
|
'name' => null,
|
|
'options' => [],
|
|
'optionValue' => 'id',
|
|
'optionLabel' => 'name',
|
|
'optionIcon' => 'icon',
|
|
'size' => 'sm',
|
|
'variant' => 'tonal',
|
|
'shape' => 'round',
|
|
'multiple' => false,
|
|
'inline' => false,
|
|
])
|
|
|
|
@php
|
|
$model = $attributes->whereStartsWith('wire:model')->first();
|
|
$name ??= $model;
|
|
// 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)) : [];
|
|
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
|
$variant = in_array($variant, ['tonal', 'filled', 'outlined'], true) ? $variant : 'tonal';
|
|
// M3's "Default shape | Round, square": a square group's ends take the corner its inner edges
|
|
// take; the chosen segment still rounds fully, which is the cue selection has always carried.
|
|
$shape = $shape === 'square' ? 'square' : 'round';
|
|
|
|
// A 20px glyph comes from M3's 20px cut, which the icon component picks from the size.
|
|
$iconSize = ['xs' => 20, 'sm' => 20, 'md' => 24, 'lg' => 32, 'xl' => 40][$size];
|
|
|
|
$root = $attributes->only(['class', 'wire:key'])->merge(array_filter([
|
|
'data-md-group' => true,
|
|
'data-md-size' => $size,
|
|
'data-md-variant' => $variant,
|
|
'data-md-shape' => $shape,
|
|
'data-md-multiple' => $multiple ? true : null,
|
|
'data-md-inline' => $inline ? true : null,
|
|
], fn ($value): bool => $value !== null));
|
|
@endphp
|
|
|
|
<fieldset {{ $root }}>
|
|
@if (filled($label))
|
|
<legend data-md-group-legend>{{ $label }}</legend>
|
|
@endif
|
|
|
|
<div data-md-button-group="connected" data-md-size="{{ $size }}" data-md-shape="{{ $shape }}">
|
|
@foreach ($options as $option)
|
|
<label data-md-group-segment>
|
|
<input
|
|
{{ $attributes->whereStartsWith(['wire:model', 'x-model']) }}
|
|
type="{{ $multiple ? 'checkbox' : 'radio' }}"
|
|
name="{{ $multiple ? $name.'[]' : $name }}"
|
|
value="{{ data_get($option, $optionValue) }}"
|
|
@disabled(data_get($option, 'disabled'))
|
|
/>
|
|
|
|
@if (filled(data_get($option, $optionIcon)))
|
|
<x-livewire-material::icon :name="data_get($option, $optionIcon)" :size="$iconSize" />
|
|
@endif
|
|
|
|
<span data-md-group-label>{{ data_get($option, $optionLabel) }}</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
|
|
@if ($messages !== [])
|
|
@foreach ($messages as $message)
|
|
<p data-md-group-error>{{ $message }}</p>
|
|
@endforeach
|
|
@elseif (filled($hint))
|
|
<p data-md-group-hint @if (filled($hintClass)) class="{{ $hintClass }}" @endif>{{ $hint }}</p>
|
|
@endif
|
|
</fieldset>
|