diff --git a/resources/css/components.css b/resources/css/components.css index 3ea05df8..adc4de27 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -70,3 +70,4 @@ /* Navigation */ @import './components/app-bar.css'; +@import './components/toolbar.css'; diff --git a/resources/css/components/toolbar.css b/resources/css/components/toolbar.css index 8cdc6eea..7e355791 100644 --- a/resources/css/components/toolbar.css +++ b/resources/css/components/toolbar.css @@ -1,6 +1,12 @@ /* - * M3 Expressive's toolbars (DockedToolbarTokens, FloatingToolbarTokens, androidx Compose Material 3, - * Apache-2.0). + * ``: M3 Expressive's toolbars (DockedToolbarTokens.kt, FloatingToolbarTokens.kt, + * androidx Compose Material 3, Apache-2.0; docs/reference/m3/components-navigation-selection-inputs.md + * § Toolbars). + * + * [data-md-toolbar] role="toolbar"; data-md-variant, data-md-vertical, data-md-vibrant, + * data-md-rounded, data-md-toolbar-place + * [data-md-toolbar-fab] a FAB docked at the end, flat on the bar (elevation 0) + * [data-md-toolbar-group] a floating toolbar and its own FAB, side by side (or stacked) * * docked a 64px bar across the bottom of the screen in surface-container, square, its * controls spread out 4 to 32px apart between 16px ends; with a FAB, the controls @@ -9,174 +15,178 @@ * 8px at its ends and 4px between controls; `vertical` stands it on end * * Placed over the page, a horizontal toolbar keeps 16dp from the window's edge and a vertical one - * 24dp, which is M3's minimum for each. + * 24dp, which is M3's minimum for each (N-12). * * On web and large screens a docked toolbar "can be rounded", and dividers can organise its - * controls into groups (docs/reference/m3/components-navigation-selection-inputs.md § Toolbars): - * `rounded` does the first from `expanded`, and a divider in the slot the second at any width. + * controls into groups: `rounded` does the first from `expanded` (840px), and a divider in the + * slot the second at any width. * - * Unlayered on purpose: a toolbar recolours the icon buttons inside it — primary in a standard - * one, on-primary-container in a vibrant one — and they draw their ink as utilities, which a rule - * in any layer loses to. + * `` and `` draw their own ink from `data-md-color`/`data-md-variant`, not a + * utility, so a toolbar's recolouring rules — vibrant's icon buttons, a standard toolbar's primary + * ones (N-13), a docked FAB's flattened shadow — sit in this file's own `material.components` + * layer like everything else: each only overrides the single `color`/`background-color`/ + * `box-shadow` declaration button.css or fab.css puts on its own base selector, so a toolbar's + * more specific selector always wins without needing to sit outside the layer. */ -[data-toolbar] { - display: flex; - align-items: center; - gap: 0.25rem; -} +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; -[data-toolbar][data-variant="docked"] { - width: 100%; - min-height: calc(4rem + var(--material-safe-bottom, env(safe-area-inset-bottom))); - justify-content: center; - column-gap: clamp(0.25rem, 4vw, 2rem); - padding-inline: 1rem; - padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom)); - background-color: var(--md-sys-color-surface-container); - color: var(--md-sys-color-on-surface-variant); -} +@layer material.components { + [data-md-toolbar] { + display: flex; + align-items: center; + gap: var(--md-sys-measurement-space50); + } -[data-toolbar][data-variant="floating"] { - display: inline-flex; - height: 4rem; - padding-inline: 0.5rem; - border-radius: var(--md-sys-shape-corner-full); - background-color: var(--md-sys-color-surface-container); - color: var(--md-sys-color-on-surface-variant); - box-shadow: var(--md-sys-elevation-3); -} + [data-md-toolbar][data-md-variant='docked'] { + width: 100%; + min-height: calc(64px + var(--material-safe-bottom, env(safe-area-inset-bottom))); + justify-content: center; + column-gap: clamp(var(--md-sys-measurement-space50), 4vw, var(--md-sys-measurement-space400)); + padding-inline: var(--md-sys-measurement-space200); + padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom)); + background-color: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface-variant); + } -[data-toolbar][data-variant="floating"][data-vertical] { - width: 4rem; - height: auto; - flex-direction: column; - padding-block: 0.5rem; - padding-inline: 0; -} - -[data-toolbar][data-vibrant] { - background-color: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); -} - -[data-toolbar][data-vibrant] [data-md-icon-button]:not([aria-pressed="true"]) { - color: var(--md-sys-color-on-primary-container); -} - -[data-toolbar][data-vibrant] [data-md-icon-button][aria-pressed="true"] { - background-color: var(--md-sys-color-surface-container); - color: var(--md-sys-color-on-surface); -} - -/* M3's colour list for a standard toolbar ends "Standard button (Primary)", as the vibrant list - ends "Standard button (On primary container)" — the row above. An icon button carries its own - ink as a utility, so the container's `color` never reaches it and this rule has to. */ -[data-toolbar]:not([data-vibrant]) [data-md-icon-button]:not([aria-pressed="true"]) { - color: var(--md-sys-color-primary); -} - -/* A docked toolbar with a FAB: the controls at the start, the FAB at the end, resting on the bar - at elevation 0 — M3 puts a FAB nested in another component there, and Compose's - FloatingActionButtonDefaults.bottomAppBarFabElevation() is 0 in every state. `` draws its - shadow with utilities, which is one more reason this file is unlayered. */ -[data-toolbar][data-variant="docked"]:has(> [data-toolbar-fab]) { - justify-content: flex-start; -} - -[data-toolbar-fab] { - display: flex; - margin-inline-start: auto; -} - -[data-toolbar-fab] [data-md-fab], -[data-toolbar-fab] [data-md-fab]:hover { - box-shadow: none; -} - -/* Groups of controls, a divider between each two (``, or `` in a - vertical toolbar). The divider draws itself stretched across its row, which in a toolbar would - run it into the container's edges; here it stands as long as the 40px icon buttons it separates, - centred on them, as the large-screen docked toolbar in M3's guidelines draws it - (toolbars/guidelines § Adaptive design → Resizing). The divider is `self-stretch`, a utility, - which this unlayered rule beats. */ -[data-toolbar]:not([data-vertical]) > [role="separator"][aria-orientation="vertical"] { - align-self: center; - height: 2.5rem; -} - -[data-toolbar][data-vertical] > [role="separator"][aria-orientation="horizontal"] { - align-self: center; - width: 2.5rem; -} - -/* A floating toolbar and its FAB, side by side (or stacked, vertical). */ -[data-toolbar-group] { - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -[data-toolbar-group][data-vertical] { - flex-direction: column; -} - -/* Placed over the page: centred above the bottom edge, or centred against the end edge. - * - * A toolbar at the bottom shares the screen region with a navigation bar, so it clears the one - * publishes as --material-bottom-bar, exactly as the FAB and the snackbar do — - * `max()`, not a sum, because that height already swallows the bottom safe area; without a bar - * the safe area alone applies. A *docked* toolbar and a navigation bar must never be on screen - * together at all (docs/reference/m3/components-navigation-selection-inputs.md § Toolbars); the - * offset is the backstop, not a licence. */ -[data-toolbar-place="bottom"] { - position: fixed; - bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + 1rem); - left: 50%; - z-index: 30; - translate: -50% 0; -} - -[data-toolbar-place="end"] { - position: fixed; - top: 50%; - inset-inline-end: calc(1rem + var(--material-safe-right, env(safe-area-inset-right))); - z-index: 30; - translate: 0 -50%; -} - -/* "Horizontal toolbars should have a minimum 16dp margin from the edge of the window. Vertical - toolbars should have a minimum 24dp margin" - (docs/reference/m3/components-navigation-selection-inputs.md § Toolbars). */ -[data-toolbar][data-vertical][data-toolbar-place="end"], -[data-toolbar-group][data-vertical][data-toolbar-place="end"] { - inset-inline-end: calc(1.5rem + var(--material-safe-right, env(safe-area-inset-right))); -} - -[data-toolbar][data-variant="docked"][data-toolbar-place="bottom"] { - inset-inline: 0; - bottom: var(--material-bottom-bar, 0px); - left: 0; - translate: none; -} - -/* The large-screen docked toolbar: from `expanded` (840px) fully rounded, like the one M3 shows on - the web, and spanning what it is written in. At the bottom of the window it lifts off the edges — - 16dp from each side, M3's minimum outside padding for a toolbar, and 16px above the bottom bar or - the safe area, as a floating toolbar sits — so the curve never meets the window's frame, and the - bottom safe area, which it no longer touches, stops padding it. Below `expanded` it is the square, - full-width docked toolbar M3 requires ("Avoid applying rounded corners to the container"). */ -@media (width >= 52.5rem) { - [data-toolbar][data-variant="docked"][data-rounded] { - min-height: 4rem; - padding-bottom: 0; + [data-md-toolbar][data-md-variant='floating'] { + display: inline-flex; + height: 64px; + padding-inline: var(--md-sys-measurement-space100); border-radius: var(--md-sys-shape-corner-full); + background-color: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface-variant); + box-shadow: var(--md-sys-elevation-3); } - [data-toolbar][data-variant="docked"][data-rounded][data-toolbar-place="bottom"] { - right: calc(1rem + 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))) + 1rem); - left: calc(1rem + var(--material-safe-left, env(safe-area-inset-left))); - width: auto; + [data-md-toolbar][data-md-variant='floating'][data-md-vertical] { + width: 64px; + height: auto; + flex-direction: column; + padding-block: var(--md-sys-measurement-space100); + padding-inline: 0; + } + + [data-md-toolbar][data-md-vibrant] { + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + } + + [data-md-toolbar][data-md-vibrant] [data-md-icon-button]:not([aria-pressed='true']) { + color: var(--md-sys-color-on-primary-container); + } + + [data-md-toolbar][data-md-vibrant] [data-md-icon-button][aria-pressed='true'] { + background-color: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface); + } + + /* M3's colour list for a standard toolbar ends "Standard button (Primary)", as the vibrant list + ends "Standard button (On primary container)" — the row above (N-13). */ + [data-md-toolbar]:not([data-md-vibrant]) [data-md-icon-button]:not([aria-pressed='true']) { + color: var(--md-sys-color-primary); + } + + /* A docked toolbar with a FAB: the controls at the start, the FAB at the end, resting on the + bar at elevation 0 — M3 puts a FAB nested in another component there, and Compose's + FloatingActionButtonDefaults.bottomAppBarFabElevation() is 0 in every state. */ + [data-md-toolbar][data-md-variant='docked']:has(> [data-md-toolbar-fab]) { + justify-content: flex-start; + } + + [data-md-toolbar-fab] { + display: flex; + margin-inline-start: auto; + } + + [data-md-toolbar-fab] [data-md-fab], + [data-md-toolbar-fab] [data-md-fab]:hover { + box-shadow: none; + } + + /* Groups of controls, a divider between each two (``, or `` + in a vertical toolbar). The divider draws itself stretched across its row, which in a + toolbar would run it into the container's edges; here it stands as long as the 40px icon + buttons it separates, centred on them, as the large-screen docked toolbar in M3's + guidelines draws it (toolbars/guidelines § Adaptive design → Resizing). */ + [data-md-toolbar]:not([data-md-vertical]) > [role='separator'][aria-orientation='vertical'] { + align-self: center; + height: 40px; + } + + [data-md-toolbar][data-md-vertical] > [role='separator'][aria-orientation='horizontal'] { + align-self: center; + width: 40px; + } + + /* A floating toolbar and its FAB, side by side (or stacked, vertical). */ + [data-md-toolbar-group] { + display: inline-flex; + align-items: center; + gap: var(--md-sys-measurement-space100); + } + + [data-md-toolbar-group][data-md-vertical] { + flex-direction: column; + } + + /* Placed over the page: centred above the bottom edge, or centred against the end edge. + * + * A toolbar at the bottom shares the screen region with a navigation bar, so it clears the one + * publishes as --material-bottom-bar, exactly as the FAB and the snackbar do — + * `max()`, not a sum, because that height already swallows the bottom safe area; without a bar + * the safe area alone applies (N-02). A *docked* toolbar and a navigation bar must never be on + * screen together at all; the offset is the backstop, not a licence. + */ + [data-md-toolbar-place='bottom'] { + position: fixed; + bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200)); + left: 50%; + z-index: 30; + translate: -50% 0; + } + + [data-md-toolbar-place='end'] { + position: fixed; + top: 50%; + inset-inline-end: calc(var(--md-sys-measurement-space200) + var(--material-safe-right, env(safe-area-inset-right))); + z-index: 30; + translate: 0 -50%; + } + + /* "Horizontal toolbars should have a minimum 16dp margin from the edge of the window. Vertical + toolbars should have a minimum 24dp margin" (N-12). */ + [data-md-toolbar][data-md-vertical][data-md-toolbar-place='end'], + [data-md-toolbar-group][data-md-vertical][data-md-toolbar-place='end'] { + inset-inline-end: calc(var(--md-sys-measurement-space300) + var(--material-safe-right, env(safe-area-inset-right))); + } + + [data-md-toolbar][data-md-variant='docked'][data-md-toolbar-place='bottom'] { + inset-inline: 0; + bottom: var(--material-bottom-bar, 0px); + left: 0; + translate: none; + } + + /* The large-screen docked toolbar: from `expanded` (840px) fully rounded, like the one M3 shows + on the web, and spanning what it is written in. At the bottom of the window it lifts off the + edges — 16dp from each side, M3's minimum outside padding for a toolbar, and 16px above the + bottom bar or the safe area, as a floating toolbar sits — so the curve never meets the + window's frame, and the bottom safe area, which it no longer touches, stops padding it. + Below `expanded` it is the square, full-width docked toolbar M3 requires ("Avoid applying + rounded corners to the container"). */ + @media (width >= 840px) { + [data-md-toolbar][data-md-variant='docked'][data-md-rounded] { + min-height: 64px; + padding-bottom: 0; + border-radius: var(--md-sys-shape-corner-full); + } + + [data-md-toolbar][data-md-variant='docked'][data-md-rounded][data-md-toolbar-place='bottom'] { + 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; + } } } diff --git a/resources/css/layout/pane.css b/resources/css/layout/pane.css index 3f3ac44a..ec3704b1 100644 --- a/resources/css/layout/pane.css +++ b/resources/css/layout/pane.css @@ -20,12 +20,15 @@ * fills the room it has, M3's flexible pane. The back button of `data-md-pane-back` points the * way it goes, so it mirrors in a right-to-left document (§ Layout → Bidirectionality / RTL). * - * In `material.layout`; `hide-below`/`hide-from` come from visibility.css. + * In `material.layout`; `hide-below`/`hide-from` come from visibility.css. Imports app-bar.css and + * button.css for the pane's own top app bar and its back button (plan step 36 carry-over). */ @layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; @import './visibility.css'; +@import '../components/app-bar.css'; +@import '../components/button.css'; @layer material.layout { [data-md-pane] { diff --git a/resources/css/tailwind.css b/resources/css/tailwind.css index 68a17ab8..436e3346 100644 --- a/resources/css/tailwind.css +++ b/resources/css/tailwind.css @@ -20,4 +20,3 @@ /* Navigation — leaving in step 36 */ @import './components/tabs.css'; @import './components/navigation.css'; -@import './components/toolbar.css'; diff --git a/resources/views/components/toolbar.blade.php b/resources/views/components/toolbar.blade.php index 87cffeb2..7eb2be87 100644 --- a/resources/views/components/toolbar.blade.php +++ b/resources/views/components/toolbar.blade.php @@ -14,13 +14,13 @@ `rounded` gives a docked toolbar the form M3 lets it take on web and large screens: "the docked toolbar can be rounded. Dividers can be used to organize large amounts of items", and it "can be … placed in different parts of the page" (docs/reference/m3/components-navigation-selection-inputs.md - § Toolbars → Behaviour and guidelines; toolbars/guidelines § Adaptive design → Resizing). From `expanded` - (840px) it is a fully rounded bar that spans what it is written in, and at `place="bottom"` it - lifts clear of the window's edges; below `expanded` it is the square, full-width docked toolbar, - as M3 requires there. Separate groups of controls with `` between them (in - a vertical toolbar, ``): inside a toolbar it stands as tall as an icon button rather - than across the whole bar. The floating toolbar has no such form — it is fully rounded already, - and M3's large-screen advice for it is to show more controls, or two toolbars at opposite edges. + § Toolbars → Behaviour and guidelines). From `expanded` (840px) it is a fully rounded bar that + spans what it is written in, and at `place="bottom"` it lifts clear of the window's edges; below + `expanded` it is the square, full-width docked toolbar, as M3 requires there. Separate groups of + controls with `` between them (in a vertical toolbar, ``): + inside a toolbar it stands as tall as an icon button rather than across the whole bar. The + floating toolbar has no such form — it is fully rounded already, and M3's large-screen advice + for it is to show more controls, or two toolbars at opposite edges. A docked toolbar and a navigation bar occupy the same region of the screen and M3 says never to show both at once: the bar belongs on a primary page, the toolbar on a secondary or contextual @@ -29,7 +29,14 @@ Put `` controls in the slot (`:selected` for toggles). It is a `role="toolbar"`: the arrow keys move between its controls (resources/js/toolbar.js, - resources/css/components/toolbar.css). --}} + resources/css/components/toolbar.css). + + The toolbar renders `data-md-toolbar` with `data-md-variant`, `data-md-vertical`, + `data-md-vibrant`, `data-md-rounded` and `data-md-toolbar-place`; a FAB beside a floating + toolbar sits in `data-md-toolbar-group` (with the toolbar itself), one docked in + `data-md-toolbar-fab`. Drawn by resources/css/components/toolbar.css, which the view renders + no other component into — a caller's own ``, `` and `` bring + their own stylesheets. --}} @props([ 'variant' => 'floating', @@ -50,26 +57,26 @@ @endphp @if ($grouped) -
+
@endif
{{ $slot }} @if ($docksFab) -
{{ $fab }}
+
{{ $fab }}
@endif
diff --git a/tests/Browser/BarsTest.php b/tests/Browser/BarsTest.php index 2fe68515..83b0e755 100644 --- a/tests/Browser/BarsTest.php +++ b/tests/Browser/BarsTest.php @@ -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' + + + + + @vite(config('livewire-material.showcase.vite')) + @livewireStyles + + + + + + + + + +
+
+ @livewireScripts + + + 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(); +}); diff --git a/tests/Feature/Components/AppBarTest.php b/tests/Feature/Components/AppBarTest.php index e770fae4..292b7010 100644 --- a/tests/Feature/Components/AppBarTest.php +++ b/tests/Feature/Components/AppBarTest.php @@ -136,20 +136,20 @@ it('draws floating and docked toolbars', function () { BLADE); expect($html) - ->toContain('
') + ->toContain('
') ->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('') ->and((string) $this->blade('')) - ->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".*')) - ->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 () { diff --git a/tests/Feature/Components/NavigationStylesheetsTest.php b/tests/Feature/Components/NavigationStylesheetsTest.php index 37eda178..85ff32df 100644 --- a/tests/Feature/Components/NavigationStylesheetsTest.php +++ b/tests/Feature/Components/NavigationStylesheetsTest.php @@ -19,6 +19,7 @@ use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses; */ dataset('navigation components', [ 'app-bar', + 'toolbar', ]); /** diff --git a/tests/Feature/Components/ToolbarTest.php b/tests/Feature/Components/ToolbarTest.php index 152f7658..aa4d2f11 100644 --- a/tests/Feature/Components/ToolbarTest.php +++ b/tests/Feature/Components/ToolbarTest.php @@ -1,5 +1,7 @@ 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".*]*>\s*<\/div>.*aria-label="Bold"/s') ->and((string) $this->blade('')) - ->toContain('data-variant="floating"') - ->not->toContain('data-rounded') + ->toContain('data-md-variant="floating"') + ->not->toContain('data-md-rounded') ->and((string) $this->blade('')) - ->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']); }); diff --git a/tests/Feature/StylesheetsTest.php b/tests/Feature/StylesheetsTest.php index bd8a2919..1915cdd3 100644 --- a/tests/Feature/StylesheetsTest.php +++ b/tests/Feature/StylesheetsTest.php @@ -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)