Files
livewire-material/resources/views/components/app-bar.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 092100c6ae Overflow an app bar's trailing actions below medium
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
2026-09-14 10:57:50 +02:00

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>