Files
livewire-material/resources/views/components/tabs.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 1e580c573e Resolve the package's own components through its namespace
Package views now write <x-livewire-material::button>, so a configured
prefix (which Blade spells <x-m::button>) and an application component
of the same name can no longer break or shadow them. The showcase
rewrites its examples to the configured prefix. Adds the README, and
waits for the adaptive rail to hear a resize before the navigation
tests press its menu button.

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

91 lines
3.9 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.
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
(resources/css/components/tabs.css). --}}
@props([
'tabs' => [],
'variant' => 'primary',
'selected' => null,
'stacked' => false,
'scrollable' => false,
])
@php
$model = $attributes->wire('model')->value() ?: null;
$variant = $variant === 'secondary' ? 'secondary' : 'primary';
$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
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id'])->class(['min-w-0']) }}
>
<div
x-ref="bar"
role="tablist"
data-tabs-bar
data-variant="{{ $variant }}"
@if ($stacked && $variant === 'primary') data-stacked @endif
@if ($scrollable) data-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-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-tab-content>
@isset($tab['icon'])
<x-livewire-material::icon :name="$tab['icon']" class="size-6" />
@endisset
<span class="inline-flex items-center gap-1.5">
{{ $tab['label'] }}
@if (filled($tab['badge'] ?? null))
<x-livewire-material::badge :value="$tab['badge']" />
@endif
</span>
<span data-tab-indicator aria-hidden="true"></span>
</span>
</button>
@endforeach
</div>
{{ $slot }}
</div>