Five or more sections wrapped onto a 3- or 4-column grid, which left the tab bar's divider under the last row only and stranded every upper row's active indicator against nothing. From five they are now what M3 prescribes and its accessibility page defends: a scrollable tab bar, each tab as wide as its label, offset 52dp. Four or fewer still share the row. The 20px icons take the 20 cut. Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-16). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
92 lines
4.7 KiB
PHP
92 lines
4.7 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. 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);
|
|
|
|
// Four fit a row at the widths this bar is used at; from five they are M3's scrollable tabs.
|
|
$scrollable = count($items) >= 5;
|
|
@endphp
|
|
|
|
<div {{ $attributes->class(['min-w-0']) }} data-section-nav>
|
|
@if ($current)
|
|
<div class="medium:hidden" data-section-picker>
|
|
<x-livewire-material::menu :$label position="bottom-start">
|
|
<x-slot:trigger>
|
|
<button type="button" class="focus-ring flex h-12 w-[min(20rem,calc(100vw-2rem))] cursor-pointer items-center gap-3 rounded-corner-xs border border-outline px-4 text-start type-body-lg text-on-surface">
|
|
@isset($current['icon'])
|
|
<x-livewire-material::icon :name="$current['icon']" optical="20" class="size-5 text-on-surface-variant" />
|
|
@endisset
|
|
<span class="min-w-0 flex-1 truncate">{{ $current['title'] }}</span>
|
|
<x-livewire-material::icon name="arrow_drop_down" class="size-6 text-on-surface-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 }}" class="max-medium:hidden">
|
|
<ul data-tabs-bar data-variant="secondary" @if ($scrollable) data-scrollable @endif>
|
|
@foreach ($items as $item)
|
|
@php($on = $isCurrent($item))
|
|
|
|
<li class="flex min-w-0">
|
|
<a
|
|
href="{{ $item['url'] }}"
|
|
data-tab
|
|
@if ($on) aria-current="page" @endif
|
|
@unless ($noWireNavigate) wire:navigate @endunless
|
|
>
|
|
<span data-tab-content>
|
|
@isset($item['icon'])
|
|
<x-livewire-material::icon :name="$item['icon']" :filled="$on" optical="20" class="size-5" />
|
|
@endisset
|
|
<span class="truncate">{{ $item['title'] }}</span>
|
|
@if (filled($item['badge'] ?? null))
|
|
<x-livewire-material::badge :value="$item['badge']" />
|
|
@endif
|
|
<span data-tab-indicator aria-hidden="true"></span>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
@endforeach
|
|
</ul>
|
|
</nav>
|
|
</div>
|