Size the loading indicator and publish the rail's value
Two gaps found rewriting the Boost skill for 2.0's plain CSS, each
closed the way M3 describes it.
`<x-loading size="96">`: M3 gives the loading indicator a responsive
size, 48dp by default and flexible from 24 to 240dp ("never exceed that
range"), with the container and the active shape in a fixed ratio. The
size had no prop, so an application wrote a width and height of its own;
`size` now takes a whole number of px in that range, written as
`--md-loading-size`, and the SVG keeps the 48:38 ratio as it scales. A
value outside the range is ignored, as `<x-icon>` and `<x-shape>` ignore
theirs, and an application's own width and height still win.
`--md-navigation-rail-value`: M3 Expressive's rail has two values,
collapsed and expanded (Compose's WideNavigationRailValue), and content
in a rail follows it. Without a hook an application copied the rail's
seven conditions — mode, `data-rail`, `data-rail-auto`, open, and the
window band — out of navigation-rail.css. The rail now publishes the
answer from the same branches that narrow it: `expanded` by default,
`collapsed` wherever it is drawn collapsed, so a style query in the
application's CSS switches at the first paint and at the same moment
as the rail's own items. A rail open over a scrim reads `expanded`, and
outside a rail the property is unset. A JS attribute would have missed
the first paint; a width container query would have lagged the collapse
animation.
Style queries on a custom property need Firefox 151, so the browser
floor moves from Firefox 147 to 151 (README, the CI note, UPGRADE's new
2.1.0 section); Chrome 125 and Safari 18.4 are unchanged.
Browser tests pin both in Chrome, Firefox and Safari: the indicator's
drawn box at 96 and 32px, and the rail's value — with a style query
acting on it — at the first paint for fixed modes, across the window
classes and the menu button for an adaptive rail, and open and closed
for a modal one. NavigationRailTest pins the value in each of the five
collapsed branches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
24eb811f34
commit
f2a3c4d056
@@ -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
|
||||
{
|
||||
|
||||
@@ -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'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<x-theme-script />
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
<style>
|
||||
@container style(--md-navigation-rail-value: collapsed) {
|
||||
[data-test='expanded-only'] { display: none; }
|
||||
}
|
||||
@container style(--md-navigation-rail-value: expanded) {
|
||||
[data-test='collapsed-only'] { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex; min-height: 100dvh">
|
||||
<x-navigation-rail :mode="$mode" label="Probe">
|
||||
<x-navigation-rail-item label="Inbox" icon="inbox" link="#a" no-wire-navigate active />
|
||||
<x-navigation-rail-item label="Sent" icon="send" link="#b" no-wire-navigate />
|
||||
<x-slot:footer>
|
||||
<p data-test="expanded-only">Signed in as Ada</p>
|
||||
<p data-test="collapsed-only">A</p>
|
||||
</x-slot:footer>
|
||||
</x-navigation-rail>
|
||||
<script>
|
||||
window.firstPaint = {
|
||||
value: getComputedStyle(document.querySelector('[data-md-navigation-rail]')).getPropertyValue('--md-navigation-rail-value').trim(),
|
||||
expandedOnly: getComputedStyle(document.querySelector('[data-test="expanded-only"]')).display,
|
||||
collapsedOnly: getComputedStyle(document.querySelector('[data-test="collapsed-only"]')).display,
|
||||
alpine: typeof window.Alpine,
|
||||
};
|
||||
</script>
|
||||
<main>
|
||||
<button type="button" id="open-rail" x-data x-on:click="$store.rail.show()">Open navigation</button>
|
||||
<p data-test="outside">Outside the rail</p>
|
||||
</main>
|
||||
</div>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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('<x-loading class="app-upload-spinner" label="Uploading" />'))
|
||||
->toContain('class="app-upload-spinner"')
|
||||
->toContain('aria-label="Uploading"');
|
||||
});
|
||||
|
||||
it('draws a size from M3\'s responsive range, 24 to 240px', function () {
|
||||
$this->blade('<x-loading size="96" />')->assertSee('style="--md-loading-size: 96px"', false);
|
||||
$this->blade('<x-loading size="24" />')->assertSee('style="--md-loading-size: 24px"', false);
|
||||
$this->blade('<x-loading size="240" />')->assertSee('style="--md-loading-size: 240px"', false);
|
||||
|
||||
foreach (['16', '480', 'large'] as $outside) {
|
||||
$this->blade("<x-loading size=\"{$outside}\" />")->assertDontSee('--md-loading-size', false);
|
||||
}
|
||||
});
|
||||
|
||||
it('sits on a primary-container circle when contained', function () {
|
||||
expect((string) $this->blade('<x-loading contained />'))->toContain('data-md-contained')
|
||||
->and((string) $this->blade('<x-loading />'))->not->toContain('data-md-contained')
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user