Files
livewire-material/resources/views/components/list-item.blade.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 9ff5a7a809 Rewrite list and list-item without Tailwind
Plan step 36 (containment group, last of its four components): moves
into resources/css/components/list.css (the container: display, the
2px segmented gap) and list-item.css (everything else — anatomy,
states, the segmented item's own background/corner, and the row
interaction pattern list.css used to carry). One commit for both:
list-item.css keys heavily on list's own data-md-list="segmented"
attribute and the two files are designed together.

Kept, now on data-md-* hooks: selectable/selection listbox semantics
(role="listbox"/"option", aria-selected, a trailing check as the
second cue, C-03), a disabled item's link dropped entirely rather than
left focusable (C-02), the segmented row's own state layer no longer
racing its background through an unlayered escape hatch (C-08 doesn't
recur — nothing here is unlayered), leading video/icon/avatar slots,
three-line top alignment and 12px vertical padding (C-15), 16px
leading/trailing gap (C-14), dividers inset 16px (C-16), the segmented
list's surface fill (C-21) and its hover/focus/pressed/selected corner
morph (C-22). The segmented icon size (20px) is now the `size` prop
`<x-icon>` already takes from the parent's @aware(['segmented']),
replacing the CSS override C-26's fix suggested.

data-md-lines="1|2|3" replaces the old line-count classes; data-md-list-item-*
hooks replace every other class list.

Hooks renamed data-list -> data-md-list, data-list-item -> data-md-list-item,
data-dividers -> data-md-dividers, data-selected -> data-md-selected, the
generic data-list-row/-open row pattern's last consumer (list-item)
moved, so list-rows.js drops the transitional dual-hook fallback the
card commit added. table.css now @imports list-item.css, completing
what its own comment already claimed (a row answers a pointer as a
list row does) under the new per-component-import architecture.

Imported from the Containment block of components.css; leaves
tailwind.css, which now carries only dialog.css for this group.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 20:37:38 +02:00

154 lines
7.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{-- 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), `image` (a 56px thumbnail) or
`video`, 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 items (`data-md-lines`, 56, 72, 88px) follow from what is given, 16px between the
item and what leads or trails it, and a three-line item top-aligns rather than centring M3
aligns an item middle "by default, top-aligned if the item is 88dp+ or has 3+ lines of text".
Its icons are 24px, or 20px in a `segmented` list, which is M3 Expressive's (ListTokens,
androidx Compose Material 3, Apache-2.0).
`video` is M3's leading media in its landscape size a poster URL, or a `<x-slot:video>`
holding a `<video>` or a thumbnail with a play badge. ListTokens gives it two: 100×56px in a
two-line item and 114×64px in a three-line one (`LeadingVideoSmall` 56dp × 100dp,
`LeadingVideoLarge` 64dp × 114dp height first, both 16:9), so a video always lifts the item
to at least the 72px two-line height, where 56 + 2×8 comes to 72 exactly, and to 88 with the
large one, where 64 + 2×12 comes to 88. It takes the small corner the leading image takes
(`ItemLeadingImageShape` = CornerSmall; the site publishes no separate video shape) and sits
in the leading slot, 16px in from the edge like every other leading element. M3's slot model
asks that the leading slot stay narrower than the content slot, so give an item with a large
video the room its text needs
(docs/reference/m3/components-actions-communication-containment.md § Lists → Anatomy, Specs).
`link` makes the whole item the link. Otherwise, to make it open something while its trailing
controls keep their own presses, give it `data-md-list-row` and put `data-md-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,
'video' => null,
'trailing' => null,
'iconRight' => null,
'link' => null,
'external' => false,
'noWireNavigate' => false,
'selected' => false,
'disabled' => false,
])
{{-- The `<x-list>` around it: whether it is one a person chooses from and how many it takes, and
whether it is the expressive (segmented) one, whose icons are a size smaller. --}}
@aware([
'selectable' => false,
'selection' => null,
'segmented' => false,
])
@php
$isLink = filled($link) && ! $disabled;
$lines = (filled($overline) ? 1 : 0) + (filled($description) ? 1 : 0);
// `video` is either a poster URL or a `<x-slot:video>`; a slot is an object, which `filled()`
// would call full even when it holds nothing.
$hasVideo = $video instanceof \Illuminate\View\ComponentSlot ? $video->isNotEmpty() : filled($video);
// The tallest element sets the item's height: the small video needs the 72px two-line row,
// and the large one goes with the 88px three-line item M3 draws it in.
$videoLarge = $hasVideo && $lines === 2;
$lines = $hasVideo ? max($lines, 1) : $lines;
$initials = filled($avatar) && ! str_contains((string) $avatar, '/') && ! str_contains((string) $avatar, '.');
$option = $selectable || $selection !== null;
$check = $option && $selected && blank($iconRight);
// ListTokens.kt:153,156,332,335 — ItemLeading/TrailingIconExpressiveSize = 20dp against the
// baseline 24dp. At 20px the symbol is drawn from the 20 optical cut, not the 24 scaled down.
$iconSize = $segmented ? 20 : 24;
@endphp
<div
role="{{ $option ? 'option' : 'listitem' }}"
@if ($option) aria-selected="{{ $selected ? 'true' : 'false' }}" @elseif ($selected) aria-current="true" @endif
data-md-list-item
data-md-lines="{{ $lines + 1 }}"
@if ($isLink) data-md-list-row @endif
@if ($selected) data-md-selected @endif
@if ($disabled) aria-disabled="true" @endif
{{ $attributes }}
>
@isset($leading)
<div data-md-list-item-leading>{{ $leading }}</div>
@elseif ($hasVideo)
<div data-md-list-item-video data-md-size="{{ $videoLarge ? 'lg' : 'sm' }}">
@if ($video instanceof \Illuminate\View\ComponentSlot)
{{ $video }}
@else
<img src="{{ $video }}" alt="" />
@endif
</div>
@elseif ($avatar)
@if ($initials)
<span data-md-list-item-avatar aria-hidden="true">{{ $avatar }}</span>
@else
<img src="{{ $avatar }}" alt="" data-md-list-item-avatar />
@endif
@elseif ($image)
<img src="{{ $image }}" alt="" data-md-list-item-image />
@elseif ($icon)
<x-livewire-material::icon :name="$icon" :size="$iconSize" data-md-list-item-icon />
@endif
<div data-md-list-item-content>
@if ($overline)
<p data-md-list-item-overline>{{ $overline }}</p>
@endif
@if ($isLink)
<a
href="{{ $link }}"
data-md-list-open
data-md-list-item-title
@if ($external) target="_blank" rel="noopener" @elseif (! $noWireNavigate) wire:navigate @endif
>{{ $title ?? $slot }}</a>
@else
<p data-md-list-item-title>{{ $title ?? $slot }}</p>
@endif
@if ($description)
<p data-md-list-item-description>{{ $description }}</p>
@endif
</div>
@isset($end)
<div data-md-list-item-end>{{ $end }}</div>
@endisset
@if ($trailing)
<span data-md-list-item-trailing>{{ $trailing }}</span>
@endif
@if ($iconRight)
<x-livewire-material::icon :name="$iconRight" :size="$iconSize" data-md-list-item-icon />
@elseif ($check)
<x-livewire-material::icon name="check" :size="$iconSize" />
@endif
</div>