diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 3c4da5c9..ecdf9984 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -718,7 +718,7 @@ A visitor who has pressed the menu button keeps that choice in both standard ban ``` - `destinations`: `title`, `icon`, `url`; optional `active` (default: the URL is the page's, also during a Livewire update request), `badge` (`true` for a dot, or a count), `badgeLabel` (what a screen reader hears for the badge: "3 unread"), `section` (a heading in the rail, shown only while it is expanded; consecutive destinations with the same section are grouped), `bar` (default `true`; `false` keeps it out of the bottom bar — M3 wants three to five there), `navigate` (`false` for a full page load instead of `wire:navigate`). -- Slots, each rendered once: `banner` (a bar across the whole window, above the rail and the page), `brand` (beside the rail's menu button, expanded only), `rail-header` (a FAB), `rail-footer` (pinned to the foot of the rail), `actions` (a row of icon buttons at the very foot, stacked when collapsed), `top` (the page's own bar, above the page and beside the rail), and the page. `label` names the landmarks ("Main"); `rail-width` is the expanded width (`16rem`); `tall-bar` picks M3's 80px navigation bar over the 64px one; `hide-bar-on-scroll` lets the bar leave the window while the page scrolls down. +- Slots, each rendered once: `banner` (a bar across the whole window, above the rail and the page), `brand` (beside the rail's menu button, expanded only), `rail-header` (a FAB), `rail-footer` (pinned to the foot of the rail), `actions` (a row of icon buttons at the very foot, stacked when collapsed), `top` (the page's own bar, above the page and beside the rail), and the page. `label` names the landmarks ("Main"); `rail-width` is the expanded width (`16rem`); `tall-bar` picks M3's 80px navigation bar over the 64px one; `hide-bar-on-scroll` lets the bar leave the window while the page scrolls down; `hide-rail-when-collapsed` takes the rail out of the layout from `expanded` when its menu button collapses it, instead of narrowing it to 96px — the only way back is `$store.rail.show()`, so the app bar then needs a menu button at every width. - `banner` or `top`: M3's scaffold is bars, then rails, then panes. An application-wide bar — one search, one account menu, the same on every page — goes in `banner` and the rail starts under it; a bar that titles the page goes in `top`, beside the rail. Never both. A banner that pins itself to the top of the window says how tall it is (`style="--material-banner: 4rem"` on ``), so the rail sticks under it instead of behind it. - The rail is one element at every width: what is in it is also what a phone sees in the modal rail. On a compact window nothing opens it but `$store.rail.show()`, so a page whose destinations are not all in the bar needs a menu button in its app bar (hidden from `medium`). - `--material-margin` is M3's window margin (16px compact, 24px from `medium`) and the content region already carries it, so a page inside the shell writes no gutters of its own; something that must reach the window's edges opts out with `-mx-(--material-margin)`. @@ -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 — 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), `align` (`top` default, or `center` for M3's centred destinations — preferred on a tablet; the menu button, brand and FAB stay at the top and the footer at the foot), `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), `align` (`top` default, or `center` for M3's centred destinations — preferred on a tablet; the menu button, brand and FAB stay at the top and the footer at the foot), `hide-when-collapsed` (M3's immersive expanded behaviour, `collapsible` and `adaptive` only: collapsing the rail takes it out of the layout instead of narrowing it, and `$store.rail.show()` brings it back expanded over a scrim — so put a menu button in the app bar; the rail's own button then docks it again. Not below `medium` for a collapsible rail nor at `medium` for an adaptive one, where the window rather than the visitor collapses it and M3's collapsed rail may never hide), `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 d646b646..99059700 100644 --- a/resources/css/components/navigation.css +++ b/resources/css/components/navigation.css @@ -48,12 +48,14 @@ * in the rail: those have to beat a utility, and a layer never does. */ +/* Every branch below reads `:not([data-open])`: a rail open over a scrim is drawn expanded whatever + made it collapsed, which only `hide-when-collapsed` can bring about outside the modal modes. */ @custom-variant rail-collapsed { &:where([data-navigation-rail='collapsed'], [data-navigation-rail='collapsed'] *) { @slot; } - &:where([data-rail='collapsed'] [data-navigation-rail='collapsible'], [data-rail='collapsed'] [data-navigation-rail='collapsible'] *) { + &:where([data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]) *) { @slot; } @@ -64,7 +66,7 @@ /* Below `medium` a 220–360px rail would be most of the screen, so a rail whose width is the visitor's choice is held collapsed there whatever they chose. */ @media (width < 37.5rem) { - &:where([data-navigation-rail='collapsible'], [data-navigation-rail='collapsible'] *) { + &:where([data-navigation-rail='collapsible']:not([data-open]), [data-navigation-rail='collapsible']:not([data-open]) *) { @slot; } } @@ -80,14 +82,14 @@ `data-rail-auto`, which sets while nothing is stored, stands for "no choice yet" and the class's own default applies. */ @media (52.5rem <= width < 75rem) { - &:where(:is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive'], :is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive'] *) { + &:where(:is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive']:not([data-open]), :is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive']:not([data-open]) *) { @slot; } } /* Large and extra-large (from 1200): expanded to begin with, which is what M3 prefers there. */ @media (width >= 75rem) { - &:where([data-rail='collapsed'] [data-navigation-rail='adaptive'], [data-rail='collapsed'] [data-navigation-rail='adaptive'] *) { + &:where([data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]) *) { @slot; } } @@ -239,6 +241,18 @@ --navigation-rail-collapsed-width: 5rem; } + /* "Expanded behavior | Hide when collapsed" (the reference's Navigation rail § Variants and + configurations): instead of narrowing, the rail leaves the layout entirely — for an + immersive page — and comes back over it when a menu button somewhere else, the app bar's, + calls `$store.rail.show()`. It is the *expanded* rail that hides, which is why what comes + back is the expanded rail: "collapsed rail may not hide", and the two bands where a window + size class rather than the visitor collapses one are given back below. */ + [data-navigation-rail][data-hide-when-collapsed] { + @variant rail-collapsed { + width: 0; + } + } + /* 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. */ @@ -381,6 +395,72 @@ } } + /* A rail that hides when collapsed is in the same position: nothing is left in the layout for + it to grow out of, so it takes the compact window's slide-in. The whole treatment is under + the `rail-collapsed` variant, which stops the moment the rail is open — that is what makes + it the expanded rail that comes back, on the rule above. */ + [data-navigation-rail][data-hide-when-collapsed] > [data-navigation-rail-panel] { + @variant rail-collapsed { + display: none; + translate: -100% 0; + transition: + translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate), + display var(--md-sys-motion-effects-default-duration) allow-discrete; + + &:dir(rtl) { + translate: 100% 0; + } + } + } + + [data-navigation-rail][data-hide-when-collapsed][data-open] > [data-navigation-rail-panel] { + display: flex; + translate: 0 0; + transition: + translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate), + display var(--md-sys-motion-spatial-default-duration) allow-discrete; + + @starting-style { + translate: -100% 0; + } + + &:dir(rtl) { + translate: 0 0; + + @starting-style { + translate: 100% 0; + } + } + } + + /* The two bands the configuration does not reach, because there it is the window and not the + visitor that collapses the rail: below `medium` for a collapsible rail — where a rail is + held at its collapsed width anyway — and `medium` itself for the adaptive one, which M3 + gives a collapsed rail. (Below `medium` the adaptive rail is already away, over the page, + and the block above it says so.) resources/js/navigation.js reads the same two numbers, so + the menu button and the drawing agree at every width. */ + @media (width < 37.5rem) { + [data-navigation-rail='collapsible'][data-hide-when-collapsed] { + width: var(--navigation-rail-collapsed-width); + } + + [data-navigation-rail='collapsible'][data-hide-when-collapsed] > [data-navigation-rail-panel] { + display: flex; + translate: none; + } + } + + @media (37.5rem <= width < 52.5rem) { + [data-navigation-rail='adaptive'][data-hide-when-collapsed] { + width: var(--navigation-rail-collapsed-width); + } + + [data-navigation-rail='adaptive'][data-hide-when-collapsed] > [data-navigation-rail-panel] { + display: flex; + translate: none; + } + } + [data-navigation-rail-scrim] { position: fixed; inset: 0; diff --git a/resources/js/navigation.js b/resources/js/navigation.js index c6c05939..f6076578 100644 --- a/resources/js/navigation.js +++ b/resources/js/navigation.js @@ -13,12 +13,12 @@ * button opens it over a scrim (`show()`), and Escape, the scrim or leaving the page closes it * (`hide()`). It is never remembered. * - * `materialNavigationRail` is one rail's view of the store for its `mode` — see - * resources/views/components/navigation-rail.blade.php. + * `materialNavigationRail` is one rail's view of the store for its `mode` and whether it hides + * when collapsed — see resources/views/components/navigation-rail.blade.php. * * `materialNavigationBar` is `` — see the same file's sibling. */ -import { from } from './breakpoints.js' +import { from, upTo } from './breakpoints.js' /* * The active indicator grows out of its centre when a page arrives through wire:navigate. The @@ -96,14 +96,29 @@ document.addEventListener('alpine:init', () => { // A destination chosen in the modal rail leaves the page; the next one starts with it shut. document.addEventListener('livewire:navigating', () => window.Alpine.store('rail').hide()) - window.Alpine.data('materialNavigationRail', (mode) => ({ + window.Alpine.data('materialNavigationRail', (mode, hideWhenCollapsed = false) => ({ wide: mode === 'adaptive' ? from('expanded').matches : false, roomy: mode === 'adaptive' ? from('large').matches : false, + tight: mode === 'collapsible' ? upTo('medium').matches : false, queries: [], listeners: [], init() { if (mode !== 'adaptive') { + // Below `medium` a collapsible rail is held at its collapsed width whatever the + // visitor chose, so `hide-when-collapsed` does not reach it there. + // A drawer left open as the window narrows past it is shut, as the adaptive + // rail's is below, or it would spring open again the next time the rail is away. + if (mode === 'collapsible' && hideWhenCollapsed) { + this.watch(upTo('medium'), (matches) => { + this.tight = matches + + if (matches) { + this.$store.rail.hide() + } + }) + } + return } @@ -136,17 +151,9 @@ document.addEventListener('alpine:init', () => { this.queries.forEach((query, index) => query.removeEventListener('change', this.listeners[index])) }, - /** Whether this rail expands over a scrim rather than in the layout. */ - get modal() { - return mode === 'modal' || (mode === 'adaptive' && !this.wide) - }, - - get open() { - return this.modal && this.$store.rail.open - }, - - get expanded() { - if (this.open || mode === 'expanded') { + /** What this rail's mode and the visitor's choice make of it, before anything opens it. */ + get standing() { + if (mode === 'expanded') { return true } @@ -163,9 +170,49 @@ document.addEventListener('alpine:init', () => { return mode === 'collapsible' }, + /** Whether the mode or the window leaves no room for an expanded rail in the layout. */ + get cramped() { + return mode === 'modal' || (mode === 'adaptive' && !this.wide) + }, + + /** + * Whether the rail has left the layout altogether — `hide-when-collapsed`, once the + * visitor collapses it. Not in the two bands where it is the window size class and not the + * visitor that collapses a rail: M3's "collapsed rail may not hide". The same two numbers + * are in resources/css/components/navigation.css. + */ + get away() { + if (!hideWhenCollapsed || this.cramped || this.standing) { + return false + } + + return mode === 'adaptive' ? this.wide : !this.tight + }, + + /** Whether this rail expands over a scrim rather than in the layout. */ + get modal() { + // A rail that is away has nothing left in the layout to expand, so the menu button + // that brings it back — the app bar's — opens it over the page. + return this.cramped || this.away + }, + + get open() { + return this.modal && this.$store.rail.open + }, + + get expanded() { + return this.open || this.standing + }, + /** The rail's own menu button: open or close the modal, or collapse and expand in place. */ menu() { - if (this.modal) { + if (this.open && this.away) { + // This rail is only over the page because it hid itself, so the button docks it + // back into the layout — the same "expand" it means on a rail that is standing + // there. Expanding drops `away`, which closes the drawer behind it. + this.$store.rail.expand() + this.$store.rail.hide() + } else if (this.modal) { this.$store.rail.open ? this.$store.rail.hide() : this.$store.rail.show() } else { // `set`, not `toggle`: with nothing stored the store's `collapsed` is only diff --git a/resources/views/components/app-shell.blade.php b/resources/views/components/app-shell.blade.php index 3a7e208a..d286e3cb 100644 --- a/resources/views/components/app-shell.blade.php +++ b/resources/views/components/app-shell.blade.php @@ -56,7 +56,11 @@ phone opens. `label` names both navigation landmarks ("Main"); `rail-width` is the expanded rail's width; `tall-bar` picks M3's 80px navigation bar over the 64px one, and the bottom offset every pinned thing reads follows it; `hide-bar-on-scroll` lets the bar leave the window - while the page scrolls down, and `--material-bottom-bar` goes down and comes back with it. + while the page scrolls down, and `--material-bottom-bar` goes down and comes back with it; + `hide-rail-when-collapsed` is M3's immersive configuration — from `expanded` the rail leaves + the layout when the menu button collapses it, rather than narrowing to 96px, so the page has + the whole window. The only way back is `$store.rail.show()`, so put a menu button in the app + bar at every width, not just below `medium`. `banner` or `top` is a decision about what the bar belongs to: an application-wide bar — one search, one account menu, the same on every page — spans the window and the rail starts under @@ -86,6 +90,7 @@ 'railWidth' => '16rem', 'tallBar' => false, 'hideBarOnScroll' => false, + 'hideRailWhenCollapsed' => false, ]) @php @@ -132,7 +137,7 @@ @endisset
- + @isset($brand) {{ $brand }} @endisset diff --git a/resources/views/components/navigation-rail.blade.php b/resources/views/components/navigation-rail.blade.php index 8db0c46d..c097a3f2 100644 --- a/resources/views/components/navigation-rail.blade.php +++ b/resources/views/components/navigation-rail.blade.php @@ -60,7 +60,13 @@ puts the destinations at the rail's vertical centre — M3 prefers that on a tablet, for reach — while the menu button, the brand and the FAB stay at the top and the footer at the foot, as M3 asks; more destinations than fit go back to the top rather than out of reach above the - scroller; `menu` shows the menu button (by default for `collapsible`, + scroller; `hide-when-collapsed` is M3's other expanded behaviour, for a `collapsible` or + `adaptive` rail: collapsing it takes it out of the layout altogether instead of narrowing it + to 96px, and it comes back expanded over a scrim when something calls `$store.rail.show()` — + a menu button in the app bar, which is the only way back, so put one there. Its own menu + button then docks it into the layout again. Not below `medium` for a collapsible rail, nor at + `medium` for an adaptive one: there it is the window and not the visitor that collapses a + rail, and M3's collapsed rail may never hide; `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, @@ -83,6 +89,7 @@ 'label' => null, 'width' => '16rem', 'align' => 'top', + 'hideWhenCollapsed' => false, 'menu' => null, 'divider' => false, 'fill' => true, @@ -91,7 +98,10 @@ @php $mode = in_array($mode, ['collapsed', 'expanded', 'collapsible', 'modal', 'adaptive'], true) ? $mode : 'collapsible'; $interactive = in_array($mode, ['collapsible', 'modal', 'adaptive'], true); - $canOpen = in_array($mode, ['modal', 'adaptive'], true); + // Only a rail that has a collapsed *and* an expanded state of its own can hide instead of + // narrowing; a `modal` one is already over the page, and the two fixed modes mean what they say. + $hideWhenCollapsed = $hideWhenCollapsed && in_array($mode, ['collapsible', 'adaptive'], true); + $canOpen = in_array($mode, ['modal', 'adaptive'], true) || $hideWhenCollapsed; $menu ??= $interactive; $collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true); $narrow = $width === 'narrow'; @@ -103,10 +113,11 @@ data-navigation-rail="{{ $mode }}" @if ($narrow) data-width="narrow" @endif @if ($centred) data-align="center" @endif + @if ($hideWhenCollapsed) data-hide-when-collapsed @endif @if ($divider) data-divider @endif @unless ($fill) data-fill="false" @endunless @if ($interactive) - x-data="materialNavigationRail('{{ $mode }}')" + x-data="materialNavigationRail('{{ $mode }}', {{ $hideWhenCollapsed ? 'true' : 'false' }})" x-bind:data-open="open" @endif {{ $attributes->merge(['style' => "--navigation-rail-width: {$width}"]) }} diff --git a/resources/views/showcase/sections/navigation.blade.php b/resources/views/showcase/sections/navigation.blade.php index 5182f2e9..db1e2e46 100644 --- a/resources/views/showcase/sections/navigation.blade.php +++ b/resources/views/showcase/sections/navigation.blade.php @@ -138,6 +138,24 @@
BLADE, + 'Rail that hides when collapsed' => <<<'BLADE' +
+ + + Studio + + + + + + + +
+

