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>
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 16:10:13 +02:00
co-authored by Claude Opus 5
parent 0a028b158f
commit 5b05e52e1c
6 changed files with 152 additions and 20 deletions
@@ -13,7 +13,11 @@
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. From five the row is M3's scrollable tab bar
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
@@ -36,7 +40,8 @@
$isCurrent = fn (array $item): bool => ($item['active'] ?? false) || (filled($item['url'] ?? null) && $page === url($item['url']));
$current = collect($items)->first($isCurrent) ?? ($items[0] ?? null);
// Four fit a row at the widths this bar is used at; from five they are M3's scrollable tabs.
// 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
@@ -77,10 +82,12 @@
@isset($item['icon'])
<x-livewire-material::icon :name="$item['icon']" :filled="$on" :size="20" />
@endisset
<span class="md-truncate">{{ $item['title'] }}</span>
@if (filled($item['badge'] ?? null))
<x-livewire-material::badge :value="$item['badge']" />
@endif
<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>
+8 -4
View File
@@ -12,12 +12,16 @@
the label, in primary) or `secondary` (under the whole tab, for tabs inside a section);
`stacked` puts a primary tab's icon over its label (64px); `scrollable` lets tabs keep their
own width and scroll sideways instead of sharing the width equally, offset 52dp from the
leading edge so it reads as scrollable (N-10, resources/css/components/tabs.css).
leading edge so it reads as scrollable (N-10, resources/css/components/tabs.css). A fixed
tab's label too long for its share wraps to a second line and truncates there, the whole bar
growing to hold it; a scrollable tab keeps its label on one line (M3: "may wrap to a max
second line if needed with truncation, or use scrollable tabs to give longer titles more
room").
The root renders `data-md-tabs`; the bar is `data-md-tabs-bar` with `data-md-variant`,
`data-md-stacked` and `data-md-scrollable`, holding `data-md-tab` buttons (each
`data-md-tab-content`, `data-md-tab-label` and `data-md-tab-indicator`). Imports icon.css and
badge.css for what the view renders. --}}
`data-md-tab-content`, `data-md-tab-label` around the label's `data-md-tab-text` and its badge,
and `data-md-tab-indicator`). Imports icon.css and badge.css for what the view renders. --}}
@props([
'tabs' => [],
@@ -84,7 +88,7 @@
<x-livewire-material::icon :name="$tab['icon']" />
@endisset
<span data-md-tab-label>
{{ $tab['label'] }}
<span data-md-tab-text>{{ $tab['label'] }}</span>
@if (filled($tab['badge'] ?? null))
<x-livewire-material::badge :value="$tab['badge']" />
@endif