diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 54cccead..50782461 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -244,7 +244,7 @@ M3's plain tooltip, standalone around any trigger: ` ``` -``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`. ``: `label`, `icon`, `icon-class` (classes for the leading icon; a colour there paints it, a selected item's too, but not a disabled one's — `icon-class="text-sport-run"`), `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`), `disabled`, `keep-open`. Choosing an item closes the menu unless `keep-open`; a second press on the menu button closes it too. An open menu stays open while the Livewire component around it renders, a `keep-open` item's own `wire:click` included. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab. +``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`. ``: `label`, `icon`, `icon-class` (classes for the leading icon; a colour there paints it, a selected item's too, but not a disabled one's — `icon-class="text-sport-run"`), `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`), `current` (for a menu of places: marks the page you are on with `aria-current="page"` in secondary-container, never a checked choice), `badge` (`true` for a dot, or a count, at the end of the row), `disabled`, `keep-open`. Choosing an item closes the menu unless `keep-open`; a second press on the menu button closes it too. An open menu stays open while the Livewire component around it renders, a `keep-open` item's own `wire:click` included. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab. ### `` @@ -776,7 +776,7 @@ M3 tabs with a server-rendered tablist (arrow keys, Home/End, disabled tabs skip ### `` -Navigation between the sections of one area (settings, admin): secondary tabs as links from `sm` (wrapping onto a grid rather than scrolling), a menu picker below. `items`: `['title', 'url', 'icon', 'active', 'badge']` — current when `active` or its `url` is the page's (during a Livewire update request, the page the component was rendered on, so the section stays lit when a component re-renders). `label`, `no-wire-navigate`. +Navigation between the sections of one area (settings, admin): secondary tabs as links from `sm` (wrapping onto a grid rather than scrolling), a menu picker below, whose items mark the current section as the page (`current`) and carry each section's badge. `items`: `['title', 'url', 'icon', 'active', 'badge']` — current when `active` or its `url` is the page's (during a Livewire update request, the page the component was rendered on, so the section stays lit when a component re-renders). `label`, `no-wire-navigate`. ### `` diff --git a/resources/views/components/menu-item.blade.php b/resources/views/components/menu-item.blade.php index 3c37ba77..43a46ed7 100644 --- a/resources/views/components/menu-item.blade.php +++ b/resources/views/components/menu-item.blade.php @@ -4,7 +4,10 @@ under the label and a `shortcut` at the end (M3's trailing supporting text: "⌘C"). `link` makes it an anchor, with `wire:navigate` unless `external` or `no-wire-navigate`. `selected` (true or false) makes it a `menuitemcheckbox` with `aria-checked`; a selected item takes - Expressive's selected shape and tertiary-container. `disabled` keeps it in the list, out of + Expressive's selected shape and tertiary-container. `current` is for a menu of places rather + than choices — a section picker — and marks the page you are on: `aria-current="page"`, the + selected shape in secondary-container, the colour M3 gives the navigation indicator. `badge` + draws `` at the end of the row: `true` for a dot, or a count. `disabled` keeps it in the list, out of reach. `keep-open` leaves the menu open when it is activated — for a choice the person may want to change twice. @@ -28,6 +31,8 @@ 'external' => false, 'noWireNavigate' => false, 'selected' => null, + 'current' => false, + 'badge' => null, 'disabled' => false, 'keepOpen' => false, ]) @@ -44,11 +49,13 @@ 'focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-secondary', 'py-2' => filled($description), 'rounded-corner-md bg-tertiary-container text-on-tertiary-container' => $selected === true, + 'rounded-corner-md bg-secondary-container text-on-secondary-container' => $current && $selected !== true, 'pointer-events-none text-on-surface/38' => $disabled, ]) ->merge(array_filter([ 'role' => $selected === null ? 'menuitem' : 'menuitemcheckbox', 'aria-checked' => $selected === null ? null : ($selected ? 'true' : 'false'), + 'aria-current' => $current ? 'page' : null, 'aria-disabled' => $disabled ? 'true' : null, 'tabindex' => '-1', 'type' => $isLink ? null : 'button', @@ -62,6 +69,7 @@ $iconInk = match (true) { $disabled => 'text-on-surface/38', $selected === true => 'text-on-tertiary-container', + $current => 'text-on-secondary-container', default => 'text-on-surface-variant', }; @@ -69,13 +77,14 @@ blank($iconClass) => 'size-5 '.$iconInk, $disabled => \Illuminate\Support\Arr::toCssClasses(['size-5', $iconClass, 'text-on-surface/38!']), $selected === true => \Illuminate\Support\Arr::toCssClasses(['size-5 [:where(&)]:text-on-tertiary-container', $iconClass]), + $current => \Illuminate\Support\Arr::toCssClasses(['size-5 [:where(&)]:text-on-secondary-container', $iconClass]), default => \Illuminate\Support\Arr::toCssClasses(['size-5 [:where(&)]:text-on-surface-variant', $iconClass]), }; @endphp <{{ $tag }} {{ $attributes }}> @if ($icon) - + @endif @@ -86,6 +95,9 @@ @endif + @if ($badge !== null && $badge !== false && $badge !== '') + + @endif @if ($shortcut) {{ $shortcut }} @endif diff --git a/resources/views/components/section-nav.blade.php b/resources/views/components/section-nav.blade.php index 0ecf1362..0a5baa78 100644 --- a/resources/views/components/section-nav.blade.php +++ b/resources/views/components/section-nav.blade.php @@ -4,7 +4,8 @@ `items`, a list of `['title' => …, 'url' => …]` with an optional `icon`, `active` and `badge` (an item is current when `active` is true, or when its `url` is the page's). From `sm` they are M3's secondary tabs as links, the current one underlined; below `sm`, where a row of them - never fits, a button naming the current section opens a menu of all of them. The same list is + never fits, a button naming the current section opens a menu of all of them, the current one + marked `aria-current="page"` and each with its badge. The same list is rendered for both, and CSS shows one. The page's URL is `Livewire::originalUrl()`: while a Livewire component on the page updates, the @@ -49,7 +50,7 @@ @foreach ($items as $item) - + @endforeach diff --git a/tests/Feature/Components/MenuTest.php b/tests/Feature/Components/MenuTest.php index 07d8ae82..cf58b0f8 100644 --- a/tests/Feature/Components/MenuTest.php +++ b/tests/Feature/Components/MenuTest.php @@ -53,6 +53,20 @@ it('makes a selectable item a menuitemcheckbox', function () { ->toContain('aria-checked="false"'); }); +it('marks the page an item leads to, and carries a badge', function () { + $html = (string) $this->blade(''); + + expect($html) + ->toContain('role="menuitem"') + ->toContain('aria-current="page"') + ->not->toContain('aria-checked') + ->toContain('bg-secondary-container text-on-secondary-container') + ->toContain('>3<') + ->and((string) $this->blade('')) + ->not->toContain('aria-current') + ->not->toContain('secondary-container'); +}); + it('links an item, and keeps a disabled one out of reach', function () { $this->blade('') ->assertSee('href="/settings"', false) diff --git a/tests/Feature/Components/TabsTest.php b/tests/Feature/Components/TabsTest.php index 5b6c3838..9d1e3b97 100644 --- a/tests/Feature/Components/TabsTest.php +++ b/tests/Feature/Components/TabsTest.php @@ -67,7 +67,11 @@ it('draws section navigation as secondary tabs and a picker', function () { ->toContain('sm:flex') ->toMatch('/href="\/settings\/security"\s+data-tab\s+aria-current="page"\s+wire:navigate/') ->not->toMatch('/href="\/settings\/profile"\s+data-tab\s+aria-current/') - ->toContain('role="menuitemcheckbox"'); + // The picker is a menu of places: the current one is the page, not a checked choice, + // and a section's badge shows there as well as on its tab. + ->not->toContain('role="menuitemcheckbox"') + ->toMatch('/role="menuitem"[^>]*aria-current="page"[^>]*href="\/settings\/security"/') + ->toMatch('/data-section-picker.*Security.*>\s*1\s*<.*