Files
livewire-material/resources/views/components/list-item.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 d64fb91303 Announce a selectable list as M3's list box of options
Plan step 12, containment.md C-03. `selected` was colour alone on a
`listitem`, which cannot carry a selected state at all. `<x-list
selectable>` (or `selection="single|multi"`) now makes the container a
`role="listbox"` and each item an `option` with `aria-selected`; a plain
list keeps `role="list"` and marks a selected item `aria-current`. A
selected option draws a trailing check as M3's second cue, which
`icon-right` replaces. The item reads its list's mode with `@aware`.

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

119 lines
5.2 KiB
PHP

{{-- One M3 list item: a headline with what leads it, what supports it and what trails it.
<x-list-item title="holiday-photos.zip" description="248 MB · expires in 3 days" icon="folder_zip" trailing="3 files" />
<x-list-item title="Settings" icon="settings" icon-right="chevron_right" link="/settings" />
`title` (or the slot) is the body-large headline; `overline` sits above it (label-small),
`description` under it (body-medium, up to two lines). The leading element is one of `icon`,
`avatar` (an image URL, or initials in primary-container) or `image` (a 56px thumbnail), or a
`leading` slot (a checkbox, a switch). The trailing element is `trailing` text (label-small),
`icon-right`, or an `end` slot for controls (a menu, a switch). One-, two- and three-line
heights (56, 72, 88px) follow from what is given (ListTokens, androidx Compose Material 3, Apache-2.0).
`link` makes the whole item the link. Otherwise, to make it open something while its trailing
controls keep their own presses, give it `data-list-row` and put `data-list-open` on the one
control that opens see resources/js/list-rows.js. `selected` (true) is M3's selected item,
in secondary-container; `disabled` greys it to 38%, drops its link and announces it
`aria-disabled` — M3's states model treats disabled as not interactive, so a disabled item
answers neither the pointer nor the keyboard.
It takes its role from the `<x-list>` around it: a `listitem` in a plain list, where
`selected` is `aria-current`, and an `option` announcing `aria-selected` in a `selectable`
one. M3 asks for two cues on a selected item, never colour alone, so a selected option also
draws a trailing check unless `icon-right` says otherwise. --}}
@props([
'title' => null,
'overline' => null,
'description' => null,
'icon' => null,
'avatar' => null,
'image' => null,
'trailing' => null,
'iconRight' => null,
'link' => null,
'external' => false,
'noWireNavigate' => false,
'selected' => false,
'disabled' => false,
])
{{-- Whether the `<x-list>` around it is one a person chooses from, and how many it takes. --}}
@aware([
'selectable' => false,
'selection' => null,
])
@php
$isLink = filled($link) && ! $disabled;
$lines = (filled($overline) ? 1 : 0) + (filled($description) ? 1 : 0);
$initials = filled($avatar) && ! str_contains((string) $avatar, '/') && ! str_contains((string) $avatar, '.');
$option = $selectable || $selection !== null;
$check = $option && $selected && blank($iconRight);
@endphp
<div
role="{{ $option ? 'option' : 'listitem' }}"
@if ($option) aria-selected="{{ $selected ? 'true' : 'false' }}" @elseif ($selected) aria-current="true" @endif
data-list-item
@if ($isLink) data-list-row @endif
@if ($selected) data-selected @endif
@if ($disabled) aria-disabled="true" @endif
{{ $attributes->class([
'relative flex items-center gap-3 px-4 text-on-surface',
'min-h-14 py-2' => $lines === 0,
'min-h-18 py-2.5' => $lines === 1,
'min-h-22 py-2.5' => $lines === 2,
'pointer-events-none text-on-surface/38' => $disabled,
]) }}
>
@isset($leading)
<div class="flex shrink-0 items-center">{{ $leading }}</div>
@elseif ($avatar)
@if ($initials)
<span class="grid size-10 shrink-0 place-items-center rounded-corner-full bg-primary-container type-title-md text-on-primary-container" aria-hidden="true">{{ $avatar }}</span>
@else
<img src="{{ $avatar }}" alt="" class="size-10 shrink-0 rounded-corner-full object-cover" />
@endif
@elseif ($image)
<img src="{{ $image }}" alt="" class="size-14 shrink-0 rounded-corner-sm object-cover" />
@elseif ($icon)
<x-livewire-material::icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['size-6', 'text-on-surface-variant' => ! $selected && ! $disabled])" />
@endif
<div class="min-w-0 flex-1">
@if ($overline)
<p @class(['type-label-sm', 'text-on-surface-variant' => ! $selected])>{{ $overline }}</p>
@endif
@if ($isLink)
<a
href="{{ $link }}"
data-list-open
@if ($external) target="_blank" rel="noopener" @elseif (! $noWireNavigate) wire:navigate @endif
class="block truncate type-body-lg outline-none"
>{{ $title ?? $slot }}</a>
@else
<p class="truncate type-body-lg">{{ $title ?? $slot }}</p>
@endif
@if ($description)
<p @class(['line-clamp-2 type-body-md', 'text-on-surface-variant' => ! $selected])>{{ $description }}</p>
@endif
</div>
@isset($end)
<div class="flex shrink-0 items-center gap-1">{{ $end }}</div>
@endisset
@if ($trailing)
<span @class(['shrink-0 type-label-sm', 'text-on-surface-variant' => ! $selected])>{{ $trailing }}</span>
@endif
@if ($iconRight)
<x-livewire-material::icon :name="$iconRight" :class="\Illuminate\Support\Arr::toCssClasses(['size-6', 'text-on-surface-variant' => ! $selected && ! $disabled])" />
@elseif ($check)
<x-livewire-material::icon name="check" class="size-6" />
@endif
</div>