diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 7b1f7f17..45db719c 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -877,6 +877,18 @@ M3 Expressive toolbar, `role="toolbar"` (arrow keys move between controls). `var A docked toolbar and a navigation bar occupy the same screen region and must never be on screen together: show the bar on a primary page and the toolbar on a secondary or contextual one. A `place="bottom"` toolbar clears `--material-bottom-bar` if a bar is there anyway, so nothing is buried. +Large screens: `rounded` gives a *docked* toolbar M3's web/large-screen form — from `expanded` (840px) fully rounded and spanning its container (at `place="bottom"` it lifts 16px off the window's edges); below `expanded` it stays the square full-width bar M3 requires. Divide groups of controls with `` (`` in a vertical toolbar); inside a toolbar it stands as tall as the icon buttons. A floating toolbar is fully rounded already and has no large-screen form: M3 lets it show more controls there, or splits the actions into two toolbars at opposite edges. + +```blade + + + + + + + +``` + ### ``, `` M3 tabs with a server-rendered tablist (arrow keys, Home/End, disabled tabs skipped, the indicator moves in a view transition). `tabs`: `['name', 'label', 'icon', 'badge', 'disabled']`; panels are `` in the slot. Bind with `wire:model` (entangled), or `selected` / `x-model` without Livewire. `variant` `primary` (default) or `secondary`; `stacked` (icon over label), `scrollable`. Give two identical tab sets on one page distinct `id`s. diff --git a/resources/css/components/toolbar.css b/resources/css/components/toolbar.css index d523c17f..14e95ab2 100644 --- a/resources/css/components/toolbar.css +++ b/resources/css/components/toolbar.css @@ -11,6 +11,10 @@ * 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. * + * 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. + * * 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. @@ -90,6 +94,22 @@ 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; @@ -139,3 +159,24 @@ 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; + border-radius: var(--md-sys-shape-corner-full); + } + + [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; + } +} diff --git a/resources/views/components/toolbar.blade.php b/resources/views/components/toolbar.blade.php index d4af1c60..8afb5592 100644 --- a/resources/views/components/toolbar.blade.php +++ b/resources/views/components/toolbar.blade.php @@ -11,6 +11,17 @@ one beside a floating toolbar keeps its own. It is one of the toolbar's controls then, so the arrow keys reach it. `label` names it for screen readers. + `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. + 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 one. Either way a toolbar placed at `bottom` clears `--material-bottom-bar`, so it is never @@ -26,12 +37,14 @@ 'vertical' => false, 'place' => null, 'label' => null, + 'rounded' => false, ]) @php $variant = $variant === 'docked' ? 'docked' : 'floating'; $vertical = $vertical && $variant === 'floating'; $place = in_array($place, ['bottom', 'end'], true) ? $place : null; + $rounded = $rounded && $variant === 'docked'; $grouped = isset($fab) && $variant === 'floating'; $docksFab = isset($fab) && $variant === 'docked'; @endphp @@ -48,6 +61,7 @@ data-variant="{{ $variant }}" @if ($vertical) data-vertical aria-orientation="vertical" @endif @if ($vibrant && $variant === 'floating') data-vibrant @endif + @if ($rounded) data-rounded @endif @if ($place && ! $grouped) data-toolbar-place="{{ $place }}" @endif @if (filled($label)) aria-label="{{ $label }}" @endif {{ $attributes }} diff --git a/resources/views/showcase/sections/bars.blade.php b/resources/views/showcase/sections/bars.blade.php index 668baf6c..5ead9279 100644 --- a/resources/views/showcase/sections/bars.blade.php +++ b/resources/views/showcase/sections/bars.blade.php @@ -96,6 +96,19 @@ + + {{-- M3's docked toolbar on a large screen: rounded from 840px, its groups of controls divided. Narrower, it is the square docked bar. --}} +
+ + + + + + + + + +
BLADE, 'Primary tabs' => <<<'BLADE' diff --git a/tests/Feature/Components/ToolbarTest.php b/tests/Feature/Components/ToolbarTest.php new file mode 100644 index 00000000..8bd2e268 --- /dev/null +++ b/tests/Feature/Components/ToolbarTest.php @@ -0,0 +1,47 @@ +blade(<<<'BLADE' + + + + + + BLADE); + + expect($docked) + ->toMatch('/role="toolbar"[^>]*data-variant="docked"\s+data-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') + ->and((string) $this->blade('')) + ->not->toContain('data-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'); + + preg_match('/@media \(width >= 52\.5rem\) \{(.*)\}\s*$/s', $css, $expanded); + + expect($expanded[1] ?? '') + // "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;/') + // 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]'); +}); + +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;/'); +});