diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 72cee4bc..6e976858 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -66,7 +66,7 @@ jobs: - name: Run feature tests run: vendor/bin/pest --testsuite=Feature - # One job per engine: the floor is Chrome 125, Firefox 147 and Safari 18.4, and the + # One job per engine: the floor is Chrome 125, Firefox 151 and Safari 18.4, and the # components lean on platform features (dialog, popover, anchor positioning) that # differ between them. Pest calls the engines chrome, firefox and safari; Playwright # installs them as chromium, firefox and webkit. diff --git a/README.md b/README.md index edba6d75..8985c8d7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Material 3 Expressive components for Laravel and Livewire, in plain CSS. - Error pages and a Markdown mail theme in the same scheme. - A showcase of every component in the application's own scheme, a design guard for tests, and for AI agents two Laravel Boost guidelines and two skills: the library's own, and Material 3's rules and tables beside its utilities. -No JavaScript libraries beyond the Alpine that ships with Livewire. Browsers: Chrome 125+, Firefox 147+, Safari 18.4+. +No JavaScript libraries beyond the Alpine that ships with Livewire. Browsers: Chrome 125+, Firefox 151+, Safari 18.4+. ## Requirements diff --git a/UPGRADE.md b/UPGRADE.md index c2551dda..2b080512 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,17 @@ # Upgrading +## From 2.0.0 to 2.1.0 + +- **Browsers:** Firefox 151 or later (was 147), for container style queries on a custom property; + Chrome 125 and Safari 18.4 are unchanged. +- **``** publishes its value as `--md-navigation-rail-value`, `collapsed` or + `expanded` (M3's two rail values). Content an application puts in a rail reads it with + `@container style(--md-navigation-rail-value: collapsed)` instead of copying the rail's + conditions from `navigation-rail.css`. +- **``** sizes the loading indicator in px, 24 to 240 (M3's responsive range), + with the container and the shape in proportion. A width and height of the application's own + still work. + ## From 1.x to 2.0.0 2.0.0 aligns the library with Material Design 3 Expressive as Google documents it @@ -122,8 +134,9 @@ colour, use the role directly. - `` is renamed ``, with no alias; it is a column with a nested row and gains `banner` and `fab` slots. ``'s `header` slot takes one `` that morphs (replace the two-FAB `rail-collapsed:` swap); new `divider` - and `fill` props. The `rail-collapsed:` variant is gone: style a rail's collapsed shape in the - application's CSS with the conditions `resources/css/components/navigation-rail.css` lists. `data-app-shell`, `data-app-shell-bar`, `data-app-shell-actions` and + and `fill` props. The `rail-collapsed:` variant is gone: style what the application puts in a + rail by the rail's value, `@container style(--md-navigation-rail-value: collapsed)` (2.1.0). + `data-app-shell`, `data-app-shell-bar`, `data-app-shell-actions` and `data-app-shell-banner` are `data-md-scaffold`, `data-md-scaffold-bar`, `data-md-scaffold-actions` and `data-md-scaffold-banner`; the skip link is `data-md-skip-link`. - `` selects the optical-size-20 cut; the components pass it for their own diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index a99d05bd..a7b52cb6 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -400,11 +400,11 @@ Two to six items open above the FAB, which turns into a close button, rising int ### `` -M3 Expressive's loading indicator — a shape morphing through seven Expressive shapes as it turns — for a wait of unknown length. 48px and `primary` unless your own CSS sizes or colours it (there is no `size` prop); `contained` sets it on a primary-container circle. A `progressbar` named by `label` ("Loading"); `:label="false"` makes it decorative. It rests under reduced motion. +M3 Expressive's loading indicator — a shape morphing through seven Expressive shapes as it turns — for a wait of unknown length. 48px and `primary` unless sized or coloured: `size` is its size in px, a whole number from 24 to 240 — M3's responsive range, which it asks you never to leave; scale it up on large and extra-large windows — and the container and the shape keep their ratio at every size (outside the range it is ignored, and your own CSS may size it). Colour it with your own CSS. `contained` sets it on a primary-container circle. A `progressbar` named by `label` ("Loading"); `:label="false"` makes it decorative. It rests under reduced motion. ```blade - +
``` @@ -1034,7 +1034,16 @@ 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), `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 can take both shapes by matching the rail's own state directly — `data-md-navigation-rail`'s value, `:not([data-md-open])`, and the window band each mode collapses in, the same selectors `resources/css/components/navigation-rail.css` uses for every branch of "collapsed" (that file, not this one, is where the numbers live). There is no single hook or variant for "collapsed": write those conditions out in your own CSS. Nothing that shows while collapsed may be wider than 96px. +- Anything else inside a rail takes both shapes from the rail's value. The rail publishes M3's two values (Compose's `WideNavigationRailValue`) as `--md-navigation-rail-value`, `collapsed` or `expanded`, from the first paint and at the same moment as its own items change shape, whatever made it so (mode, window size class, the visitor's choice); a rail open over a scrim reads `expanded`. Every descendant inherits it, so your own CSS asks a style query rather than repeating the rail's conditions. Outside a rail it is unset and neither value matches. Style queries on a custom property need Chrome 111, Safari 18 or Firefox 151. Nothing that shows while collapsed may be wider than 96px. + +```css +@container style(--md-navigation-rail-value: collapsed) { + .account-summary { + display: none; + } +} +``` + - 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 `` 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. - `$store.rail`: `collapsed`, `toggle()`, `collapse()`, `expand()` (the remembered choice; `auto` is true while nothing is stored, so an adaptive rail takes its window size class's default instead, and the first choice clears it), `open`, `show()`, `hide()` (the modal rail; closed on every `wire:navigate`). `config/livewire-material.php` → `rail.default` (`expanded` or `collapsed`) and `rail.storage_key` (`material-rail`). diff --git a/resources/css/components/loading.css b/resources/css/components/loading.css index 45bbc47f..d840966d 100644 --- a/resources/css/components/loading.css +++ b/resources/css/components/loading.css @@ -4,7 +4,9 @@ * * The drawing is resources/svg/loading-indicator/, ported from androidx Compose Material 3's * LoadingIndicator in bin/loading-indicator.mjs (Apache-2.0): LoadingIndicatorTokens' 38px - * indicator in a 48px container, which is the size here unless the caller's CSS sizes it. It is + * indicator in a 48px container, which is the size here unless `size` (`--md-loading-size`, 24 to + * 240px, M3's responsive range) or the caller's CSS sizes it; the SVG scales with its box, so the + * container and the shape keep their ratio at every size. It is * drawn in the text colour, `primary` (ActiveIndicatorColor) unless the caller colours it; * `contained` puts it on a `primary-container` circle in `on-primary-container` * (ContainedContainerColor, ContainedIndicatorColor), for a spinner over content. @@ -21,8 +23,8 @@ flex-shrink: 0; align-items: center; justify-content: center; - inline-size: 48px; - block-size: 48px; + inline-size: var(--md-loading-size, 48px); + block-size: var(--md-loading-size, 48px); color: var(--md-sys-color-primary); & > svg { diff --git a/resources/css/components/navigation-rail.css b/resources/css/components/navigation-rail.css index 4c2a3e9c..a67847b2 100644 --- a/resources/css/components/navigation-rail.css +++ b/resources/css/components/navigation-rail.css @@ -18,9 +18,8 @@ * [data-md-navigation-rail-item] navigation-rail-item.css * [data-md-navigation-rail-footer] never scrolls * - * **Collapsed**, reproduced from the Tailwind-era custom variant named `rail-collapsed`: a rail (and - * anything in it) is drawn collapsed while any of these hold, each its own rule below wherever a - * property differs by shape — a fixed `collapsed` mode; a `collapsible` rail the visitor collapsed + * **Collapsed**: a rail (and anything in it) is drawn collapsed while any of these hold, each its + * own rule below wherever a property differs by shape — a fixed `collapsed` mode; a `collapsible` rail the visitor collapsed * (``, set before the first paint by ``) and not * currently open over the page; a `modal` rail not open; below `medium` (600px) a `collapsible` * rail regardless of choice, M3's floor ("compact → use a navigation bar, not a standard rail"); @@ -34,6 +33,13 @@ * `resources/js/navigation.js` reads the same numbers, so the menu button and the drawing agree at * every width. * + * The first set of branches also publishes the answer: `--md-navigation-rail-value` is `expanded` + * on every rail and `collapsed` wherever those branches hold — M3's two rail values, Compose's + * WideNavigationRailValue — so what an application puts in a rail reads it with + * `@container style(--md-navigation-rail-value: collapsed)` rather than copying the conditions. It + * is an unregistered custom property, so it inherits into every descendant and is unset outside a + * rail, where neither value matches. + * * `data-md-width="narrow"` is M3's other collapsed width, `NarrowContainerWidth` 80px against the * default 96 (`CollapsedContainerWidth`) — a variable, so every rule below that has its own reason * to name a collapsed width does not have to know which one applies. @@ -66,6 +72,7 @@ [data-md-navigation-rail] { --navigation-rail-expanded-width: clamp(220px, var(--navigation-rail-width, 256px), 360px); --navigation-rail-collapsed-width: 96px; + --md-navigation-rail-value: expanded; position: relative; flex-shrink: 0; @@ -81,17 +88,20 @@ [data-md-navigation-rail='modal']:not([data-md-open]) * ) { width: var(--navigation-rail-collapsed-width); + --md-navigation-rail-value: collapsed; } @media (width < 600px) { &:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) { width: var(--navigation-rail-collapsed-width); + --md-navigation-rail-value: collapsed; } } @media (width < 840px) { &:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) { width: var(--navigation-rail-collapsed-width); + --md-navigation-rail-value: collapsed; } } @@ -101,6 +111,7 @@ :is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) * ) { width: var(--navigation-rail-collapsed-width); + --md-navigation-rail-value: collapsed; } } @@ -110,6 +121,7 @@ [data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) * ) { width: var(--navigation-rail-collapsed-width); + --md-navigation-rail-value: collapsed; } } } diff --git a/resources/views/components/loading.blade.php b/resources/views/components/loading.blade.php index c105ad22..d8c32c96 100644 --- a/resources/views/components/loading.blade.php +++ b/resources/views/components/loading.blade.php @@ -1,8 +1,12 @@ {{-- M3 Expressive's loading indicator: a shape that morphs through seven Expressive shapes while it turns, for a wait that has no known length. - In the text colour — primary unless the caller colours it — and 48px unless the caller's CSS - sizes it. `contained` puts it on a primary-container circle, for a spinner over content. It + In the text colour — primary unless the caller colours it — and 48px, M3's default. `size` is + its size in px, any whole number from 24 to 240, M3's responsive range ("never exceed that + range"; scale it up for large and extra-large windows): the container and the shape keep + their 48:38 ratio at every size, because the drawing scales with its box. It is written as + `--md-loading-size`; anything outside the range is ignored, and without it the caller's CSS + may size it. `contained` puts it on a primary-container circle, for a spinner over content. It is a `progressbar` named `label` ("Loading" by default); pass `:label="false"` where something else already says what is happening, as a button's own spinner does. @@ -16,15 +20,18 @@ @props([ 'contained' => false, 'label' => null, + 'size' => null, ]) @php + $size = filter_var($size, FILTER_VALIDATE_INT, ['options' => ['min_range' => 24, 'max_range' => 240]]) ?: null; $decorative = $label === false; $label = $decorative ? null : ($label ?? __('Loading')); $attributes = $attributes->merge(array_filter([ 'data-md-loading' => true, 'data-md-contained' => $contained ? true : null, + 'style' => $size !== null ? "--md-loading-size: {$size}px" : null, 'role' => $decorative ? null : 'progressbar', 'aria-label' => $label, 'aria-hidden' => $decorative ? 'true' : null, diff --git a/resources/views/components/navigation-rail.blade.php b/resources/views/components/navigation-rail.blade.php index 17063f4e..443589f3 100644 --- a/resources/views/components/navigation-rail.blade.php +++ b/resources/views/components/navigation-rail.blade.php @@ -45,11 +45,19 @@ `footer`, pinned to the foot. Header and footer never scroll, so nothing in them is cut off by the scroller's edge. - Anything else inside can take both shapes by matching the rail's own state directly, the same - selectors resources/css/components/navigation-rail.css uses for every branch of "collapsed" — - `data-md-navigation-rail`'s value, `:not([data-md-open])`, and the window band each mode - collapses in (its header lists them). Every rule in the package that draws a collapsed shape - writes them out, and tests/Feature/Components/NavigationRailTest.php keeps each copy to the same + Anything else inside takes both shapes from the rail's value: the rail publishes M3's two + (Compose's WideNavigationRailValue) as `--md-navigation-rail-value`, `collapsed` or + `expanded`, from the first paint and in step with the rail's own items, and every descendant + inherits it, so an application's CSS asks a style query instead of repeating the conditions: + + @container style(--md-navigation-rail-value: collapsed) { + .account-summary { display: none; } + } + + A rail open over a scrim reads `expanded`; outside a rail the property is unset and neither + value matches. Style queries on a custom property need Chrome 111, Safari 18 or Firefox 151. + The package's own rules write the conditions out (resources/css/components/navigation-rail.css + lists them), and tests/Feature/Components/NavigationRailTest.php keeps each copy to the same conditions. Nothing that shows while collapsed may be wider than 96px. Props: `label` names the landmark ("Main"); `width` is the expanded width (`256px`, held diff --git a/resources/views/showcase/sections/buttons.blade.php b/resources/views/showcase/sections/buttons.blade.php index 95b6c5cf..14583a74 100644 --- a/resources/views/showcase/sections/buttons.blade.php +++ b/resources/views/showcase/sections/buttons.blade.php @@ -60,8 +60,8 @@ 'Loading indicator' => <<<'BLADE' - - + + BLADE, 'Button groups: standard (press a button) and connected' => <<<'BLADE' diff --git a/tests/Browser/ActionsTest.php b/tests/Browser/ActionsTest.php index 5c85a729..b1bdca9c 100644 --- a/tests/Browser/ActionsTest.php +++ b/tests/Browser/ActionsTest.php @@ -272,6 +272,15 @@ it('animates the loading indicator in the browser', function () { showcase()->assertScript("{$clock} > 0.1"); }); +it('draws the loading indicator at the size it is given, container and shape in proportion', function () { + $box = fn (string $selector): string => "document.querySelector('#buttons {$selector}').getBoundingClientRect()"; + + showcase() + ->assertScript("(({ width, height }) => width === 96 && height === 96)({$box('[aria-label=\"Uploading\"]')})") + ->assertScript("(({ width, height }) => width === 96 && height === 96)({$box('[aria-label=\"Uploading\"] > [data-md-loading-animated]')})") + ->assertScript("(({ width, height }) => width === 32 && height === 32)({$box('[data-md-loading][data-md-contained][style]')})"); +}); + /** A script giving the rects of a menu button (`control`) and of the menu it opens (`menu`). */ function menuAgainst(string $test, string $label): string { diff --git a/tests/Browser/NavigationTest.php b/tests/Browser/NavigationTest.php index a0d3bea7..35e93666 100644 --- a/tests/Browser/NavigationTest.php +++ b/tests/Browser/NavigationTest.php @@ -431,3 +431,127 @@ it('takes an adaptive rail out of the layout across the expanded class when it h ->assertScript("document.documentElement.hasAttribute('data-rail-auto')") ->assertNoJavaScriptErrors(); }); + +/** + * One rail in the given mode, whose footer holds two lines an application styles by the rail's + * value with style queries — the way `--md-navigation-rail-value` is meant to be read. An inline + * script after the rail records the value and the lines' display while the document is still being + * parsed, before Alpine exists: what the first paint shows. + */ +function railValueProbe(string $mode, int $width = 1280): mixed +{ + Route::middleware('web')->get('/rail-value-probe/{mode}', fn (string $mode) => Blade::render(<<<'BLADE' + + + + + @vite(config('livewire-material.showcase.vite')) + @livewireStyles + + + +
+ + + + +

Signed in as Ada

+

A

+
+
+ +
+ +

Outside the rail

+
+
+ @livewireScripts + + + BLADE, ['mode' => $mode])); + + return navigationReady(visit("/rail-value-probe/{$mode}")->resize($width, 900)); +} + +const RAIL_VALUE = 'getComputedStyle('.RAIL.").getPropertyValue('--md-navigation-rail-value').trim()"; + +function railShows(string $line): string +{ + return "getComputedStyle(document.querySelector('[data-test=\"{$line}\"]')).display !== 'none'"; +} + +it('publishes a fixed rail\'s value from the first paint, for style queries in its content', function () { + railValueProbe('collapsed') + ->assertScript("window.firstPaint.alpine === 'undefined'") + ->assertScript("window.firstPaint.value === 'collapsed'") + ->assertScript("window.firstPaint.expandedOnly === 'none' && window.firstPaint.collapsedOnly !== 'none'") + ->assertScript(RAIL_VALUE." === 'collapsed'") + ->assertScript('! '.railShows('expanded-only')) + ->assertScript(railShows('collapsed-only')) + // Outside a rail the property is unset, so neither value's query matches. + ->assertScript("getComputedStyle(document.querySelector('[data-test=\"outside\"]')).getPropertyValue('--md-navigation-rail-value') === ''") + ->assertNoJavaScriptErrors(); + + railValueProbe('expanded') + ->assertScript("window.firstPaint.value === 'expanded'") + ->assertScript("window.firstPaint.expandedOnly !== 'none' && window.firstPaint.collapsedOnly === 'none'") + ->assertScript(RAIL_VALUE." === 'expanded'") + ->assertScript(railShows('expanded-only')) + ->assertScript('! '.railShows('collapsed-only')) + ->assertNoJavaScriptErrors(); +}); + +it('moves an adaptive rail\'s value with the window and the menu button, in step with its width', function () { + $page = railValueProbe('adaptive', 839) + ->assertScript(railWidth(96)) + ->assertScript(RAIL_VALUE." === 'collapsed'") + ->assertScript('! '.railShows('expanded-only')); + + // `large`: M3 starts an adaptive rail expanded while nothing is chosen. + $page->resize(1200, 900) + ->assertScript(railWidth(256)) + ->assertScript(RAIL_VALUE." === 'expanded'") + ->assertScript(railShows('expanded-only')) + ->assertScript('! '.railShows('collapsed-only')); + + // `expanded`: collapsed until the menu button widens it in place. + $page->resize(840, 900) + ->assertScript(RAIL_VALUE." === 'collapsed'") + ->click('[data-md-navigation-rail-menu]') + ->assertScript(railWidth(256)) + ->assertScript(RAIL_VALUE." === 'expanded'") + ->assertScript(railShows('expanded-only')) + ->assertNoJavaScriptErrors(); +}); + +it('reads expanded while a modal rail is open over the page, and collapsed once it closes', function () { + $page = railValueProbe('modal', 1000) + ->assertScript(RAIL_VALUE." === 'collapsed'") + ->assertScript('! '.railShows('expanded-only')); + + $page->click('#open-rail') + ->assertScript(RAIL.".hasAttribute('data-md-open')") + ->assertScript(RAIL_VALUE." === 'expanded'") + ->assertScript(railShows('expanded-only')) + ->assertScript('! '.railShows('collapsed-only')); + + $page->keys('[data-md-navigation-rail-panel]', 'Escape') + ->assertScript('! '.RAIL.".hasAttribute('data-md-open')") + ->assertScript(RAIL_VALUE." === 'collapsed'") + ->assertScript('! '.railShows('expanded-only')) + ->assertNoJavaScriptErrors(); +}); diff --git a/tests/Feature/Components/LoadingTest.php b/tests/Feature/Components/LoadingTest.php index b0623d14..3cb53f96 100644 --- a/tests/Feature/Components/LoadingTest.php +++ b/tests/Feature/Components/LoadingTest.php @@ -19,13 +19,23 @@ it('is 48px in the primary colour unless the caller sizes or colours it', functi $css = ComponentStylesheet::read('loading'); expect($css->declarations('[data-md-loading]')) - ->toMatchArray(['inline-size' => '48px', 'block-size' => '48px', 'color' => 'var(--md-sys-color-primary)']) + ->toMatchArray(['inline-size' => 'var(--md-loading-size, 48px)', 'block-size' => 'var(--md-loading-size, 48px)', 'color' => 'var(--md-sys-color-primary)']) // A caller's class lands on the root untouched, and outranks the package's layer. ->and((string) $this->blade('')) ->toContain('class="app-upload-spinner"') ->toContain('aria-label="Uploading"'); }); +it('draws a size from M3\'s responsive range, 24 to 240px', function () { + $this->blade('')->assertSee('style="--md-loading-size: 96px"', false); + $this->blade('')->assertSee('style="--md-loading-size: 24px"', false); + $this->blade('')->assertSee('style="--md-loading-size: 240px"', false); + + foreach (['16', '480', 'large'] as $outside) { + $this->blade("")->assertDontSee('--md-loading-size', false); + } +}); + it('sits on a primary-container circle when contained', function () { expect((string) $this->blade(''))->toContain('data-md-contained') ->and((string) $this->blade(''))->not->toContain('data-md-contained') diff --git a/tests/Feature/Components/NavigationRailTest.php b/tests/Feature/Components/NavigationRailTest.php index 4af6a400..bfd81c78 100644 --- a/tests/Feature/Components/NavigationRailTest.php +++ b/tests/Feature/Components/NavigationRailTest.php @@ -148,29 +148,33 @@ it('hides a collapsible or adaptive rail entirely when told to, and only those', * `X:where(A)` or `X:where(B)` or `X:where(C)`) and keeps the four media-gated ones separate, since * CSS cannot merge different `@media` queries into one rule. This pins all five physical branches * — and so every one of the seven original conditions — for the rail's own width, the property - * every branch sets. + * every branch sets, and for `--md-navigation-rail-value`, which each branch turns from `expanded` + * to `collapsed` so an application's style query answers exactly when the rail's own shape does. */ -it('reproduces every branch of the old rail-collapsed variant for the rail\'s own width', function () { +it('reproduces every branch of the old rail-collapsed variant for the rail\'s own width and its value', function () { $css = ComponentStylesheet::read('navigation-rail'); + $collapsed = ['width' => 'var(--navigation-rail-collapsed-width)', '--md-navigation-rail-value' => 'collapsed']; + + expect($css->declarations('[data-md-navigation-rail]'))->toMatchArray(['--md-navigation-rail-value' => 'expanded']); // Branches 1-3: a fixed `collapsed` mode; a `collapsible` rail the visitor collapsed and not // open; a `modal` rail not open. expect($css->declarations("[data-md-navigation-rail]:where( [data-md-navigation-rail='collapsed'], [data-md-navigation-rail='collapsed'] *, [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *, [data-md-navigation-rail='modal']:not([data-md-open]), [data-md-navigation-rail='modal']:not([data-md-open]) * )")) - ->toBe(['width' => 'var(--navigation-rail-collapsed-width)']) + ->toBe($collapsed) // Branch 4: below `medium` (600px), a `collapsible` rail regardless of choice. ->and($css->declarations("[data-md-navigation-rail]:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *)", ['@media (width < 600px)'])) - ->toBe(['width' => 'var(--navigation-rail-collapsed-width)']) + ->toBe($collapsed) // Branch 5: below `expanded` (840px), an `adaptive` rail not open. ->and($css->declarations("[data-md-navigation-rail]:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *)", ['@media (width < 840px)'])) - ->toBe(['width' => 'var(--navigation-rail-collapsed-width)']) + ->toBe($collapsed) // Branch 6: at `expanded` (840-1199), an `adaptive` rail not open, chosen collapsed or no // choice yet (`data-rail-auto`). ->and($css->declarations("[data-md-navigation-rail]:where( :is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]), :is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) * )", ['@media (840px <= width < 1200px)'])) - ->toBe(['width' => 'var(--navigation-rail-collapsed-width)']) + ->toBe($collapsed) // Branch 7: from `large` (1200px), an `adaptive` rail not open, only when chosen collapsed — // M3 starts it expanded there by default. ->and($css->declarations("[data-md-navigation-rail]:where( [data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) * )", ['@media (width >= 1200px)'])) - ->toBe(['width' => 'var(--navigation-rail-collapsed-width)']); + ->toBe($collapsed); }); it('flattens a FAB nested in the rail header and morphs its label', function () {