The actions stream has since given the small extended FAB an 80px minimum width, which the morph would have kept once the label closed. Dropping that minimum and squaring the box against the height its size already sets gives back the 56, 80 or 96px FAB without this file naming any of those numbers. Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-23). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
108 lines
5.8 KiB
PHP
108 lines
5.8 KiB
PHP
<?php
|
|
|
|
it('draws each mode, falling back to collapsible', function (string $mode, string $expected) {
|
|
expect((string) $this->blade('<x-navigation-rail :mode="$mode"><x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-rail>', ['mode' => $mode]))
|
|
->toContain("data-navigation-rail=\"{$expected}\"")
|
|
->toContain('<nav')
|
|
->toContain('aria-label="Main"');
|
|
})->with([
|
|
['collapsed', 'collapsed'],
|
|
['expanded', 'expanded'],
|
|
['collapsible', 'collapsible'],
|
|
['modal', 'modal'],
|
|
['adaptive', 'adaptive'],
|
|
['sideways', 'collapsible'],
|
|
]);
|
|
|
|
it('gives the collapsible, modal and adaptive rails a menu button and the store', function () {
|
|
$collapsible = (string) $this->blade('<x-navigation-rail />');
|
|
|
|
expect($collapsible)
|
|
->toContain('x-data="materialNavigationRail(\'collapsible\')"')
|
|
->toContain('data-navigation-rail-menu')
|
|
->toContain('aria-label="Collapse navigation"')
|
|
->toContain('aria-expanded="true"')
|
|
// The 40px menu button reaches M3's 48px target through the shared utility (N-01).
|
|
->toContain('touch-target')
|
|
->not->toContain('data-navigation-rail-scrim')
|
|
->not->toContain('x-trap')
|
|
->and((string) $this->blade('<x-navigation-rail mode="modal" />'))
|
|
->toContain('aria-label="Expand navigation"')
|
|
->toContain('data-navigation-rail-scrim')
|
|
->toContain('x-trap.inert.noscroll="open"')
|
|
->and((string) $this->blade('<x-navigation-rail mode="expanded" />'))
|
|
->not->toContain('data-navigation-rail-menu')
|
|
->not->toContain('x-data')
|
|
->and((string) $this->blade('<x-navigation-rail mode="collapsible" :menu="false" />'))->not->toContain('data-navigation-rail-menu');
|
|
});
|
|
|
|
it('keeps the header and footer out of the scrolling destinations', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-navigation-rail mode="expanded" width="20rem">
|
|
<x-slot:brand><span>Brand</span></x-slot:brand>
|
|
<x-slot:header><button>FAB</button></x-slot:header>
|
|
<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" />
|
|
<x-slot:footer><button>Account</button></x-slot:footer>
|
|
</x-navigation-rail>
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('--navigation-rail-width: 20rem')
|
|
->toMatch('/data-navigation-rail-header.*Brand.*FAB.*data-navigation-rail-destinations.*Inbox.*data-navigation-rail-footer.*Account/s');
|
|
});
|
|
|
|
it('takes M3\'s optional divider and turns the container fill off', function () {
|
|
expect((string) $this->blade('<x-navigation-rail mode="expanded" divider :fill="false" />'))
|
|
->toContain('data-divider')
|
|
->toContain('data-fill="false"')
|
|
->and((string) $this->blade('<x-navigation-rail mode="expanded" />'))
|
|
->not->toContain('data-divider')
|
|
->not->toContain('data-fill');
|
|
|
|
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
|
// 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;/");
|
|
});
|
|
|
|
it('flattens a FAB nested in the rail header and morphs its label', function () {
|
|
// Both rules hang off `data-fab` on <x-fab>'s root, and both are unlayered, because what they
|
|
// beat — the FAB's shadow, its gap, an extended FAB's minimum width — are utilities.
|
|
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
|
// A nested FAB rests at elevation 0, not the 3 a standalone one has (N-03).
|
|
->toContain('[data-navigation-rail-header] [data-fab],')
|
|
->toContain('[data-navigation-rail-header] [data-fab]:hover {')
|
|
// One FAB whose label springs shut, not two swapped by display (N-23).
|
|
->toContain('[data-navigation-rail-header] [data-fab] > span {')
|
|
->toMatch('/@variant rail-collapsed \{\s+min-width: 0;\s+aspect-ratio: 1;\s+gap: 0;/');
|
|
});
|
|
|
|
it('draws a destination in both shapes, with its count on the icon and at the end', function () {
|
|
$html = (string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" active badge="12" />');
|
|
|
|
expect($html)
|
|
->toContain('<a data-navigation-rail-item')
|
|
->toContain('wire:navigate')
|
|
->toContain('aria-current="page"')
|
|
->toContain('data-navigation-indicator')
|
|
->toContain('<span data-navigation-label>Inbox</span>')
|
|
->toContain('hidden rail-collapsed:contents')
|
|
->toContain('rail-collapsed:hidden')
|
|
->toContain('<span class="sr-only">, 12</span>')
|
|
->and(substr_count($html, '>12</span>'))->toBe(2)
|
|
->and((string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" no-wire-navigate />'))->not->toContain('wire:navigate')
|
|
->and((string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" />'))->toContain('<button data-navigation-rail-item');
|
|
});
|
|
|
|
it('groups destinations under a heading that names the group', function () {
|
|
$html = (string) $this->blade('<x-navigation-rail-section label="Labels"><x-navigation-rail-item label="Travel" icon="flight" link="/travel" /></x-navigation-rail-section>');
|
|
|
|
preg_match('/aria-labelledby="([^"]+)"/', $html, $labelledBy);
|
|
|
|
expect($html)->toContain('role="group"')
|
|
->and($labelledBy[1] ?? null)->not->toBeNull()
|
|
->and($html)->toContain("<p id=\"{$labelledBy[1]}\" data-navigation-rail-heading>Labels</p>");
|
|
});
|