The centred headline was absolutely positioned 56px in from each edge — room for exactly one of M3's two trailing icon buttons, so a long title ellipsised behind them. It is now the middle column of a three-column row and shrinks against whatever is really beside it. The search container follows M3's curve, filling the row to 312dp and then only half of what is left, so the showcase drops the hand-written max-width it had been working around it with. Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-09, N-11). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
157 lines
7.2 KiB
PHP
157 lines
7.2 KiB
PHP
<?php
|
|
|
|
it('draws a small, sticky app bar with a heading, navigation and actions', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-app-bar title="Shares" subtitle="12 active">
|
|
<x-slot:navigation><button>menu</button></x-slot:navigation>
|
|
<x-slot:actions><button>search</button></x-slot:actions>
|
|
</x-app-bar>
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('<header')
|
|
->toContain('x-data="materialAppBar"')
|
|
->toContain('data-variant="small"')
|
|
->toContain('data-sticky')
|
|
->toContain('data-subtitled')
|
|
->toContain('<h1 data-app-bar-title>Shares</h1>')
|
|
->toContain('<p data-app-bar-subtitle>12 active</p>')
|
|
->toContain('<div data-app-bar-leading><button>menu</button></div>')
|
|
->toContain('<div data-app-bar-trailing><button>search</button></div>');
|
|
});
|
|
|
|
it('gives a medium or large bar a collapsed title in its row and the heading under it', function () {
|
|
$html = (string) $this->blade('<x-app-bar variant="large" title="Settings" heading="h2" :sticky="false" />');
|
|
|
|
expect($html)
|
|
->toContain('data-variant="large"')
|
|
->not->toContain('data-sticky')
|
|
->toContain('<div data-app-bar-headline aria-hidden="true" >')
|
|
->toContain('<span data-app-bar-title>Settings</span>')
|
|
->toContain('<div data-app-bar-expanded>')
|
|
->toContain('<h2 data-app-bar-title>Settings</h2>');
|
|
});
|
|
|
|
it('puts a search bar in the row of a search app bar', function () {
|
|
expect((string) $this->blade('<x-app-bar variant="search"><x-search placeholder="Search shares" /></x-app-bar>'))
|
|
->toContain('data-app-bar-search')
|
|
->toContain('placeholder="Search shares"');
|
|
});
|
|
|
|
it('falls back to a small bar and an h1 for unknown values', function () {
|
|
expect((string) $this->blade('<x-app-bar variant="huge" heading="section" title="Shares" />'))
|
|
->toContain('data-variant="small"')
|
|
->toContain('<h1 data-app-bar-title>');
|
|
});
|
|
|
|
it('draws floating and docked toolbars', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-toolbar label="Formatting" vibrant vertical place="end">
|
|
<button>B</button>
|
|
<x-slot:fab><button>+</button></x-slot:fab>
|
|
</x-toolbar>
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('<div data-toolbar-group data-vertical data-toolbar-place="end" >')
|
|
->toContain('role="toolbar"')
|
|
->toContain('materialToolbar(true)')
|
|
->toContain('data-variant="floating"')
|
|
->toContain('aria-orientation="vertical"')
|
|
->toContain('data-vibrant')
|
|
->toContain('aria-label="Formatting"')
|
|
->toContain('<button>+</button>')
|
|
->and((string) $this->blade('<x-toolbar variant="docked" vibrant vertical place="bottom"><button>B</button></x-toolbar>'))
|
|
->toContain('data-variant="docked"')
|
|
->not->toContain('data-vibrant')
|
|
->not->toContain('data-vertical')
|
|
->toContain('data-toolbar-place="bottom"')
|
|
->not->toContain('data-toolbar-group');
|
|
});
|
|
|
|
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');
|
|
|
|
expect($css)
|
|
// Not a fixed 56px inset, which fits exactly one of M3's two trailing buttons (N-11).
|
|
->not->toContain('inset-inline: 3.5rem;')
|
|
->toMatch('/\[data-variant="center"\] \[data-app-bar-row\] \{\s+display: grid;\s+grid-template-columns: 1fr auto 1fr;/')
|
|
// 312dp, then half of what is left (N-09).
|
|
->toContain('max-width: calc(19.5rem + (100% - 19.5rem) / 2);');
|
|
});
|
|
|
|
it('keeps a toolbar at the bottom clear of the navigation bar', function () {
|
|
$css = file_get_contents(__DIR__.'/../../../resources/css/components/toolbar.css');
|
|
|
|
expect($css)
|
|
// Never a sum: --material-bottom-bar already swallows the bottom safe area (N-02).
|
|
->toContain('bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + 1rem);')
|
|
->toContain('bottom: var(--material-bottom-bar, 0px);');
|
|
});
|
|
|
|
it('offers M3\'s three contrast levels, marking the one in force', function () {
|
|
expect((string) $this->blade('<x-theme-toggle mode="contrast" />'))
|
|
->toContain('data-theme-toggle="contrast"')
|
|
->toContain('aria-label="Contrast"')
|
|
// A connected button group over native radios, not the deprecated segmented button (N-04).
|
|
->toContain('data-button-group="connected"')
|
|
->toContain('name="material-contrast"')
|
|
->toContain('value="standard"')
|
|
->toContain('value="medium"')
|
|
->toContain('value="high"')
|
|
->toContain('$store.theme.setContrast(value)')
|
|
// The row follows the resolved level, so the operating system's shows under `system`.
|
|
->toContain('return this.$store.theme.resolvedContrast');
|
|
});
|
|
|
|
it('names a theme row for a screen reader and legends it on request', function () {
|
|
expect((string) $this->blade('<x-theme-toggle mode="picker" />'))
|
|
->toContain('aria-label="Theme"')
|
|
->not->toContain('<legend')
|
|
->and((string) $this->blade('<x-theme-toggle mode="picker" label="Appearance" />'))
|
|
->toContain('aria-label="Appearance"')
|
|
->toContain('<legend class="mb-2 type-label-lg text-on-surface-variant">Appearance</legend>');
|
|
});
|
|
|
|
it('switches the theme through the store in three shapes', function () {
|
|
expect((string) $this->blade('<x-theme-toggle />'))
|
|
->toContain('data-theme-toggle="toggle"')
|
|
->toContain('aria-label="Dark theme"')
|
|
// 40px drawn, 48px reached: M3's minimum target (N-01).
|
|
->toContain('touch-target')
|
|
->toContain('$store.theme.toggle()')
|
|
->and((string) $this->blade('<x-theme-toggle mode="cycle" />'))
|
|
->toContain('data-theme-toggle="cycle"')
|
|
->toContain("{ light: 'dark', dark: 'system', system: 'light' }")
|
|
->and((string) $this->blade('<x-theme-toggle mode="picker" />'))
|
|
->toContain('data-theme-toggle="picker"')
|
|
->toContain('x-model="chosen"')
|
|
->toContain('name="material-theme"')
|
|
->toContain('value="system"')
|
|
->toContain('$store.theme.set(value)');
|
|
});
|
|
|
|
it('opens an account menu from the initials of a name', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-account-menu name="Anna Maria Keller" email="anna@example.com">
|
|
<x-menu-item label="Settings" />
|
|
<x-slot:footer><x-menu-item label="Sign out" /></x-slot:footer>
|
|
</x-account-menu>
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('aria-label="Account"')
|
|
->toContain('data-account-menu')
|
|
// The 40px avatar reaches 48px, and nothing clips the pseudo-target (N-01).
|
|
->toContain('touch-target')
|
|
->not->toContain('overflow-hidden')
|
|
->toMatch('/>\s*AM\s*<\/button>/')
|
|
->toContain('anna@example.com')
|
|
->toContain('Settings')
|
|
->toContain('data-account-theme')
|
|
->toContain('Sign out')
|
|
->and((string) $this->blade('<x-account-menu avatar="/anna.jpg" :theme="false" />'))
|
|
->toContain('<img src="/anna.jpg" alt="" class="size-full rounded-corner-full object-cover" />')
|
|
->not->toContain('data-account-theme');
|
|
});
|