diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 2f879826..4b9b8380 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -766,7 +766,7 @@ M3 Expressive's navigation rail: collapsed (96px, icon over label) or expanded ( ``` - `mode`: `collapsed`, `expanded`, `collapsible` (default: expanded until its menu button collapses it; the choice is `$store.rail`, remembered and applied before the first paint), `modal` (collapsed in the layout; the menu button or `$store.rail.show()` opens it expanded over a scrim, focus held until Escape, the scrim or leaving the page), `adaptive` (``'s, one rail per window size class: hidden and opened as a modal on a compact window, collapsed and opened as a modal at `medium`, a standard rail from `expanded` — collapsed there, expanded from `large`). -- Props: `label` ("Main"), `width` (expanded width, `16rem`, held between 220 and 360px), `menu` (the menu button; on by default for `collapsible`, `modal`, `adaptive`), `divider` (M3's optional vertical divider on the page's side — use it when the page scrolls under a fixed rail), `fill` (`false` for a transparent container, which M3 allows while the items keep 3:1 contrast). Slots: `brand` (beside the menu button, expanded only), `header` (one ``, which the rail morphs into an extended FAB and back as it expands — it also rests at elevation 0, as M3 asks of a nested FAB), the destinations (the only part that scrolls), `footer`. In a flex row the rail sticks to the top of the viewport. +- Props: `label` ("Main"), `width` (expanded width, `16rem`, held between 220 and 360px — or the word `narrow` for M3's other *collapsed* width, 80px against the default 96, where the items are their icons alone; the labels stay in the accessibility tree and a narrow rail still expands to 16rem), `menu` (the menu button; on by default for `collapsible`, `modal`, `adaptive`), `divider` (M3's optional vertical divider on the page's side — use it when the page scrolls under a fixed rail), `fill` (`false` for a transparent container, which M3 allows while the items keep 3:1 contrast). Slots: `brand` (beside the menu button, expanded only), `header` (one ``, which the rail morphs into an extended FAB and back as it expands — it also rests at elevation 0, as M3 asks of a nested FAB), the destinations (the only part that scrolls), `footer`. In a flex row the rail sticks to the top of the viewport. - Anything else inside a rail takes both shapes with the `rail-collapsed:` variant, true while that rail is drawn collapsed for whatever reason: `…expanded only…`, ``. Put the variant on a wrapper, never on a component. Nothing that shows while collapsed may be wider than 96px. - A `collapsible` rail is held to the collapsed 96px below `medium` (600px), where M3 says to use a navigation bar rather than a standard rail. `collapsed` and `expanded` are fixed-width by design: wrap one in a `medium:` element if it must not show on a phone. - ``: the same props as ``. ``: a group with a heading that shows only while the rail is expanded; it names the group for screen readers either way. diff --git a/resources/css/components/navigation.css b/resources/css/components/navigation.css index 92ce57bf..9bf3bcb8 100644 --- a/resources/css/components/navigation.css +++ b/resources/css/components/navigation.css @@ -221,25 +221,49 @@ [data-navigation-rail] { --navigation-rail-expanded-width: clamp(13.75rem, var(--navigation-rail-width, 16rem), 22.5rem); + --navigation-rail-collapsed-width: 6rem; position: relative; flex-shrink: 0; width: var(--navigation-rail-expanded-width); transition: width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default); @variant rail-collapsed { - width: 6rem; + width: var(--navigation-rail-collapsed-width); + } + } + + /* M3's other collapsed width: NavigationRailCollapsedTokens.NarrowContainerWidth, 80dp against + the default 96. A variable rather than a width here, so the rules below — which each have + their own reason to name a collapsed rail's width — do not have to know about it. */ + [data-navigation-rail][data-width='narrow'] { + --navigation-rail-collapsed-width: 5rem; + } + + /* There is no room for a label under the 56px indicator at 80px, so a narrow rail is the icons + alone. The label is taken out of the drawing, not out of the page: it is what names the + destination for a screen reader. */ + [data-navigation-rail][data-width='narrow'] [data-navigation-rail-item] [data-navigation-label] { + @variant rail-collapsed { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; } } /* A modal rail keeps its collapsed width in the layout while it is open over it, as Compose's ModalWideNavigationRail does; the adaptive rail does below `expanded`, and takes no room on a compact window. */ [data-navigation-rail='modal'] { - width: 6rem; + width: var(--navigation-rail-collapsed-width); } @media (width < 52.5rem) { [data-navigation-rail='adaptive'] { - width: 6rem; + width: var(--navigation-rail-collapsed-width); } } @@ -250,10 +274,10 @@ /* "Compact → don't use a standard rail, use a navigation bar." A `collapsible` rail takes the width the visitor chose, which on a 360px screen would be two-thirds of it, so it - is floored at the collapsed 96px. `collapsed` and `expanded` mean what they say and are + is floored at its collapsed width. `collapsed` and `expanded` mean what they say and are left alone: wrap one in a `medium:` element if it must not show on a phone. */ [data-navigation-rail='collapsible'] { - width: 6rem; + width: var(--navigation-rail-collapsed-width); } } diff --git a/resources/views/components/navigation-rail.blade.php b/resources/views/components/navigation-rail.blade.php index 78bb26e2..a96cf67f 100644 --- a/resources/views/components/navigation-rail.blade.php +++ b/resources/views/components/navigation-rail.blade.php @@ -52,7 +52,11 @@ Nothing that shows while collapsed may be wider than 96px. Props: `label` names the landmark ("Main"); `width` is the expanded width (`16rem`, held - between M3's 220 and 360dp); `menu` shows the menu button (by default for `collapsible`, + between M3's 220 and 360dp) — or the word `narrow`, M3's other *collapsed* width + (NavigationRailCollapsedTokens.NarrowContainerWidth, 80px against the default 96), where the + items are their icons alone because no label fits under a 56px indicator at that width; the + labels stay in the accessibility tree, since they are what name the destinations, and a + narrow rail expands to the default 16rem; `menu` shows the menu button (by default for `collapsible`, `modal` and `adaptive`); `divider` draws M3's optional vertical divider on the edge the page is on — which is also what M3 asks for when a page scrolls underneath a fixed rail; `fill` (`false`) drops the container colour for a transparent rail over the page's own background, @@ -85,10 +89,13 @@ $canOpen = in_array($mode, ['modal', 'adaptive'], true); $menu ??= $interactive; $collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true); + $narrow = $width === 'narrow'; + $width = $narrow ? '16rem' : $width; @endphp
@if ($menu || isset($brand)) -
$menu, 'ps-5' => ! $menu])> + {{-- The menu button is centred on the collapsed rail: 28px either side of a + 40px button in 96, 20px in a narrow 80. --}} +
$menu, 'rail-collapsed:ps-5' => $menu && $narrow, 'ps-5' => ! $menu])> @if ($menu)
+ {{-- And so is a 56px FAB: 20px either side in 96, 12px in a narrow 80. --}} +
attributes->class(['flex w-full flex-col items-start gap-2 px-5', 'rail-collapsed:px-3' => $narrow]) }}>{{ $header }}
@endisset
@endif diff --git a/resources/views/showcase/sections/navigation.blade.php b/resources/views/showcase/sections/navigation.blade.php index 1cf8e4e9..d47a3ee2 100644 --- a/resources/views/showcase/sections/navigation.blade.php +++ b/resources/views/showcase/sections/navigation.blade.php @@ -58,7 +58,7 @@
BLADE, - 'Collapsed and expanded rails' => <<<'BLADE' + 'Collapsed, narrow and expanded rails' => <<<'BLADE'
@@ -80,6 +80,22 @@
+
+ + + + + + + + + + + + + +
+
diff --git a/tests/Feature/Components/NavigationRailTest.php b/tests/Feature/Components/NavigationRailTest.php index db4b3f3c..2f073c51 100644 --- a/tests/Feature/Components/NavigationRailTest.php +++ b/tests/Feature/Components/NavigationRailTest.php @@ -63,8 +63,32 @@ it('takes M3\'s optional divider and turns the container fill off', function () // Neither reaches a rail open over a scrim, which is a surface over the page (N-22). ->toContain('[data-navigation-rail][data-divider]:not([data-open]) > [data-navigation-rail-panel] {') ->toContain("[data-navigation-rail][data-fill='false']:not([data-open]) > [data-navigation-rail-panel] {") - // A collapsible rail is held to 96px where M3 asks for a navigation bar instead (N-24). - ->toMatch("/\\[data-navigation-rail='collapsible'\\] \\{\\s+width: 6rem;/"); + // A collapsible rail is held to its collapsed width where M3 asks for a bar instead (N-24). + ->toMatch("/\\[data-navigation-rail='collapsible'\\] \\{\\s+width: var\\(--navigation-rail-collapsed-width\\);/"); +}); + +it('takes M3\'s narrow collapsed width, icons alone but still named', function () { + $narrow = (string) $this->blade(''); + + expect($narrow) + ->toContain('data-width="narrow"') + // A narrow rail expands to the same 16rem; `width` names one or the other, never both. + ->toContain('--navigation-rail-width: 16rem') + // 40px menu button and 56px FAB centred in 80px rather than in 96. + ->toContain('rail-collapsed:ps-5') + ->toContain('rail-collapsed:px-3') + // The label is still rendered: it is what names the destination. + ->toContain('Inbox') + ->and((string) $this->blade('')) + ->not->toContain('data-width') + ->toContain('--navigation-rail-width: 20rem'); + + expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css')) + // NavigationRailCollapsedTokens.NarrowContainerWidth, 80dp against the default 96dp. + ->toMatch("/\\[data-navigation-rail\\]\\[data-width='narrow'\\] \\{\\s+--navigation-rail-collapsed-width: 5rem;/") + ->toMatch('/--navigation-rail-collapsed-width: 6rem;/') + // Out of the drawing, not out of the page. + ->toContain("[data-navigation-rail][data-width='narrow'] [data-navigation-rail-item] [data-navigation-label] {"); }); it('flattens a FAB nested in the rail header and morphs its label', function () {