From a99fbd37941247a436688ac8711b30d7b53a27e1 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 06:00:07 +0200 Subject: [PATCH] Build the theme and contrast rows on the connected button group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mode="picker"` hand-rolled an outlined segmented button, point for point the design M3 Expressive deprecates, and its own arrow keys neither wrapped nor handled Up/Down. Both rows are now : native radios in a fieldset, so the browser supplies the radiogroup, both axes, the wrap and the roving tab stop, and the store is bound through an x-model accessor — a contrast row still marks the resolved level but writes through setContrast(). A new `label` prop legends the row. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-04, N-17). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../livewire-material-development/SKILL.md | 2 +- .../views/components/theme-toggle.blade.php | 109 +++++++++--------- tests/Browser/BarsTest.php | 4 +- tests/Browser/ThemeTest.php | 8 +- tests/Feature/Components/AppBarTest.php | 31 +++-- 5 files changed, 85 insertions(+), 69 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 2f2afe86..a471bf96 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -821,7 +821,7 @@ An avatar that opens a menu: `name`, `email`, `avatar` (image URL or initials; d ### `` -Switches `$store.theme`: `mode="toggle"` (default, light/dark icon button), `cycle` (light → dark → system), `picker` (a row of three for settings pages), `contrast` (the same row for M3's standard, medium and high levels, marking the one in force). Every toggle on a page shares the store. +Switches `$store.theme`: `mode="toggle"` (default, light/dark icon button), `cycle` (light → dark → system), `picker` (a row of three for settings pages), `contrast` (the same row for M3's standard, medium and high levels, marking the one in force). Every toggle on a page shares the store. Both rows are `` — a connected button group over native radios, so the arrow keys, the wrap and the roving tab stop are the browser's; `label` adds a visible legend, and without one the row is still named for a screen reader. Nothing is checked until Alpine has read the store, because the theme is known only in the browser. ### `` diff --git a/resources/views/components/theme-toggle.blade.php b/resources/views/components/theme-toggle.blade.php index a364c132..206762a8 100644 --- a/resources/views/components/theme-toggle.blade.php +++ b/resources/views/components/theme-toggle.blade.php @@ -6,76 +6,79 @@ you — a sun while the page is dark, a moon while it is light — and it is a toggle button, "Dark theme", pressed while dark. - `cycle`: an icon button that steps light → dark → system, showing the choice it is on. - - `picker`: M3's segmented buttons for the three choices, for a settings page. + - `picker`: the three choices as a connected button group, for a settings page. - `contrast`: the same row for M3's three contrast levels — standard, medium (3:1) and high (7:1), the three the scheme is generated in. The row marks the level in force (`resolvedContrast`), so the operating system's setting shows while the choice is `system`, and choosing one is an explicit choice from then on. + The two rows are `` — M3 Expressive's connected button group, the successor of the + segmented button, which the expressive update deprecates. Its segments are native radios, so + the browser supplies the radiogroup semantics, both arrow axes, the wrap and the roving tab + stop; the store is bound through an `x-model` accessor on the wrapper, since the level a + contrast row marks is the resolved one but the level it writes goes through `setContrast()`. + `label` adds a visible legend (a settings page); without one the row is still named for a + screen reader. `class` lands on the button or on the group. + The theme is the visitor's, known only in the browser, so the parts that depend on it wait for - Alpine (`x-cloak`) rather than render a guess. `class` lands on the button or the group. --}} + Alpine (`x-cloak`, an unchecked radio) rather than render a guess. --}} @props([ 'mode' => 'toggle', + 'label' => null, ]) @php $mode = in_array($mode, ['toggle', 'cycle', 'picker', 'contrast'], true) ? $mode : 'toggle'; + + $row = [ + 'picker' => [ + 'name' => __('Theme'), + 'field' => 'material-theme', + 'read' => '$store.theme.choice', + 'write' => '$store.theme.set(value)', + 'options' => [ + ['id' => 'light', 'name' => __('Light'), 'icon' => 'light_mode'], + ['id' => 'dark', 'name' => __('Dark'), 'icon' => 'dark_mode'], + ['id' => 'system', 'name' => __('System'), 'icon' => 'brightness_auto'], + ], + ], + 'contrast' => [ + 'name' => __('Contrast'), + 'field' => 'material-contrast', + 'read' => '$store.theme.resolvedContrast', + 'write' => '$store.theme.setContrast(value)', + 'options' => [ + ['id' => 'standard', 'name' => __('Standard'), 'icon' => 'contrast'], + ['id' => 'medium', 'name' => __('Medium'), 'icon' => 'contrast_circle'], + ['id' => 'high', 'name' => __('High'), 'icon' => 'contrast_square'], + ], + ], + ][$mode] ?? null; @endphp -@if ($mode === 'contrast') +@if ($row !== null)
class(['inline-flex h-10 rounded-corner-full border border-outline']) }} + role="group" + aria-label="{{ $label ?? $row['name'] }}" + data-theme-toggle="{{ $mode }}" + x-data="{ + get chosen() { + return this.{{ $row['read'] }} + }, + set chosen(value) { + this.{{ $row['write'] }} + }, + }" > - @foreach (['standard' => ['Standard', 'contrast'], 'medium' => ['Medium', 'contrast_circle'], 'high' => ['High', 'contrast_square']] as $level => [$text, $icon]) - - @endforeach -
-@elseif ($mode === 'picker') -
class(['inline-flex h-10 rounded-corner-full border border-outline']) }} - > - @foreach (['light' => ['Light', 'light_mode'], 'dark' => ['Dark', 'dark_mode'], 'system' => ['System', 'brightness_auto']] as $choice => [$text, $icon]) - - @endforeach +
@else