Plan step 25, item 7 (navigation Missing: "App bar, trailing-action overflow at small widths"). `<x-app-bar :actions="[...]">` takes the trailing actions as a list whose entries take <x-menu-item>'s props plus any action attribute. M3 allows up to two trailing icon buttons, and up to four on larger screens, and lets trailing actions collapse into an overflow menu at smaller breakpoints: below medium the bar keeps at most two icon buttons, from medium four, the "More options" button counted among them, the rest in its menu. Both forms render and media queries pick one, so there is no script and no flash; each width has its own menu so the arrow keys never reach a hidden row. The actions slot still takes markup and never overflows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
167 lines
8.4 KiB
PHP
167 lines
8.4 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; 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.
|
|
|
|
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). --}}
|
|
|
|
@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-scrolled="scrolled ? '' : null"
|
|
x-bind:data-collapsed="collapsed ? '' : null"
|
|
x-bind:style="measured"
|
|
data-app-bar
|
|
data-variant="{{ $variant }}"
|
|
@if ($sticky) data-sticky @endif
|
|
@if (filled($subtitle)) data-subtitled @endif
|
|
{{ $attributes }}
|
|
>
|
|
<div data-app-bar-row>
|
|
@isset($navigation)
|
|
<div data-app-bar-leading>{{ $navigation }}</div>
|
|
@endisset
|
|
|
|
@if ($variant === 'search')
|
|
<div data-app-bar-search>{{ $slot }}</div>
|
|
@elseif (filled($title))
|
|
<div data-app-bar-headline @if ($flexible) aria-hidden="true" @endif>
|
|
@if ($flexible)
|
|
<span data-app-bar-title>{{ $title }}</span>
|
|
@else
|
|
<{{ $heading }} data-app-bar-title>{{ $title }}</{{ $heading }}>
|
|
@if (filled($subtitle))
|
|
<p data-app-bar-subtitle>{{ $subtitle }}</p>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
@if ($list !== null)
|
|
@if ($count > 0)
|
|
<div data-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>
|
|
<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-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-app-bar-trailing>{{ $actions }}</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($flexible && filled($title))
|
|
<div data-app-bar-expanded>
|
|
<{{ $heading }} data-app-bar-title>{{ $title }}</{{ $heading }}>
|
|
@if (filled($subtitle))
|
|
<p data-app-bar-subtitle>{{ $subtitle }}</p>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
@if ($variant !== 'search' && $slot->hasActualContent())
|
|
{{ $slot }}
|
|
@endif
|
|
</header>
|