Files
livewire-material/resources/views/components/tabs.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 348803484a Keep a deferred wire:model on the tabs deferred
Plan step 36, navigation review. The tabs rewrite appended `.live` to
@entangle, so every tab set bound with a plain wire:model sent a request
on each click. @entangle already adds `.live` for wire:model.live, which
the view's own header promises is the caller's choice. Pinned for both
spellings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 01:09:13 +02:00

100 lines
4.6 KiB
PHP

{{-- M3's tabs: the sections of one thing, with the chosen one marked by an active indicator.
The bar is rendered on the server from `tabs`, a list of `['name' => …, 'label' => …]` with an
optional `icon`, `badge` and `disabled` — so it arrives with the page, never a moment after it.
The panels are `<x-tab name="…">` in the slot; every panel is rendered, so switching never waits
on the server, and each arrives with its id and with every panel but the chosen one already
hidden, so nothing flashes and no screen reader meets four panels at once (N-05).
The chosen tab is the Livewire property in `wire:model` (entangled; `wire:model.live` tells the
server at once), or, without it, `selected` (the first tab by default) and `x-model`. The bar is
a real tablist (resources/js/tabs.js). `variant` is `primary` (the default: the indicator under
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).
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. --}}
@props([
'tabs' => [],
'variant' => 'primary',
'selected' => null,
'stacked' => false,
'scrollable' => false,
])
@php
$model = $attributes->wire('model')->value() ?: null;
$variant = $variant === 'secondary' ? 'secondary' : 'primary';
// tab.blade.php repeats these two lines off the component stack, because a slot renders before
// the component around it: change them together, or a panel's id stops matching aria-controls.
$id = $attributes->get('id') ?? 'tabs-'.substr(md5($model.'|'.implode('|', array_column($tabs, 'name'))), 0, 8);
$initial = $selected ?? (collect($tabs)->first(fn (array $tab): bool => ! ($tab['disabled'] ?? false))['name'] ?? null);
if ($model !== null && ($component = \Livewire\Livewire::current()) !== null && filled(data_get($component, $model))) {
$initial = data_get($component, $model);
}
@endphp
<div
x-data="{
@if ($model !== null) selected: @entangle($attributes->wire('model')), @else selected: @js($initial), @endif
tabsId: @js($id),
...materialTabs(),
}"
@if ($model === null) x-modelable="selected" @endif
data-md-tabs
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id']) }}
>
<div
x-ref="bar"
role="tablist"
data-md-tabs-bar
data-md-variant="{{ $variant }}"
@if ($stacked && $variant === 'primary') data-md-stacked @endif
@if ($scrollable) data-md-scrollable @endif
style="--tabs-indicator: {{ $id }}-indicator"
x-on:keydown.arrow-right.prevent="step(1)"
x-on:keydown.arrow-left.prevent="step(-1)"
x-on:keydown.home.prevent="edge(false)"
x-on:keydown.end.prevent="edge(true)"
>
@foreach ($tabs as $tab)
@php($isInitial = (string) $tab['name'] === (string) $initial)
<button
type="button"
role="tab"
id="{{ $id }}-{{ $tab['name'] }}"
data-md-tab="{{ $tab['name'] }}"
aria-controls="{{ $id }}-{{ $tab['name'] }}-panel"
aria-selected="{{ $isInitial ? 'true' : 'false' }}"
tabindex="{{ $isInitial ? '0' : '-1' }}"
x-bind:aria-selected="(selected === @js($tab['name'])).toString()"
x-bind:tabindex="selected === @js($tab['name']) ? 0 : -1"
x-on:click="choose(@js($tab['name']))"
@disabled($tab['disabled'] ?? false)
>
<span data-md-tab-content>
@isset($tab['icon'])
<x-livewire-material::icon :name="$tab['icon']" />
@endisset
<span data-md-tab-label>
{{ $tab['label'] }}
@if (filled($tab['badge'] ?? null))
<x-livewire-material::badge :value="$tab['badge']" />
@endif
</span>
<span data-md-tab-indicator aria-hidden="true"></span>
</span>
</button>
@endforeach
</div>
{{ $slot }}
</div>