blade('', ['mode' => $mode])) ->toContain("data-navigation-rail=\"{$expected}\"") ->toContain('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(''); 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('')) ->toContain('aria-label="Expand navigation"') ->toContain('data-navigation-rail-scrim') ->toContain('x-trap.inert.noscroll="open"') ->and((string) $this->blade('')) ->not->toContain('data-navigation-rail-menu') ->not->toContain('x-data') ->and((string) $this->blade(''))->not->toContain('data-navigation-rail-menu'); }); it('keeps the header and footer out of the scrolling destinations', function () { $html = (string) $this->blade(<<<'BLADE' Brand 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('')) ->toContain('data-divider') ->toContain('data-fill="false"') ->and((string) $this->blade('')) ->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 its collapsed width where M3 asks for a bar instead (N-24). ->toMatch("/\\[data-navigation-rail='collapsible'\\] \\{\\s+width: var\\(--navigation-rail-collapsed-width\\);/"); }); it('takes M3\'s narrow collapsed width, icons alone but still named', function () { $narrow = (string) $this->blade(''); expect($narrow) ->toContain('data-width="narrow"') // A narrow rail expands to the same 16rem; `width` names one or the other, never both. ->toContain('--navigation-rail-width: 16rem') // 40px menu button and 56px FAB centred in 80px rather than in 96. ->toContain('rail-collapsed:ps-5') ->toContain('rail-collapsed:px-3') // The label is still rendered: it is what names the destination. ->toContain('Inbox') ->and((string) $this->blade('')) ->not->toContain('data-width') ->toContain('--navigation-rail-width: 20rem'); expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css')) // NavigationRailCollapsedTokens.NarrowContainerWidth, 80dp against the default 96dp. ->toMatch("/\\[data-navigation-rail\\]\\[data-width='narrow'\\] \\{\\s+--navigation-rail-collapsed-width: 5rem;/") ->toMatch('/--navigation-rail-collapsed-width: 6rem;/') // Out of the drawing, not out of the page. ->toContain("[data-navigation-rail][data-width='narrow'] [data-navigation-rail-item] [data-navigation-label] {"); }); it('centres the destinations on request, leaving the header and footer at their ends', function () { expect((string) $this->blade('')) ->toContain('data-align="center"') ->and((string) $this->blade(''))->not->toContain('data-align') ->and((string) $this->blade(''))->not->toContain('data-align'); expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css')) // Only the destinations move, and `safe` gives the top back when there are too many. ->toMatch("/\\[data-navigation-rail\\]\\[data-align='center'\\] \\[data-navigation-rail-destinations\\] \\{\\s+justify-content: safe center;/"); }); 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. 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(''); expect($html) ->toContain('toContain('wire:navigate') ->toContain('aria-current="page"') ->toContain('data-navigation-indicator') ->toContain('Inbox') ->toContain('hidden rail-collapsed:contents') ->toContain('rail-collapsed:hidden') ->toContain(', 12') ->and(substr_count($html, '>12'))->toBe(2) ->and((string) $this->blade(''))->not->toContain('wire:navigate') ->and((string) $this->blade(''))->toContain('