M3's other expanded behaviour: collapsing this rail takes it out of the layout instead of narrowing it to 96px, so an immersive page gets the whole window. The only way back is a menu button of the application's own — its own button then docks it again.

+ +
+
+ BLADE, ]; @endphp diff --git a/tests/Feature/Components/AppShellTest.php b/tests/Feature/Components/AppShellTest.php index 8abe61ca..ea309bfc 100644 --- a/tests/Feature/Components/AppShellTest.php +++ b/tests/Feature/Components/AppShellTest.php @@ -115,6 +115,15 @@ it('lets the bar leave the window while the page scrolls down', function () { ->not->toContain('data-hide-on-scroll'); }); +it('exposes the rail that hides when collapsed', function () { + expect((string) $this->blade('', ['destinations' => shellDestinations()])) + ->toContain('data-hide-when-collapsed') + ->toContain("materialNavigationRail('adaptive', true)") + ->and((string) $this->blade('', ['destinations' => shellDestinations()])) + ->not->toContain('data-hide-when-collapsed') + ->toContain("materialNavigationRail('adaptive', false)"); +}); + it('reads the safe area and anything docked on the bar through variables an application can set', function () { $html = (string) $this->blade('', ['destinations' => shellDestinations()]); diff --git a/tests/Feature/Components/NavigationRailTest.php b/tests/Feature/Components/NavigationRailTest.php index ce2b0f8d..7fa83ae3 100644 --- a/tests/Feature/Components/NavigationRailTest.php +++ b/tests/Feature/Components/NavigationRailTest.php @@ -18,7 +18,7 @@ it('gives the collapsible, modal and adaptive rails a menu button and the store' $collapsible = (string) $this->blade(''); expect($collapsible) - ->toContain('x-data="materialNavigationRail(\'collapsible\')"') + ->toContain('x-data="materialNavigationRail(\'collapsible\', false)"') ->toContain('data-navigation-rail-menu') ->toContain('aria-label="Collapse navigation"') ->toContain('aria-expanded="true"') @@ -102,6 +102,35 @@ it('centres the destinations on request, leaving the header and footer at their ->toMatch("/\\[data-navigation-rail\\]\\[data-align='center'\\] \\[data-navigation-rail-destinations\\] \\{\\s+justify-content: safe center;/"); }); +it('hides a collapsible or adaptive rail entirely when told to, and only those', function () { + $hiding = (string) $this->blade(''); + + expect($hiding) + ->toContain('data-hide-when-collapsed') + ->toContain("x-data=\"materialNavigationRail('collapsible', true)\"") + // It needs the scrim and the focus trap a modal rail has: it comes back over the page. + ->toContain('data-navigation-rail-scrim') + ->toContain('x-trap.inert.noscroll="open"') + ->and((string) $this->blade('')) + ->toContain("x-data=\"materialNavigationRail('adaptive', true)\"") + // A modal rail is already over the page; the two fixed modes mean what they say. + ->and((string) $this->blade('')) + ->not->toContain('data-hide-when-collapsed') + ->toContain("materialNavigationRail('modal', false)") + ->and((string) $this->blade('')) + ->not->toContain('data-hide-when-collapsed') + ->and((string) $this->blade(''))->not->toContain('data-hide-when-collapsed'); + + expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css')) + ->toMatch('/\[data-navigation-rail\]\[data-hide-when-collapsed\] \{\s+@variant rail-collapsed \{\s+width: 0;/') + // The two bands where the window, not the visitor, collapses a rail keep theirs. + ->toMatch("/@media \\(width < 37\\.5rem\\) \\{\\s+\\[data-navigation-rail='collapsible'\\]\\[data-hide-when-collapsed\\]/") + ->toMatch("/@media \\(37\\.5rem <= width < 52\\.5rem\\) \\{\\s+\\[data-navigation-rail='adaptive'\\]\\[data-hide-when-collapsed\\]/") + // Every collapsed branch stops while the rail is open, or what comes back would be narrow. + ->and(substr_count(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'), ':not([data-open])')) + ->toBeGreaterThanOrEqual(12); +}); + it('flattens a FAB nested in the rail header and morphs its label', function () { // Both rules hang off `data-fab` on 's root, and both are unlayered, because what they // beat — the FAB's shadow, its gap, an extended FAB's minimum width — are utilities.