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
176 lines
9.1 KiB
PHP
176 lines
9.1 KiB
PHP
{{-- M3 Expressive's top app bar: the title of the screen, a navigation button and its actions.
|
|
|
|
`variant`: `small` (the default, 64px), `center` (the title centred), `medium` and `large` (a
|
|
big title under the row that collapses into the small one as the page scrolls), and `search`
|
|
(a search bar fills the row: put an `<x-search>` in the slot). `title` and `subtitle`; the
|
|
title is an `<h1>` unless `heading` names another element. Slots: `navigation` (the leading
|
|
icon button: back, or the menu) and `actions` (trailing icon buttons, an avatar).
|
|
|
|
`actions` can instead be a list, `:actions="[['label' => 'Share', 'icon' => 'share',
|
|
'wire:click' => 'share'], …]"`, in the order of use — M3 puts the most used nearest the
|
|
headline. Each entry takes what `<x-menu-item>` takes: `label` and `icon`, `link` (with
|
|
`external` or `no-wire-navigate`), `disabled`, `selected`, and any other key — `wire:click`,
|
|
`x-on:click` — as an attribute. Every entry is rendered twice, as an icon button named and
|
|
tooltipped by its label and as a menu item, and the window's width decides which one shows.
|
|
M3 allows "up to two icon buttons" after the headline, gives a larger screen "up to four
|
|
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). 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).
|
|
|
|
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,
|
|
'subtitle' => null,
|
|
'variant' => 'small',
|
|
'sticky' => true,
|
|
'heading' => 'h1',
|
|
'actions' => null,
|
|
])
|
|
|
|
@php
|
|
$variant = in_array($variant, ['small', 'center', 'medium', 'large', 'search'], true) ? $variant : 'small';
|
|
$flexible = in_array($variant, ['medium', 'large'], true);
|
|
$heading = in_array($heading, ['h1', 'h2', 'h3', 'div', 'p'], true) ? $heading : 'h1';
|
|
|
|
// The list form of `actions`, each entry split into the props an icon button and a menu item
|
|
// share and the attributes (its action) both of them carry.
|
|
$list = is_iterable($actions) ? collect($actions)->values()->map(fn (array $action): array => [
|
|
'label' => $action['label'] ?? null,
|
|
'icon' => $action['icon'] ?? null,
|
|
'link' => $action['link'] ?? null,
|
|
'external' => (bool) ($action['external'] ?? false),
|
|
'noWireNavigate' => (bool) ($action['noWireNavigate'] ?? $action['no-wire-navigate'] ?? false),
|
|
'disabled' => (bool) ($action['disabled'] ?? false),
|
|
'selected' => $action['selected'] ?? null,
|
|
'attributes' => new \Illuminate\View\ComponentAttributeBag(\Illuminate\Support\Arr::except($action, [
|
|
'label', 'icon', 'link', 'external', 'noWireNavigate', 'no-wire-navigate', 'disabled', 'selected',
|
|
])),
|
|
])->all() : null;
|
|
|
|
// How many stay icon buttons: two trailing icon buttons below `medium`, four from it, the
|
|
// overflow button one of them whenever it is there.
|
|
$count = count($list ?? []);
|
|
$shown = [
|
|
'compact' => $count > 2 ? 1 : $count,
|
|
'medium' => $count > 4 ? 3 : $count,
|
|
];
|
|
@endphp
|
|
|
|
<header
|
|
x-data="materialAppBar"
|
|
x-bind:data-md-scrolled="scrolled ? '' : null"
|
|
x-bind:data-md-collapsed="collapsed ? '' : null"
|
|
x-bind:style="measured"
|
|
data-md-app-bar
|
|
data-md-variant="{{ $variant }}"
|
|
@if ($sticky) data-md-sticky @endif
|
|
@if (filled($subtitle)) data-md-subtitled @endif
|
|
{{ $attributes }}
|
|
>
|
|
<div data-md-app-bar-row>
|
|
@isset($navigation)
|
|
<div data-md-app-bar-leading>{{ $navigation }}</div>
|
|
@endisset
|
|
|
|
@if ($variant === 'search')
|
|
<div data-md-app-bar-search>{{ $slot }}</div>
|
|
@elseif (filled($title))
|
|
<div data-md-app-bar-headline @if ($flexible) aria-hidden="true" @endif>
|
|
@if ($flexible)
|
|
<span data-md-app-bar-title>{{ $title }}</span>
|
|
@else
|
|
<{{ $heading }} data-md-app-bar-title>{{ $title }}</{{ $heading }}>
|
|
@if (filled($subtitle))
|
|
<p data-md-app-bar-subtitle>{{ $subtitle }}</p>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
@if ($list !== null)
|
|
@if ($count > 0)
|
|
<div data-md-app-bar-trailing>
|
|
@foreach ($list as $index => $action)
|
|
@if ($index < $shown['medium'])
|
|
<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']"
|
|
:link="$action['link']"
|
|
:external="$action['external']"
|
|
:no-wire-navigate="$action['noWireNavigate']"
|
|
:disabled="$action['disabled']"
|
|
:selected="$action['selected']"
|
|
:attributes="$action['attributes']"
|
|
/>
|
|
</span>
|
|
@endif
|
|
@endforeach
|
|
|
|
@foreach ($shown as $width => $kept)
|
|
@if ($count > $kept)
|
|
<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')" />
|
|
</x-slot:trigger>
|
|
|
|
@foreach (array_slice($list, $kept) as $action)
|
|
<x-livewire-material::menu-item
|
|
:label="$action['label']"
|
|
:icon="$action['icon']"
|
|
:link="$action['link']"
|
|
:external="$action['external']"
|
|
:no-wire-navigate="$action['noWireNavigate']"
|
|
:disabled="$action['disabled']"
|
|
:selected="$action['selected']"
|
|
:attributes="$action['attributes']"
|
|
/>
|
|
@endforeach
|
|
</x-livewire-material::menu>
|
|
</span>
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
@elseif (isset($actions))
|
|
<div data-md-app-bar-trailing>{{ $actions }}</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($flexible && filled($title))
|
|
<div data-md-app-bar-expanded>
|
|
<{{ $heading }} data-md-app-bar-title>{{ $title }}</{{ $heading }}>
|
|
@if (filled($subtitle))
|
|
<p data-md-app-bar-subtitle>{{ $subtitle }}</p>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
@if ($variant !== 'search' && $slot->hasActualContent())
|
|
{{ $slot }}
|
|
@endif
|
|
</header>
|