diff --git a/resources/css/components/radio.css b/resources/css/components/radio.css index 15ffe431..67b201ff 100644 --- a/resources/css/components/radio.css +++ b/resources/css/components/radio.css @@ -5,9 +5,20 @@ * RadioButtonTokens (androidx Compose Material 3, Apache-2.0): a 20px ring with a 2px * on-surface-variant outline, primary once chosen, and a 10px primary dot that grows in on the fast * spatial spring; the state layer a 40px circle around it. With the server's error the ring and - * the dot are error. Disabled, both are on-surface at 38%. + * the dot are error. Disabled, both are on-surface at 38%. Beside a hint the ring drops 2px, to sit + * on the label's first line. * - * [data-md-radio-button] the ring: input, [data-md-radio-dot] + * The group is a
whose legend (label-large, on-surface-variant, 12px above the options) + * is the question. The options stack 16px apart, as M3 lays radio buttons out; `data-md-inline` + * lays them in a wrapping row 24px apart from `medium` (600px), and compact always stacks. The + * hint, or the errors in its place, sits 8px under the options. + * + * [data-md-radio] the
; `class` and `style` land here; data-md-inline + * [data-md-radio-legend] + * [data-md-radio-options] + * [data-md-selection-row] + * [data-md-radio-button] the ring: input, [data-md-radio-dot] + * [data-md-selection-support] the hint, or the errors */ @layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; @@ -15,6 +26,45 @@ @import './selection.css'; @layer material.components { + [data-md-radio] { + min-width: 0; + } + + [data-md-radio-legend] { + margin-bottom: 12px; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-label-lg); + letter-spacing: var(--md-sys-typescale-label-lg-tracking); + font-variation-settings: normal; + } + + [data-md-radio-options] { + display: grid; + gap: var(--md-sys-measurement-space200); + } + + /* M3 stacks radio buttons and cautions against a row at any width; `inline` is for two or three + short labels, and only from medium. */ + @media (width >= 600px) { + [data-md-radio][data-md-inline] [data-md-radio-options] { + display: flex; + flex-wrap: wrap; + column-gap: var(--md-sys-measurement-space300); + } + } + + [data-md-radio] [data-md-selection-support] { + margin-top: var(--md-sys-measurement-space100); + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-body-sm); + letter-spacing: var(--md-sys-typescale-body-sm-tracking); + font-variation-settings: normal; + } + + [data-md-radio] [data-md-selection-support][role='alert'] { + color: var(--md-sys-color-error); + } + [data-md-radio-button] { position: relative; display: grid; @@ -28,6 +78,10 @@ grid-area: 1 / 1; } + [data-md-selection-row]:has([data-md-selection-hint]) [data-md-radio-button] { + margin-top: var(--md-sys-measurement-space25); + } + [data-md-radio-button]::before { inset: -10px; } diff --git a/resources/views/components/radio.blade.php b/resources/views/components/radio.blade.php index e7404785..e11bac8c 100644 --- a/resources/views/components/radio.blade.php +++ b/resources/views/components/radio.blade.php @@ -35,28 +35,23 @@ @endphp
only(['class', 'wire:key'])->class(['min-w-0']) }} + data-md-radio + @if ($inline) data-md-inline @endif + {{ $attributes->only(['class', 'style', 'wire:key']) }} @if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif > @if (filled($label)) - {{ $label }} + {{ $label }} @endif -
$inline])> +
@foreach ($options as $option) @php($optionHintText = data_get($option, $optionHint)) -
@if ($messages !== []) -
diff --git a/tests/Feature/Components/SelectionTest.php b/tests/Feature/Components/SelectionTest.php index 61a3dbbc..c0c95ba5 100644 --- a/tests/Feature/Components/SelectionTest.php +++ b/tests/Feature/Components/SelectionTest.php @@ -31,12 +31,12 @@ it('marks a checkbox that is partly ticked', function () { it('gives a selection control without a label a 48px target', function () { expect((string) $this->blade(''))->toMatch('//') ->and((string) $this->blade(''))->toContain('touch-target') - ->and((string) $this->blade(''))->toContain('touch-target'); + ->and((string) $this->blade(''))->toMatch('//'); // A labelled control is pressed anywhere along its row, so it needs no extra target. expect((string) $this->blade(''))->not->toContain('md-touch-target') ->and((string) $this->blade(''))->not->toContain('touch-target') - ->and((string) $this->blade(''))->not->toContain('touch-target'); + ->and((string) $this->blade(''))->not->toContain('md-touch-target'); }); it('leaves a checkbox group to the caller and says how M3 wants it laid out', function () { @@ -89,8 +89,11 @@ it('draws radio buttons in a fieldset named by its question', function () { BLADE); expect($html) - ->toContain('toContain('Who can open it') + ->toMatch('/toContain('Who can open it') + ->toContain('
') + ->toContain('Share it separately') + ->not->toContain('class=') ->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) @@ -103,7 +106,7 @@ it('names unbound radios after their group and checks the given value', function $html = (string) $this->blade(''); expect($html) - ->toContain('medium:flex') + ->toMatch('/and(substr_count($html, 'name="theme"'))->toBe(2) ->and($html)->toMatch('/value="dark"\s+checked/') ->not->toMatch('/value="light"\s+checked/'); @@ -113,6 +116,7 @@ it('shows the errors of a radio group', function () { expect((string) $this->withViewErrors(['audience' => ['Choose who can open it.']]) ->blade('')) ->toContain('data-md-invalid') + ->toMatch('/