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:
Andreas Reinhold / reini
2026-09-14 23:21:54 +02:00
co-authored by Claude Sonnet 5
parent b7f120af98
commit b7641a35c2
9 changed files with 437 additions and 155 deletions
+38 -29
View File
@@ -16,20 +16,29 @@
trailing icons" (said of the search app bar, the only count it gives for a wider window), and
lets actions at the trailing edge "collapse into an overflow menu at smaller breakpoints" and
"become visible again at larger sizes" (docs/reference/m3/components-navigation-selection-inputs.md
§ Top app bar Behaviour and guidelines; app-bars/guidelines § Trailing icon buttons, § Large
screens, § Resizing). So below `medium` (600px) the bar shows at most two icon buttons and from
there at most four, the overflow button `more_vert`, "More options" counted among them:
with more actions than that, the first one (or three) stay and the rest go into its menu. Each
width has a menu of its own holding only what overflows there, so the arrow keys never land on
a hidden row, and it is all CSS (resources/css/components/app-bar.css): nothing moves after the
page has drawn. Only the list overflows; markup in the slot is drawn as written. A slot and a
list are alternatives a slot of the same name replaces the attribute.
§ Top App Bar Behaviour and guidelines). So below `medium` (600px) the bar shows at most two
icon buttons and from there at most four, the overflow button `more_vert`, "More options"
counted among them: with more actions than that, the first one (or three) stay and the rest go
into its menu. Each width has a menu of its own holding only what overflows there, so the
arrow keys never land on a hidden row, and it is all CSS (resources/css/components/app-bar.css):
nothing moves after the page has drawn. Only the list overflows; markup in the slot is drawn
as written. A slot and a list are alternatives a slot of the same name replaces the
attribute.
Sticky at the top by default (`:sticky="false"` for one that scrolls away), under the top safe
area; the surface turns surface-container once content scrolls under it
(resources/css/components/app-bar.css, resources/js/app-bar.js). A collapsing bar needs the
window to be what scrolls: an ancestor with `overflow: hidden` or `auto` would stop it
sticking (`overflow-x: clip` is fine). --}}
sticking (`overflow-x: clip` is fine).
The root renders `data-md-app-bar` with `data-md-variant`, `data-md-sticky`,
`data-md-subtitled`, and `data-md-scrolled`/`data-md-collapsed` once the page has scrolled
under it; its row is `data-md-app-bar-row` holding `data-md-app-bar-leading`,
`data-md-app-bar-headline` (`data-md-app-bar-title`, `data-md-app-bar-subtitle`),
`data-md-app-bar-search` and `data-md-app-bar-trailing` (`data-md-app-bar-action`,
`data-md-app-bar-overflow`); a flexible bar's big title is `data-md-app-bar-expanded`. Drawn
by resources/css/components/app-bar.css, which imports button.css, menu.css and menu-item.css
for what the view renders. --}}
@props([
'title' => null,
@@ -71,30 +80,30 @@
<header
x-data="materialAppBar"
x-bind:data-scrolled="scrolled ? '' : null"
x-bind:data-collapsed="collapsed ? '' : null"
x-bind:data-md-scrolled="scrolled ? '' : null"
x-bind:data-md-collapsed="collapsed ? '' : null"
x-bind:style="measured"
data-app-bar
data-variant="{{ $variant }}"
@if ($sticky) data-sticky @endif
@if (filled($subtitle)) data-subtitled @endif
data-md-app-bar
data-md-variant="{{ $variant }}"
@if ($sticky) data-md-sticky @endif
@if (filled($subtitle)) data-md-subtitled @endif
{{ $attributes }}
>
<div data-app-bar-row>
<div data-md-app-bar-row>
@isset($navigation)
<div data-app-bar-leading>{{ $navigation }}</div>
<div data-md-app-bar-leading>{{ $navigation }}</div>
@endisset
@if ($variant === 'search')
<div data-app-bar-search>{{ $slot }}</div>
<div data-md-app-bar-search>{{ $slot }}</div>
@elseif (filled($title))
<div data-app-bar-headline @if ($flexible) aria-hidden="true" @endif>
<div data-md-app-bar-headline @if ($flexible) aria-hidden="true" @endif>
@if ($flexible)
<span data-app-bar-title>{{ $title }}</span>
<span data-md-app-bar-title>{{ $title }}</span>
@else
<{{ $heading }} data-app-bar-title>{{ $title }}</{{ $heading }}>
<{{ $heading }} data-md-app-bar-title>{{ $title }}</{{ $heading }}>
@if (filled($subtitle))
<p data-app-bar-subtitle>{{ $subtitle }}</p>
<p data-md-app-bar-subtitle>{{ $subtitle }}</p>
@endif
@endif
</div>
@@ -102,10 +111,10 @@
@if ($list !== null)
@if ($count > 0)
<div data-app-bar-trailing>
<div data-md-app-bar-trailing>
@foreach ($list as $index => $action)
@if ($index < $shown['medium'])
<span data-app-bar-action @if ($index >= $shown['compact']) data-overflows-compact @endif>
<span data-md-app-bar-action @if ($index >= $shown['compact']) data-md-overflows-compact @endif>
<x-livewire-material::button
:icon="$action['icon']"
:tooltip-bottom="$action['label']"
@@ -122,7 +131,7 @@
@foreach ($shown as $width => $kept)
@if ($count > $kept)
<span data-app-bar-overflow="{{ $width }}">
<span data-md-app-bar-overflow="{{ $width }}">
<x-livewire-material::menu :label="__('More options')" position="bottom-end">
<x-slot:trigger>
<x-livewire-material::button icon="more_vert" :tooltip-bottom="__('More options')" />
@@ -147,15 +156,15 @@
</div>
@endif
@elseif (isset($actions))
<div data-app-bar-trailing>{{ $actions }}</div>
<div data-md-app-bar-trailing>{{ $actions }}</div>
@endif
</div>
@if ($flexible && filled($title))
<div data-app-bar-expanded>
<{{ $heading }} data-app-bar-title>{{ $title }}</{{ $heading }}>
<div data-md-app-bar-expanded>
<{{ $heading }} data-md-app-bar-title>{{ $title }}</{{ $heading }}>
@if (filled($subtitle))
<p data-app-bar-subtitle>{{ $subtitle }}</p>
<p data-md-app-bar-subtitle>{{ $subtitle }}</p>
@endif
</div>
@endif