Files
Andreas Reinhold / reiniandClaude Opus 5 5b05e52e1c Wrap a fixed tab's long label to a second line
Plan step 46, M3 § Tabs: "Labels: single row by default; may wrap to a
max second line if needed with truncation, or use scrollable tabs to
give longer titles more room." In fixed tabs (<x-tabs>, <x-section-nav>
up to four sections) the label's text, now its own data-md-tab-text in
both views, wraps and truncates at two lines. The tab's 48px (64px
stacked) is a minimum, the bar stretches its tabs to one height, and the
content stretches with them, so the indicator stays on the divider.
Scrollable tabs keep one row. SealShare's "Two-Factor Auth" and
"Appearance" showed an ellipsis at 600px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 16:10:13 +02:00

99 lines
5.1 KiB
PHP

{{-- The navigation inside one area of an app the sections of settings, of an admin which is a
single destination in the app's own navigation.
`items`, a list of `['title' => …, 'url' => …]` with an optional `icon`, `active` and `badge`
(an item is current when `active` is true, or when its `url` is the page's). From `medium`
(600px) they
are M3's secondary tabs as links, the current one underlined; on a compact window, where a row
of them
never fits, a button naming the current section opens a menu of all of them, the current one
marked `aria-current="page"` and each with its badge. The same list is
rendered for both, and CSS shows one.
The page's URL is `Livewire::originalUrl()`: while a Livewire component on the page updates, the
request is Livewire's update endpoint, and comparing with it left no section lit.
Up to four sections share the row as fixed tabs, a title too long for its share wrapping to
a second line and truncating there, the bar growing to hold it, as M3 lets a fixed tab's label
("may wrap to a max second line if needed with truncation"); each tab renders `<x-tabs>`'s
label anatomy, `data-md-tab-label` around `data-md-tab-text` and the badge, so tabs.css draws
both alike. From five the row is M3's scrollable tab bar
each tab as wide as its own label, the set scrolling sideways, offset 52dp from the leading
edge so it reads as scrollable which is M3's own answer to a row that will not fit, and the
one its accessibility page blesses ("horizontal scrolling tabs meet accessibility requirements
because they need to increase in width to respond to label text without affecting the
layout"). An earlier version wrapped them onto a grid instead, which left the bar's divider
under the last row only and stranded the upper rows' indicators against nothing.
`label` names the navigation ("Sections"). Links use `wire:navigate` unless
`no-wire-navigate`. --}}
@props([
'items' => [],
'label' => null,
'noWireNavigate' => false,
])
@php
$label ??= __('Sections');
$page = \Livewire\Livewire::originalUrl();
$isCurrent = fn (array $item): bool => ($item['active'] ?? false) || (filled($item['url'] ?? null) && $page === url($item['url']));
$current = collect($items)->first($isCurrent) ?? ($items[0] ?? null);
// Up to four share the row as fixed tabs, a long title wrapping to a second line and truncating
// there (tabs.css); from five they are M3's scrollable tabs, which give longer titles room.
$scrollable = count($items) >= 5;
@endphp
<div {{ $attributes }} data-md-section-nav>
@if ($current)
<div data-md-section-nav-picker>
<x-livewire-material::menu :$label position="bottom-start">
<x-slot:trigger>
<button type="button" data-md-section-nav-trigger class="md-focus-ring md-type-body-lg md-ink md-text-start">
@isset($current['icon'])
<x-livewire-material::icon :name="$current['icon']" :size="20" class="md-ink-variant" />
@endisset
<span data-md-section-nav-trigger-label class="md-truncate">{{ $current['title'] }}</span>
<x-livewire-material::icon name="arrow_drop_down" class="md-ink-variant" />
</button>
</x-slot:trigger>
@foreach ($items as $item)
<x-livewire-material::menu-item :label="$item['title']" :icon="$item['icon'] ?? null" :link="$item['url']" :current="$isCurrent($item)" :badge="$item['badge'] ?? null" :no-wire-navigate="$noWireNavigate" />
@endforeach
</x-livewire-material::menu>
</div>
@endif
<nav aria-label="{{ $label }}">
<ul data-md-tabs-bar data-md-variant="secondary" @if ($scrollable) data-md-scrollable @endif>
@foreach ($items as $item)
@php($on = $isCurrent($item))
<li>
<a
href="{{ $item['url'] }}"
data-md-tab
@if ($on) aria-current="page" @endif
@unless ($noWireNavigate) wire:navigate @endunless
>
<span data-md-tab-content>
@isset($item['icon'])
<x-livewire-material::icon :name="$item['icon']" :filled="$on" :size="20" />
@endisset
<span data-md-tab-label>
<span data-md-tab-text>{{ $item['title'] }}</span>
@if (filled($item['badge'] ?? null))
<x-livewire-material::badge :value="$item['badge']" />
@endif
</span>
<span data-md-tab-indicator aria-hidden="true"></span>
</span>
</a>
</li>
@endforeach
</ul>
</nav>
</div>