Merge branch 'worktree-agent-aed4618fbd0b6aeac'
This commit is contained in:
@@ -69,6 +69,27 @@ it('draws floating and docked toolbars', function () {
|
||||
->not->toContain('data-toolbar-group');
|
||||
});
|
||||
|
||||
it('sets a FAB at the end of a docked toolbar, resting flat on it', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-toolbar variant="docked" label="Editing">
|
||||
<button>Draw</button>
|
||||
<x-slot:fab><button>+</button></x-slot:fab>
|
||||
</x-toolbar>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
// Inside the toolbar, after its controls, where the arrow keys reach it too.
|
||||
->toMatch('/role="toolbar".*<button>Draw<\/button>\s*<div data-toolbar-fab><button>\+<\/button><\/div>\s*<\/div>/s')
|
||||
->not->toContain('data-toolbar-group')
|
||||
->and((string) $this->blade('<x-toolbar variant="docked"><button>Draw</button></x-toolbar>'))
|
||||
->not->toContain('data-toolbar-fab');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/toolbar.css'))
|
||||
->toMatch('/\[data-toolbar-fab\] \{\s+display: flex;\s+margin-inline-start: auto;/')
|
||||
// A nested FAB rests at elevation 0.
|
||||
->toMatch('/\[data-toolbar-fab\] \[data-fab\],\s+\[data-toolbar-fab\] \[data-fab\]:hover \{\s+box-shadow: none;/');
|
||||
});
|
||||
|
||||
it('centres a headline on a three-column row and curves the search container', function () {
|
||||
$css = file_get_contents(__DIR__.'/../../../resources/css/components/app-bar.css');
|
||||
|
||||
|
||||
@@ -97,6 +97,33 @@ it('lifts the snackbar above the bar only when there is a bar', function () {
|
||||
->not->toContain('--material-bottom-bar:');
|
||||
});
|
||||
|
||||
it('passes M3\'s tall bar through, and the bottom offset grows with it', function () {
|
||||
expect((string) $this->blade('<x-app-shell :destinations="$destinations" tall-bar />', ['destinations' => shellDestinations()]))
|
||||
->toMatch('/data-navigation-bar\s+data-tall\s/')
|
||||
->toContain('max-medium:[--material-bottom-bar:calc(5rem+')
|
||||
->not->toContain('[--material-bottom-bar:calc(4rem+')
|
||||
->and((string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]))
|
||||
->not->toContain('data-tall')
|
||||
->toContain('max-medium:[--material-bottom-bar:calc(4rem+');
|
||||
});
|
||||
|
||||
it('lets the bar leave the window while the page scrolls down', function () {
|
||||
expect((string) $this->blade('<x-app-shell :destinations="$destinations" hide-bar-on-scroll />', ['destinations' => shellDestinations()]))
|
||||
->toContain('data-hide-on-scroll')
|
||||
->toContain('x-data="materialNavigationBar"')
|
||||
->and((string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]))
|
||||
->not->toContain('data-hide-on-scroll');
|
||||
});
|
||||
|
||||
it('exposes the rail that hides when collapsed', function () {
|
||||
expect((string) $this->blade('<x-app-shell :destinations="$destinations" hide-rail-when-collapsed />', ['destinations' => shellDestinations()]))
|
||||
->toContain('data-hide-when-collapsed')
|
||||
->toContain("materialNavigationRail('adaptive', true)")
|
||||
->and((string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]))
|
||||
->not->toContain('data-hide-when-collapsed')
|
||||
->toContain("materialNavigationRail('adaptive', false)");
|
||||
});
|
||||
|
||||
it('reads the safe area and anything docked on the bar through variables an application can set', function () {
|
||||
$html = (string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use Illuminate\Support\Str;
|
||||
|
||||
it('draws a navigation landmark around its items', function () {
|
||||
expect((string) $this->blade('<x-navigation-bar><x-navigation-bar-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-bar>'))
|
||||
->toContain('<nav aria-label="Main" data-navigation-bar')
|
||||
->toMatch('/<nav\s+aria-label="Main"\s+data-navigation-bar/')
|
||||
->toContain('data-navigation-bar-items')
|
||||
->and((string) $this->blade('<x-navigation-bar label="Sections" />'))->toContain('aria-label="Sections"');
|
||||
});
|
||||
@@ -60,3 +60,46 @@ it('keeps the bar item on the label and state-layer colours M3 tokens', function
|
||||
// The indicator and the pill wash in on-secondary-container, as the rail's do (N-19).
|
||||
->toContain('[data-navigation-bar-item] :is([data-navigation-indicator], [data-navigation-pill])::before {');
|
||||
});
|
||||
|
||||
it('takes M3\'s tall container, which keeps the vertical item layout at every width', function () {
|
||||
expect((string) $this->blade('<x-navigation-bar tall><x-navigation-bar-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-bar>'))
|
||||
->toMatch('/data-navigation-bar\s+data-tall\s/')
|
||||
->and((string) $this->blade('<x-navigation-bar />'))->not->toContain('data-tall');
|
||||
|
||||
$css = file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css');
|
||||
|
||||
expect($css)
|
||||
// NavigationBarTokens.TallContainerHeight, 80dp, against the short bar's 64.
|
||||
->toMatch('/\[data-navigation-bar\]\[data-tall\] \[data-navigation-bar-items\] \{\s+min-height: 5rem;/')
|
||||
// The vertical layout is the base — a compact bar and a tall one at any width share it —
|
||||
// and only the short bar's horizontal layout is behind the 600px container query, so no
|
||||
// rule inside one reaches an item without saying `:not([data-tall])` first.
|
||||
->and(preg_match_all('/@container \(width [<>]=? 37\.5rem\) \{\n(.*?)\n \}/s', $css, $blocks) > 0)->toBeTrue()
|
||||
->and(implode("\n", $blocks[1]))->not->toMatch('/^ \[data-navigation-bar-item/m');
|
||||
});
|
||||
|
||||
it('hides on a scroll down and springs back on a scroll up', function () {
|
||||
expect((string) $this->blade('<x-navigation-bar hide-on-scroll />'))
|
||||
->toContain('data-hide-on-scroll')
|
||||
->toContain('x-data="materialNavigationBar"')
|
||||
->toContain('x-bind:data-hidden="away ? \'\' : null"')
|
||||
// Focus reaching the bar brings it back — the web's nearest answer to M3's "never hide it
|
||||
// while a screen reader is active".
|
||||
->toContain('x-on:focusin="show()"')
|
||||
->and((string) $this->blade('<x-navigation-bar />'))
|
||||
->not->toContain('data-hide-on-scroll')
|
||||
->not->toContain('materialNavigationBar');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
||||
// The spatial spring, which reduced motion zeroes along with every other duration token.
|
||||
->toMatch('/\[data-navigation-bar\]\[data-hide-on-scroll\] \{\s+transition: translate var\(--md-sys-motion-spatial-default-duration\) var\(--md-sys-motion-spatial-default\);/')
|
||||
->toMatch('/\[data-navigation-bar\]\[data-hide-on-scroll\]\[data-hidden\] \{\s+translate: 0 100%;/')
|
||||
// Unlayered, or the utility <x-app-shell> publishes the offset with would win.
|
||||
->toContain('[data-app-shell]:has([data-navigation-bar][data-hide-on-scroll][data-hidden]) {')
|
||||
->and(Str::of(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))->after('[data-app-shell]:has(')->toString())
|
||||
->not->toContain('@layer');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/js/navigation.js'))
|
||||
// Never out from under a snackbar, a bottom sheet or a drawer resting on its edge.
|
||||
->toContain("document.querySelectorAll('[data-toast], [role=\"dialog\"]')");
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ it('gives the collapsible, modal and adaptive rails a menu button and the store'
|
||||
$collapsible = (string) $this->blade('<x-navigation-rail />');
|
||||
|
||||
expect($collapsible)
|
||||
->toContain('x-data="materialNavigationRail(\'collapsible\')"')
|
||||
->toContain('x-data="materialNavigationRail(\'collapsible\', false)"')
|
||||
->toContain('data-navigation-rail-menu')
|
||||
->toContain('aria-label="Collapse navigation"')
|
||||
->toContain('aria-expanded="true"')
|
||||
@@ -63,8 +63,72 @@ it('takes M3\'s optional divider and turns the container fill off', function ()
|
||||
// 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;/");
|
||||
// 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('<x-navigation-rail width="narrow"><x-slot:header><button>FAB</button></x-slot:header><x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-rail>');
|
||||
|
||||
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('<span data-navigation-label>Inbox</span>')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" width="20rem" />'))
|
||||
->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('<x-navigation-rail mode="collapsed" align="center" />'))
|
||||
->toContain('data-align="center"')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" />'))->not->toContain('data-align')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" align="middle" />'))->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('hides a collapsible or adaptive rail entirely when told to, and only those', function () {
|
||||
$hiding = (string) $this->blade('<x-navigation-rail hide-when-collapsed />');
|
||||
|
||||
expect($hiding)
|
||||
->toContain('data-hide-when-collapsed')
|
||||
->toContain("x-data=\"materialNavigationRail('collapsible', true)\"")
|
||||
// It needs the scrim and the focus trap a modal rail has: it comes back over the page.
|
||||
->toContain('data-navigation-rail-scrim')
|
||||
->toContain('x-trap.inert.noscroll="open"')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="adaptive" hide-when-collapsed />'))
|
||||
->toContain("x-data=\"materialNavigationRail('adaptive', true)\"")
|
||||
// A modal rail is already over the page; the two fixed modes mean what they say.
|
||||
->and((string) $this->blade('<x-navigation-rail mode="modal" hide-when-collapsed />'))
|
||||
->not->toContain('data-hide-when-collapsed')
|
||||
->toContain("materialNavigationRail('modal', false)")
|
||||
->and((string) $this->blade('<x-navigation-rail mode="expanded" hide-when-collapsed />'))
|
||||
->not->toContain('data-hide-when-collapsed')
|
||||
->and((string) $this->blade('<x-navigation-rail />'))->not->toContain('data-hide-when-collapsed');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
||||
->toMatch('/\[data-navigation-rail\]\[data-hide-when-collapsed\] \{\s+@variant rail-collapsed \{\s+width: 0;/')
|
||||
// The two bands where the window, not the visitor, collapses a rail keep theirs.
|
||||
->toMatch("/@media \\(width < 37\\.5rem\\) \\{\\s+\\[data-navigation-rail='collapsible'\\]\\[data-hide-when-collapsed\\]/")
|
||||
->toMatch("/@media \\(37\\.5rem <= width < 52\\.5rem\\) \\{\\s+\\[data-navigation-rail='adaptive'\\]\\[data-hide-when-collapsed\\]/")
|
||||
// Every collapsed branch stops while the rail is open, or what comes back would be narrow.
|
||||
->and(substr_count(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'), ':not([data-open])'))
|
||||
->toBeGreaterThanOrEqual(12);
|
||||
});
|
||||
|
||||
it('flattens a FAB nested in the rail header and morphs its label', function () {
|
||||
|
||||
Reference in New Issue
Block a user