Add the rail's narrow collapsed width
Plan step 25 (navigation Missing): NavigationRailCollapsedTokens .NarrowContainerWidth, 80dp, had no way in — only the default 96. `<x-navigation-rail width="narrow">` takes it; the collapsed width is now a variable, so the four rules that each have their own reason to name it follow without a specificity fight. No label fits under a 56px indicator at 80px, so a narrow rail's items are their icons alone, with the label taken out of the drawing and left in the accessibility tree, where it names the destination. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ce1cd6eec0
commit
cef350d739
@@ -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` (`<x-app-shell>`'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`).
|
- `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` (`<x-app-shell>`'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 `<x-fab label icon>`, 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 `<x-fab label icon>`, 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: `<span class="rail-collapsed:hidden">…expanded only…</span>`, `<span class="hidden rail-collapsed:inline-flex">…collapsed only…</span>`. Put the variant on a wrapper, never on a component. Nothing that shows while collapsed may be wider than 96px.
|
- Anything else inside a rail takes both shapes with the `rail-collapsed:` variant, true while that rail is drawn collapsed for whatever reason: `<span class="rail-collapsed:hidden">…expanded only…</span>`, `<span class="hidden rail-collapsed:inline-flex">…collapsed only…</span>`. 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.
|
- 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.
|
||||||
- `<x-navigation-rail-item>`: the same props as `<x-navigation-bar-item>`. `<x-navigation-rail-section label="…">`: a group with a heading that shows only while the rail is expanded; it names the group for screen readers either way.
|
- `<x-navigation-rail-item>`: the same props as `<x-navigation-bar-item>`. `<x-navigation-rail-section label="…">`: a group with a heading that shows only while the rail is expanded; it names the group for screen readers either way.
|
||||||
|
|||||||
@@ -221,25 +221,49 @@
|
|||||||
|
|
||||||
[data-navigation-rail] {
|
[data-navigation-rail] {
|
||||||
--navigation-rail-expanded-width: clamp(13.75rem, var(--navigation-rail-width, 16rem), 22.5rem);
|
--navigation-rail-expanded-width: clamp(13.75rem, var(--navigation-rail-width, 16rem), 22.5rem);
|
||||||
|
--navigation-rail-collapsed-width: 6rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: var(--navigation-rail-expanded-width);
|
width: var(--navigation-rail-expanded-width);
|
||||||
transition: width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
transition: width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||||
|
|
||||||
@variant rail-collapsed {
|
@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
|
/* 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. */
|
ModalWideNavigationRail does; the adaptive rail does below `expanded`, and takes no room on a compact window. */
|
||||||
[data-navigation-rail='modal'] {
|
[data-navigation-rail='modal'] {
|
||||||
width: 6rem;
|
width: var(--navigation-rail-collapsed-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width < 52.5rem) {
|
@media (width < 52.5rem) {
|
||||||
[data-navigation-rail='adaptive'] {
|
[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
|
/* "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
|
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. */
|
left alone: wrap one in a `medium:` element if it must not show on a phone. */
|
||||||
[data-navigation-rail='collapsible'] {
|
[data-navigation-rail='collapsible'] {
|
||||||
width: 6rem;
|
width: var(--navigation-rail-collapsed-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,11 @@
|
|||||||
Nothing that shows while collapsed may be wider than 96px.
|
Nothing that shows while collapsed may be wider than 96px.
|
||||||
|
|
||||||
Props: `label` names the landmark ("Main"); `width` is the expanded width (`16rem`, held
|
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
|
`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`
|
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,
|
(`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);
|
$canOpen = in_array($mode, ['modal', 'adaptive'], true);
|
||||||
$menu ??= $interactive;
|
$menu ??= $interactive;
|
||||||
$collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true);
|
$collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true);
|
||||||
|
$narrow = $width === 'narrow';
|
||||||
|
$width = $narrow ? '16rem' : $width;
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-navigation-rail="{{ $mode }}"
|
data-navigation-rail="{{ $mode }}"
|
||||||
|
@if ($narrow) data-width="narrow" @endif
|
||||||
@if ($divider) data-divider @endif
|
@if ($divider) data-divider @endif
|
||||||
@unless ($fill) data-fill="false" @endunless
|
@unless ($fill) data-fill="false" @endunless
|
||||||
@if ($interactive)
|
@if ($interactive)
|
||||||
@@ -112,7 +119,9 @@
|
|||||||
@if ($menu || isset($brand) || isset($header))
|
@if ($menu || isset($brand) || isset($header))
|
||||||
<div data-navigation-rail-header>
|
<div data-navigation-rail-header>
|
||||||
@if ($menu || isset($brand))
|
@if ($menu || isset($brand))
|
||||||
<div @class(['flex w-full min-w-0 items-center gap-3 pe-5', 'ps-7' => $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. --}}
|
||||||
|
<div @class(['flex w-full min-w-0 items-center gap-3 pe-5', 'ps-7' => $menu, 'rail-collapsed:ps-5' => $menu && $narrow, 'ps-5' => ! $menu])>
|
||||||
@if ($menu)
|
@if ($menu)
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -136,7 +145,8 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
@isset($header)
|
@isset($header)
|
||||||
<div {{ $header->attributes->class(['flex w-full flex-col items-start gap-2 px-5']) }}>{{ $header }}</div>
|
{{-- And so is a 56px FAB: 20px either side in 96, 12px in a narrow 80. --}}
|
||||||
|
<div {{ $header->attributes->class(['flex w-full flex-col items-start gap-2 px-5', 'rail-collapsed:px-3' => $narrow]) }}>{{ $header }}</div>
|
||||||
@endisset
|
@endisset
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
BLADE,
|
BLADE,
|
||||||
'Collapsed and expanded rails' => <<<'BLADE'
|
'Collapsed, narrow and expanded rails' => <<<'BLADE'
|
||||||
<div class="flex w-full flex-wrap items-start gap-6">
|
<div class="flex w-full flex-wrap items-start gap-6">
|
||||||
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
|
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||||
<x-navigation-rail mode="collapsed" label="Collapsed example" divider>
|
<x-navigation-rail mode="collapsed" label="Collapsed example" divider>
|
||||||
@@ -80,6 +80,22 @@
|
|||||||
</x-navigation-rail>
|
</x-navigation-rail>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||||
|
<x-navigation-rail mode="collapsed" width="narrow" label="Narrow example" divider>
|
||||||
|
<x-slot:header>
|
||||||
|
<x-fab label="Compose" icon="edit" />
|
||||||
|
</x-slot:header>
|
||||||
|
|
||||||
|
<x-navigation-rail-item label="Inbox" icon="inbox" link="#navigation" no-wire-navigate active badge="12" />
|
||||||
|
<x-navigation-rail-item label="Starred" icon="star" link="#navigation" no-wire-navigate />
|
||||||
|
<x-navigation-rail-item label="Sent" icon="send" link="#navigation" no-wire-navigate />
|
||||||
|
|
||||||
|
<x-slot:footer>
|
||||||
|
<x-navigation-rail-item label="Settings" icon="settings" link="#navigation" no-wire-navigate />
|
||||||
|
</x-slot:footer>
|
||||||
|
</x-navigation-rail>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
|
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||||
<x-navigation-rail mode="expanded" label="Expanded example">
|
<x-navigation-rail mode="expanded" label="Expanded example">
|
||||||
<x-slot:header>
|
<x-slot:header>
|
||||||
|
|||||||
@@ -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).
|
// 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-divider]:not([data-open]) > [data-navigation-rail-panel] {')
|
||||||
->toContain("[data-navigation-rail][data-fill='false']: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).
|
// 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: 6rem;/");
|
->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('<x-navigation-rail width="narrow"><x-slot:header><button>FAB</button></x-slot:header><x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-rail>');
|
||||||
|
|
||||||
|
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('<span data-navigation-label>Inbox</span>')
|
||||||
|
->and((string) $this->blade('<x-navigation-rail mode="collapsed" width="20rem" />'))
|
||||||
|
->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 () {
|
it('flattens a FAB nested in the rail header and morphs its label', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user