Draw the toolbar without Tailwind
Plan step 36 (navigation group, second stream): <x-toolbar>'s class
lists move into resources/css/components/toolbar.css, keyed on
data-md-toolbar (data-md-variant/-vertical/-vibrant/-rounded/
-toolbar-place) and data-md-toolbar-group/-toolbar-fab. The docked
form with a FAB (N-02's --material-bottom-bar clearance), the vertical
24dp margin (N-12), the standard toolbar's primary icon buttons
(N-13), and the rounded large-screen form with dividers from 840px
were all already correct; only their hooks, units and layer needed to
change. The file no longer sits outside every layer: button.css and
fab.css now draw their own ink through data-md-* attributes rather
than a Tailwind utility, so a toolbar's more specific selector always
beats their single base color/background-color/box-shadow declaration
without needing to escape the cascade layers.
Sizes stay px (64px row, 40px divider); spacing goes through the
measurement tokens where one matches the value (4px leading/trailing
gap, 16/32px docked spread, 8px floating ends, 16/24px placed
margins).
Hooks renamed: every unprefixed data-toolbar* attribute to data-md-*,
updated in tests/Feature/Components/{AppBarTest,ToolbarTest}.php and
tests/Browser/BarsTest.php. toolbar.js needed no change: it reads the
bar through $root and role="toolbar", not by hook name.
Carry-over from the app-bar step: layout/pane.css now imports
app-bar.css and button.css for the pane's own top app bar and back
button, now that both are rewritten. That import crosses into
material.components for the first time from a layout file, so
tests/Feature/StylesheetsTest.php's two layout-shape checks now skip
non-`layout/` files reached through it — each already covered by its
own group's stylesheet test.
Added the owed browser tests (docs/plans/material-3-browser-tests.md):
a docked toolbar clear of the navigation bar below medium, and the
rounded form with its divider height from 840px, clear of the
window's edges.
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
b7641a35c2
commit
9c134b2dc7
@@ -246,3 +246,59 @@ it('runs a wire:click action from either the icon button or its overflow menu it
|
||||
->click('[role="menuitem"]:has-text("Star")')
|
||||
->assertSeeIn('#stars', '2');
|
||||
});
|
||||
|
||||
function toolbarScaffoldProbe()
|
||||
{
|
||||
Route::middleware('web')->get('/toolbar-scaffold-probe', fn () => Blade::render(<<<'BLADE'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<x-theme-script />
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="bg-surface">
|
||||
<x-scaffold :destinations="[
|
||||
['title' => 'Inbox', 'icon' => 'inbox', 'url' => '/toolbar-scaffold-probe', 'active' => true],
|
||||
['title' => 'Sent', 'icon' => 'send', 'url' => '/toolbar-scaffold-probe'],
|
||||
]">
|
||||
<x-toolbar variant="docked" rounded place="bottom" label="Formatting" id="toolbar">
|
||||
<x-button icon="format_bold" tooltip="Bold" />
|
||||
<x-divider vertical />
|
||||
<x-button icon="format_italic" tooltip="Italic" />
|
||||
</x-toolbar>
|
||||
|
||||
<div style="height: 200vh"></div>
|
||||
</x-scaffold>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
BLADE));
|
||||
|
||||
return visit('/toolbar-scaffold-probe')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
it('keeps a docked toolbar clear of the navigation bar below medium', function () {
|
||||
toolbarScaffoldProbe()->resize(599, 800)
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-app-shell-bar]')).display !== 'none'")
|
||||
// A docked toolbar at place="bottom" sits above --material-bottom-bar, never on top of it.
|
||||
->assertScript("document.querySelector('#toolbar').getBoundingClientRect().bottom <= document.querySelector('[data-app-shell-bar]').getBoundingClientRect().top")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
it('rounds a docked toolbar and its divider from 840px, clear of the window\'s edges', function () {
|
||||
$page = toolbarScaffoldProbe()->resize(800, 800);
|
||||
|
||||
$page->assertScript("getComputedStyle(document.querySelector('#toolbar')).borderRadius === '0px'")
|
||||
->assertScript("Math.round(document.querySelector('#toolbar').getBoundingClientRect().left) === 0")
|
||||
->assertScript("Math.round(document.querySelector('#toolbar').getBoundingClientRect().right) === Math.round(window.innerWidth)");
|
||||
|
||||
$page->resize(1024, 800)
|
||||
->assertScript("getComputedStyle(document.querySelector('#toolbar')).borderRadius !== '0px'")
|
||||
->assertScript("document.querySelector('#toolbar').getBoundingClientRect().left >= 16")
|
||||
->assertScript("window.innerWidth - document.querySelector('#toolbar').getBoundingClientRect().right >= 16")
|
||||
->assertScript("window.innerHeight - document.querySelector('#toolbar').getBoundingClientRect().bottom >= 16")
|
||||
->assertScript("getComputedStyle(document.querySelector('#toolbar [role=\"separator\"]')).height === '40px'")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
@@ -136,20 +136,20 @@ it('draws floating and docked toolbars', function () {
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('<div data-toolbar-group data-vertical data-toolbar-place="end" >')
|
||||
->toContain('<div data-md-toolbar-group data-md-vertical data-md-toolbar-place="end" >')
|
||||
->toContain('role="toolbar"')
|
||||
->toContain('materialToolbar(true)')
|
||||
->toContain('data-variant="floating"')
|
||||
->toContain('data-md-variant="floating"')
|
||||
->toContain('aria-orientation="vertical"')
|
||||
->toContain('data-vibrant')
|
||||
->toContain('data-md-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');
|
||||
->toContain('data-md-variant="docked"')
|
||||
->not->toContain('data-md-vibrant')
|
||||
->not->toContain('data-md-vertical')
|
||||
->toContain('data-md-toolbar-place="bottom"')
|
||||
->not->toContain('data-md-toolbar-group');
|
||||
});
|
||||
|
||||
it('sets a FAB at the end of a docked toolbar, resting flat on it', function () {
|
||||
@@ -162,15 +162,17 @@ it('sets a FAB at the end of a docked toolbar, resting flat on it', function ()
|
||||
|
||||
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')
|
||||
->toMatch('/role="toolbar".*<button>Draw<\/button>\s*<div data-md-toolbar-fab><button>\+<\/button><\/div>\s*<\/div>/s')
|
||||
->not->toContain('data-md-toolbar-group')
|
||||
->and((string) $this->blade('<x-toolbar variant="docked"><button>Draw</button></x-toolbar>'))
|
||||
->not->toContain('data-toolbar-fab');
|
||||
->not->toContain('data-md-toolbar-fab');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/toolbar.css'))
|
||||
->toMatch('/\[data-toolbar-fab\] \{\s+display: flex;\s+margin-inline-start: auto;/')
|
||||
$css = ComponentStylesheet::read('toolbar');
|
||||
|
||||
expect($css->declarations('[data-md-toolbar-fab]'))->toBe(['display' => 'flex', 'margin-inline-start' => 'auto'])
|
||||
// A nested FAB rests at elevation 0.
|
||||
->toMatch('/\[data-toolbar-fab\] \[data-md-fab\],\s+\[data-toolbar-fab\] \[data-md-fab\]:hover \{\s+box-shadow: none;/');
|
||||
->and($css->declarations('[data-md-toolbar-fab] [data-md-fab], [data-md-toolbar-fab] [data-md-fab]:hover'))
|
||||
->toBe(['box-shadow' => 'none']);
|
||||
});
|
||||
|
||||
it('centres a headline on a three-column row and curves the search container', function () {
|
||||
@@ -199,16 +201,19 @@ it('collapses a medium or large bar into its row at the breakpoint its title col
|
||||
});
|
||||
|
||||
it('keeps a toolbar at the bottom clear of the navigation bar', function () {
|
||||
$css = file_get_contents(__DIR__.'/../../../resources/css/components/toolbar.css');
|
||||
$css = ComponentStylesheet::read('toolbar');
|
||||
|
||||
expect($css)
|
||||
expect($css->declarations('[data-md-toolbar-place=\'bottom\']'))
|
||||
// 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);')
|
||||
->toHaveKey('bottom', 'calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200))')
|
||||
->and($css->declarations('[data-md-toolbar][data-md-variant=\'docked\'][data-md-toolbar-place=\'bottom\']'))
|
||||
->toHaveKey('bottom', 'var(--material-bottom-bar, 0px)')
|
||||
// A vertical toolbar keeps 24dp from the edge where a horizontal one keeps 16 (N-12).
|
||||
->toContain('inset-inline-end: calc(1.5rem + var(--material-safe-right, env(safe-area-inset-right)));')
|
||||
->and($css->declarations('[data-md-toolbar][data-md-vertical][data-md-toolbar-place=\'end\'], [data-md-toolbar-group][data-md-vertical][data-md-toolbar-place=\'end\']'))
|
||||
->toBe(['inset-inline-end' => 'calc(var(--md-sys-measurement-space300) + var(--material-safe-right, env(safe-area-inset-right)))'])
|
||||
// A standard toolbar's standard buttons are primary (N-13).
|
||||
->toContain('[data-toolbar]:not([data-vibrant]) [data-md-icon-button]:not([aria-pressed="true"]) {');
|
||||
->and($css->declarations('[data-md-toolbar]:not([data-md-vibrant]) [data-md-icon-button]:not([aria-pressed=\'true\'])'))
|
||||
->toBe(['color' => 'var(--md-sys-color-primary)']);
|
||||
});
|
||||
|
||||
it('offers M3\'s three contrast levels, marking the one in force', function () {
|
||||
|
||||
@@ -19,6 +19,7 @@ use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
||||
*/
|
||||
dataset('navigation components', [
|
||||
'app-bar',
|
||||
'toolbar',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
|
||||
// The toolbar's first tests live in AppBarTest.php, beside the app bar they were written with.
|
||||
|
||||
it('gives a docked toolbar the rounded large-screen form on request, and only a docked one', function () {
|
||||
@@ -12,36 +14,42 @@ it('gives a docked toolbar the rounded large-screen form on request, and only a
|
||||
BLADE);
|
||||
|
||||
expect($docked)
|
||||
->toMatch('/role="toolbar"[^>]*data-variant="docked"\s+data-rounded/')
|
||||
->toMatch('/role="toolbar"[^>]*data-md-variant="docked"\s+data-md-rounded/')
|
||||
// The divider between two groups is the toolbar's child, where toolbar.css stands it up.
|
||||
->toMatch('/role="toolbar".*aria-label="Undo".*<div\s+data-md-divider\s+data-md-orientation="vertical"\s+role="separator" aria-orientation="vertical"[^>]*>\s*<\/div>.*aria-label="Bold"/s')
|
||||
->and((string) $this->blade('<x-toolbar rounded><x-button icon="undo" tooltip="Undo" /></x-toolbar>'))
|
||||
->toContain('data-variant="floating"')
|
||||
->not->toContain('data-rounded')
|
||||
->toContain('data-md-variant="floating"')
|
||||
->not->toContain('data-md-rounded')
|
||||
->and((string) $this->blade('<x-toolbar variant="docked"><x-button icon="undo" tooltip="Undo" /></x-toolbar>'))
|
||||
->not->toContain('data-rounded');
|
||||
->not->toContain('data-md-rounded');
|
||||
});
|
||||
|
||||
it('rounds a docked toolbar from expanded only, and lifts a placed one off the window\'s edges', function () {
|
||||
$css = file_get_contents(__DIR__.'/../../../resources/css/components/toolbar.css');
|
||||
$css = ComponentStylesheet::read('toolbar');
|
||||
|
||||
preg_match('/@media \(width >= 52\.5rem\) \{(.*)\}\s*$/s', $css, $expanded);
|
||||
|
||||
expect($expanded[1] ?? '')
|
||||
expect($css->declarations('[data-md-toolbar][data-md-variant=\'docked\'][data-md-rounded]', ['@media (width >= 840px)']))
|
||||
// "On web and large screens, the docked toolbar can be rounded" (toolbars/guidelines).
|
||||
->toMatch('/\[data-toolbar\]\[data-variant="docked"\]\[data-rounded\] \{[^}]*border-radius: var\(--md-sys-shape-corner-full\);/')
|
||||
->toMatch('/\[data-toolbar\]\[data-variant="docked"\]\[data-rounded\] \{[^}]*padding-bottom: 0;/')
|
||||
->toBe(['min-height' => '64px', 'padding-bottom' => '0', 'border-radius' => 'var(--md-sys-shape-corner-full)'])
|
||||
// 16dp from each side, M3's minimum outside padding; 16px above a bottom bar.
|
||||
->toMatch('/\[data-rounded\]\[data-toolbar-place="bottom"\] \{[^}]*right: calc\(1rem \+ var\(--material-safe-right, env\(safe-area-inset-right\)\)\);/')
|
||||
->toMatch('/\[data-rounded\]\[data-toolbar-place="bottom"\] \{[^}]*left: calc\(1rem \+ var\(--material-safe-left, env\(safe-area-inset-left\)\)\);/')
|
||||
->toMatch('/\[data-rounded\]\[data-toolbar-place="bottom"\] \{[^}]*bottom: calc\(max\(var\(--material-bottom-bar, 0px\), var\(--material-safe-bottom, env\(safe-area-inset-bottom\)\)\) \+ 1rem\);/')
|
||||
// Nothing outside the query rounds a docked toolbar: below expanded it stays square.
|
||||
->and(str_replace($expanded[0] ?? '', '', $css))
|
||||
->not->toContain('[data-rounded]');
|
||||
->and($css->declarations('[data-md-toolbar][data-md-variant=\'docked\'][data-md-rounded][data-md-toolbar-place=\'bottom\']', ['@media (width >= 840px)']))
|
||||
->toBe([
|
||||
'right' => 'calc(var(--md-sys-measurement-space200) + var(--material-safe-right, env(safe-area-inset-right)))',
|
||||
'bottom' => 'calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200))',
|
||||
'left' => 'calc(var(--md-sys-measurement-space200) + var(--material-safe-left, env(safe-area-inset-left)))',
|
||||
'width' => 'auto',
|
||||
]);
|
||||
|
||||
preg_match('/@media \(width >= 840px\) \{(.*)\}\s*\}\s*$/s', $css->css, $expanded);
|
||||
|
||||
// Nothing outside the query rounds a docked toolbar: below expanded it stays square.
|
||||
expect(str_replace($expanded[0] ?? '', '', $css->css))->not->toContain('[data-md-rounded]');
|
||||
});
|
||||
|
||||
it('stands a divider in a toolbar as tall as the buttons it separates', function () {
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/toolbar.css'))
|
||||
->toMatch('/\[data-toolbar\]:not\(\[data-vertical\]\) > \[role="separator"\]\[aria-orientation="vertical"\] \{\s+align-self: center;\s+height: 2\.5rem;/')
|
||||
->toMatch('/\[data-toolbar\]\[data-vertical\] > \[role="separator"\]\[aria-orientation="horizontal"\] \{\s+align-self: center;\s+width: 2\.5rem;/');
|
||||
$css = ComponentStylesheet::read('toolbar');
|
||||
|
||||
expect($css->declarations('[data-md-toolbar]:not([data-md-vertical]) > [role=\'separator\'][aria-orientation=\'vertical\']'))
|
||||
->toBe(['align-self' => 'center', 'height' => '40px'])
|
||||
->and($css->declarations('[data-md-toolbar][data-md-vertical] > [role=\'separator\'][aria-orientation=\'horizontal\']'))
|
||||
->toBe(['align-self' => 'center', 'width' => '40px']);
|
||||
});
|
||||
|
||||
@@ -381,8 +381,16 @@ it('opens every layout stylesheet with a header, the layer statement and plain i
|
||||
});
|
||||
|
||||
it('keeps every layout rule in material.layout, and the visibility props in material.visibility', function () {
|
||||
// layoutStylesheets() follows every @import, so it now reaches component stylesheets a layout
|
||||
// file draws on (pane.css: app-bar.css and button.css, for its own top app bar and back
|
||||
// button) — each already checked, in material.components, by its own group's stylesheet test.
|
||||
foreach (layoutStylesheets() as $file) {
|
||||
$name = stylesheetName($file);
|
||||
|
||||
if (! str_starts_with($name, 'layout/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$css = File::get($file);
|
||||
$layer = match ($name) {
|
||||
'layout/visibility.css' => 'material.visibility',
|
||||
@@ -405,8 +413,15 @@ it('keeps every layout rule in material.layout, and the visibility props in mate
|
||||
it('writes no Tailwind in the layout stylesheets, and media queries only at M3\'s breakpoints, in px', function () {
|
||||
$queries = 0;
|
||||
|
||||
// Component stylesheets reached through a layout file (see the test above) are not layout
|
||||
// stylesheets themselves, and keep their own group's rules (a hover query included).
|
||||
foreach (layoutStylesheets() as $file) {
|
||||
$name = stylesheetName($file);
|
||||
|
||||
if (! str_starts_with($name, 'layout/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$css = stylesheetWithoutComments(File::get($file));
|
||||
|
||||
expect($css)
|
||||
|
||||
Reference in New Issue
Block a user