M3 puts a FAB nested in another component at level 0, and the documented pattern puts one in the rail's header, so every documented use sat at elevation 3. The rule is unlayered: the FAB's shadow is a utility and a layer would lose to it. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-03). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
95 lines
4.6 KiB
PHP
95 lines
4.6 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('flattens a FAB nested in the rail header to elevation 0', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-navigation-rail mode="expanded">
|
|
<x-slot:header><x-fab icon="edit" tooltip-right="Compose" /></x-slot:header>
|
|
</x-navigation-rail>
|
|
BLADE);
|
|
|
|
// The hook the unlayered rule in navigation.css needs (N-03).
|
|
expect($html)->toContain('data-fab');
|
|
|
|
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
|
->toContain('[data-navigation-rail-header] [data-fab],')
|
|
->toContain('[data-navigation-rail-header] [data-fab]:hover {');
|
|
});
|
|
|
|
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>");
|
|
});
|