Plan step 35: <x-list-detail>, following the reference's visible-panes table row by row - one pane below expanded, the detail replacing the list with a back button once something is selected; from 840px the list a fixed 360px (412px from 1200px) beside the detail, 24px apart. The selection binds with wire:model or x-model; below expanded focus moves to the detail and back() returns it to the item (resources/js/layout.js). Grid columns mirror in RTL, and the back arrow turns with them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
107 lines
5.0 KiB
PHP
107 lines
5.0 KiB
PHP
{{-- M3's list-detail canonical layout: a list, and the detail of what is selected in it.
|
||
|
||
<x-list-detail wire:model.live="selectedId">
|
||
<x-slot:list>
|
||
<x-pane title="Inbox">
|
||
<x-list>… <x-list-item wire:click="$set('selectedId', {{ $message->id }})" …/> …</x-list>
|
||
</x-pane>
|
||
</x-slot:list>
|
||
<x-slot:detail>
|
||
<x-pane :title="$message?->subject" heading="h2" back>…</x-pane>
|
||
</x-slot:detail>
|
||
</x-list-detail>
|
||
|
||
Parent and child content: an inbox and a message, folders and a file, settings and a category
|
||
(docs/reference/m3/foundations-supplement.md § Canonical layouts → List-detail). Its
|
||
visible-panes table, row by row: compact (below 600px) shows 1 pane; medium (600–839) 1 or 2,
|
||
and this layout shows 1, M3's recommendation for anything but low-density content; expanded
|
||
(840+), large (1200–1599) and extra-large (1600+) show 2.
|
||
|
||
So below 840px the list shows until something is selected, then the detail replaces it with
|
||
a back button; from 840px the list is a fixed pane, 360px wide (412px from 1200px, M3's
|
||
fixed-pane widths at expanded and at large, § Breakpoints), the detail takes the rest, 24px
|
||
apart, and no back button shows. Resizing moves between the two as M3 describes ("rotating
|
||
from expanded to medium collapses two panes back to one"). A right-to-left document puts the
|
||
list on the right and turns the back arrow.
|
||
|
||
`selected` is what is selected, bound with `wire:model` (the Livewire property, whose value
|
||
the server also renders from, so the first paint shows the right pane) or `x-model`, or given
|
||
once as `selected`. Nothing selected is `null`, `false` or `''`. Inside both slots `selected`
|
||
is in Alpine scope, and `back()` clears it (a boolean to `false`, anything else to `null`).
|
||
A list item can select with `wire:click="$set('selectedId', 7)"` or `x-on:click="selected = 7"`.
|
||
M3 shows the list's selected state only where both panes show; give the item `aria-current`
|
||
for it, which also tells `back()` where to return focus.
|
||
|
||
The back button: `<x-pane back>` puts it in the detail pane's own app bar. Without one in the
|
||
detail slot, the layout draws its own row with it above the detail. Either way it is hidden
|
||
from 840px.
|
||
|
||
Focus (resources/js/layout.js): below 840px a selection moves focus to the detail pane, and
|
||
`back()` returns it to the list item it came from — or the item marked `aria-current` or
|
||
`aria-selected`, or the list — as a dialog returns focus to its trigger (WAI-ARIA APG). From
|
||
840px focus stays where it is, since both panes are on screen.
|
||
|
||
The layout keeps M3's margin (16px below medium, 24px from it) unless it sits inside something
|
||
that already does, such as `<x-scaffold>`'s content region. It takes `as`, `hide-below` and
|
||
`hide-from` like every layout component, and the caller's `class` and `style` land on it
|
||
untouched. Its own attributes belong to Alpine (`wire:ignore.self`), while both slots morph as
|
||
usual. Drawn by resources/css/layout/list-detail.css. --}}
|
||
|
||
@props([
|
||
'as' => null,
|
||
'selected' => null,
|
||
'hideBelow' => null,
|
||
'hideFrom' => null,
|
||
])
|
||
|
||
@php
|
||
$layout = \NoNameWeb\LivewireMaterial\Support\Layout::class;
|
||
$element = $layout::element($as);
|
||
$model = $attributes->wire('model')->value() ?: null;
|
||
$initial = $selected;
|
||
|
||
if ($model !== null && ($component = \Livewire\Livewire::current()) !== null) {
|
||
$initial = data_get($component, $model);
|
||
}
|
||
|
||
$chosen = $initial !== null && $initial !== false && $initial !== '';
|
||
$wire = $attributes->wire('model');
|
||
$detailSlot = $detail ?? null;
|
||
$ownBack = $detailSlot !== null && str_contains((string) $detailSlot, 'data-md-list-detail-back');
|
||
|
||
$attributes = $attributes->whereDoesntStartWith('wire:model')->merge(array_filter([
|
||
'data-md-list-detail' => true,
|
||
'data-md-selected' => $chosen ? true : null,
|
||
] + $layout::visibility($hideBelow, $hideFrom), fn ($value): bool => $value !== null));
|
||
@endphp
|
||
|
||
<{{ $element }}
|
||
x-data="materialListDetail(@if ($model !== null) @entangle($wire) @else @js($initial) @endif)"
|
||
@if ($model === null) x-modelable="selected" @endif
|
||
x-bind:data-md-selected="hasSelection ? '' : null"
|
||
x-on:focusin="track($event)"
|
||
x-on:focusout="track($event)"
|
||
wire:ignore.self
|
||
{{ $attributes }}
|
||
>
|
||
<div
|
||
data-md-list-detail-pane="list"
|
||
tabindex="-1"
|
||
x-ref="list"
|
||
x-on:focusin="remember($event)"
|
||
x-on:click="remember($event)"
|
||
>{{ $list ?? '' }}</div>
|
||
|
||
<div data-md-list-detail-pane="detail" tabindex="-1" x-ref="detail">
|
||
@unless ($ownBack)
|
||
<div data-md-list-detail-back-row>
|
||
<span data-md-list-detail-back>
|
||
<x-livewire-material::button icon="arrow_back" :tooltip="__('Back')" x-on:click="back()" />
|
||
</span>
|
||
</div>
|
||
@endunless
|
||
|
||
{{ $detailSlot ?? '' }}
|
||
</div>
|
||
</{{ $element }}>
|