Draw the app bar without Tailwind
Plan step 36 (navigation group, first stream): <x-app-bar>'s class
lists move into resources/css/components/app-bar.css, keyed on
data-md-app-bar (with data-md-variant/-sticky/-subtitled/-scrolled/
-collapsed) and its parts data-md-app-bar-row/-leading/-headline/
-title/-subtitle/-search/-trailing/-action/-overflow/-expanded. The
overflow menu (two icon buttons below 600px, four from it, N-09's
312px search cap and N-11's three-column centred row) already matched
the audit; only its hooks, units and layer needed to change. Every
length is px, spacing through the measurement tokens where one
matches (M3's own "4dp leading/trailing space" and "16dp" headline
inset had no exact token, so those stay literal). Imports button.css,
menu.css and menu-item.css for the overflow menu the view renders.
Hooks renamed: every unprefixed data-app-bar-* attribute and
data-scrolled/data-collapsed to data-md-*, updated in
tests/Feature/Components/{AppBarTest,PaneTest}.php and
tests/Browser/{BarsTest,NavigationTest}.php. app-bar.js needed no
change: it reads the bar through $root, not by hook name.
tests/Feature/Components/NavigationStylesheetsTest.php is the
navigation twin of ContainmentStylesheetsTest.php, on the same
tests/Support/ViewClasses.php rule; its dataset starts at 'app-bar'
and a 'tabs' entry (once that commit lands) will read both
tabs.blade.php and tab.blade.php, since they share one stylesheet.
Added the owed browser tests (docs/plans/material-3-browser-tests.md):
the overflow at 599/600px, keyboard in its menu, a menu closing once
its width no longer shows it, and a wire:click action from both the
icon button and the menu item form.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b7f120af98
commit
b7641a35c2
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
|
||||
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">
|
||||
@@ -11,37 +13,37 @@ it('draws a small, sticky app bar with a heading, navigation and actions', funct
|
||||
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>');
|
||||
->toContain('data-md-variant="small"')
|
||||
->toContain('data-md-sticky')
|
||||
->toContain('data-md-subtitled')
|
||||
->toContain('<h1 data-md-app-bar-title>Shares</h1>')
|
||||
->toContain('<p data-md-app-bar-subtitle>12 active</p>')
|
||||
->toContain('<div data-md-app-bar-leading><button>menu</button></div>')
|
||||
->toContain('<div data-md-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>');
|
||||
->toContain('data-md-variant="large"')
|
||||
->not->toContain('data-md-sticky')
|
||||
->toContain('<div data-md-app-bar-headline aria-hidden="true" >')
|
||||
->toContain('<span data-md-app-bar-title>Settings</span>')
|
||||
->toContain('<div data-md-app-bar-expanded>')
|
||||
->toContain('<h2 data-md-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('data-md-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>');
|
||||
->toContain('data-md-variant="small"')
|
||||
->toContain('<h1 data-md-app-bar-title>');
|
||||
});
|
||||
|
||||
it('keeps two trailing icon buttons below medium and four from it, the rest in an overflow menu', function () {
|
||||
@@ -55,17 +57,17 @@ it('keeps two trailing icon buttons below medium and four from it, the rest in a
|
||||
]" />
|
||||
BLADE);
|
||||
|
||||
preg_match('/data-app-bar-overflow="compact">(.*?)data-app-bar-overflow="medium">(.*)$/s', $html, $menus);
|
||||
preg_match('/data-md-app-bar-overflow="compact">(.*?)data-md-app-bar-overflow="medium">(.*)$/s', $html, $menus);
|
||||
[, $compact, $medium] = $menus + [null, '', ''];
|
||||
|
||||
expect($html)
|
||||
// M3's "up to four trailing icons" on a larger screen, the overflow button one of them: the
|
||||
// first three are icon buttons, and the first stays one on a compact window too, beside the
|
||||
// overflow button — M3's "up to two icon buttons" (navigation reference § Top app bar).
|
||||
->and(substr_count($html, '<span data-app-bar-action'))->toBe(3)
|
||||
->and(substr_count($html, 'data-overflows-compact'))->toBe(2)
|
||||
->and($html)->toMatch('/<span data-app-bar-action\s*>\s*<button[^>]*aria-label="Search"/')
|
||||
->toMatch('/<span data-app-bar-action\s+data-overflows-compact\s*>\s*<a [^>]*href="\/shares\/1\/share"[^>]*aria-label="Share"/')
|
||||
->and(substr_count($html, '<span data-md-app-bar-action'))->toBe(3)
|
||||
->and(substr_count($html, 'data-md-overflows-compact'))->toBe(2)
|
||||
->and($html)->toMatch('/<span data-md-app-bar-action\s*>\s*<button[^>]*aria-label="Search"/')
|
||||
->toMatch('/<span data-md-app-bar-action\s+data-md-overflows-compact\s*>\s*<a [^>]*href="\/shares\/1\/share"[^>]*aria-label="Share"/')
|
||||
->not->toMatch('/<(?:button|a)[^>]*aria-label="(?:Archive|Delete)"/')
|
||||
// Every icon button is named, tooltipped and reaches 48px.
|
||||
->toMatch('/<button[^>]*data-md-size="sm"[^>]*aria-label="Search"/')
|
||||
@@ -103,26 +105,26 @@ it('adds an overflow menu only at the widths something overflows', function () {
|
||||
$four = (string) $this->blade('<x-app-bar title="Shares" :actions="$actions" />', ['actions' => $actions(4)]);
|
||||
|
||||
expect($two)
|
||||
->not->toContain('data-app-bar-overflow')
|
||||
->not->toContain('data-overflows-compact')
|
||||
->not->toContain('data-md-app-bar-overflow')
|
||||
->not->toContain('data-md-overflows-compact')
|
||||
->not->toContain('More options')
|
||||
->and(substr_count($two, '<span data-app-bar-action'))->toBe(2)
|
||||
->and(substr_count($two, '<span data-md-app-bar-action'))->toBe(2)
|
||||
->and($four)
|
||||
->toContain('data-app-bar-overflow="compact"')
|
||||
->not->toContain('data-app-bar-overflow="medium"')
|
||||
->and(substr_count($four, '<span data-app-bar-action'))->toBe(4)
|
||||
->and(substr_count($four, 'data-overflows-compact'))->toBe(3)
|
||||
->toContain('data-md-app-bar-overflow="compact"')
|
||||
->not->toContain('data-md-app-bar-overflow="medium"')
|
||||
->and(substr_count($four, '<span data-md-app-bar-action'))->toBe(4)
|
||||
->and(substr_count($four, 'data-md-overflows-compact'))->toBe(3)
|
||||
// An empty list draws no trailing area; markup in the slot is drawn as written, never
|
||||
// overflowing.
|
||||
->and((string) $this->blade('<x-app-bar title="Shares" :actions="[]" />'))
|
||||
->not->toContain('data-app-bar-trailing')
|
||||
->not->toContain('data-md-app-bar-trailing')
|
||||
->and((string) $this->blade('<x-app-bar title="Shares"><x-slot:actions><button>a</button><button>b</button><button>c</button></x-slot:actions></x-app-bar>'))
|
||||
->toContain('<div data-app-bar-trailing><button>a</button><button>b</button><button>c</button></div>')
|
||||
->not->toContain('data-app-bar-overflow');
|
||||
->toContain('<div data-md-app-bar-trailing><button>a</button><button>b</button><button>c</button></div>')
|
||||
->not->toContain('data-md-app-bar-overflow');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/app-bar.css'))
|
||||
->toMatch('/@media \(width < 37\.5rem\) \{\s+\[data-app-bar-action\]\[data-overflows-compact\],\s+\[data-app-bar-overflow="medium"\] \{\s+display: none;/')
|
||||
->toMatch('/@media \(width >= 37\.5rem\) \{\s+\[data-app-bar-overflow="compact"\] \{\s+display: none;/');
|
||||
->toMatch('/@media \(width < 600px\) \{\s+\[data-md-app-bar-action\]\[data-md-overflows-compact\],\s+\[data-md-app-bar-overflow=\'medium\'\] \{\s+display: none;/')
|
||||
->toMatch('/@media \(width >= 600px\) \{\s+\[data-md-app-bar-overflow=\'compact\'\] \{\s+display: none;/');
|
||||
});
|
||||
|
||||
it('draws floating and docked toolbars', function () {
|
||||
@@ -172,14 +174,28 @@ it('sets a FAB at the end of a docked toolbar, resting flat on it', function ()
|
||||
});
|
||||
|
||||
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');
|
||||
$css = ComponentStylesheet::read('app-bar');
|
||||
|
||||
expect($css)
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/app-bar.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;/')
|
||||
->not->toContain('inset-inline: 56px;')
|
||||
->and($css->declarations('[data-md-app-bar][data-md-variant=\'center\'] [data-md-app-bar-row]'))
|
||||
->toBe(['display' => 'grid', '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);');
|
||||
->and($css->declarations('[data-md-app-bar-search]'))
|
||||
->toHaveKey('max-width', 'calc(312px + (100% - 312px) / 2)');
|
||||
});
|
||||
|
||||
it('collapses a medium or large bar into its row at the breakpoint its title collapses to', function () {
|
||||
$css = ComponentStylesheet::read('app-bar');
|
||||
|
||||
// Sticks at a negative top (the bar minus its measured height) so the row alone remains.
|
||||
expect($css->declarations('[data-md-app-bar][data-md-sticky]:is([data-md-variant=\'medium\'], [data-md-variant=\'large\'])'))
|
||||
->toBe(['top' => 'calc(var(--app-bar-row) - var(--app-bar-measured, var(--app-bar-height)))'])
|
||||
->and($css->declarations('[data-md-app-bar-action][data-md-overflows-compact], [data-md-app-bar-overflow=\'medium\']', ['@media (width < 600px)']))
|
||||
->toBe(['display' => 'none'])
|
||||
->and($css->declarations('[data-md-app-bar-overflow=\'compact\']', ['@media (width >= 600px)']))
|
||||
->toBe(['display' => 'none']);
|
||||
});
|
||||
|
||||
it('keeps a toolbar at the bottom clear of the navigation bar', function () {
|
||||
|
||||
Reference in New Issue
Block a user