Lead a list item with M3's video slot
Plan step 23, containment.md § Missing ("Lists: leading video slot").
`<x-list-item video="…">` (or `<x-slot:video>`) draws M3's leading media
in its landscape sizes: 100×56px in a two-line item, 114×64px in a
three-line one (ListTokens' LeadingVideoSmall 56×100dp and
LeadingVideoLarge 64×114dp), with the small corner the leading image
takes. The tallest element sets an item's height, so a video lifts the
item to at least the 72px two-line row — 56 + 2×8 is 72 exactly, and
64 + 2×12 is 88.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ce04101f89
commit
927e439064
@@ -436,7 +436,9 @@ That is M3's *non-actionable card with actionable elements*: Tab walks the contr
|
||||
|
||||
### `<x-list>`, `<x-list-item>`
|
||||
|
||||
`<x-list>`: `label`, `dividers`, `segmented` (M3 Expressive: separate tiles 2px apart). `<x-list-item>`: `title` (or slot), `overline`, `description`, leading `icon` / `avatar` (image URL or initials) / `image` / `leading` slot, trailing `trailing` text / `icon-right` / `end` slot, `link` (the whole item becomes a row that opens it), `selected`, `disabled`. One-, two- and three-line heights follow from the content, and a three-line item top-aligns as M3 asks. Its icons are 24px, 20px in a `segmented` list.
|
||||
`<x-list>`: `label`, `dividers`, `segmented` (M3 Expressive: separate tiles 2px apart). `<x-list-item>`: `title` (or slot), `overline`, `description`, leading `icon` / `avatar` (image URL or initials) / `image` / `video` / `leading` slot, trailing `trailing` text / `icon-right` / `end` slot, `link` (the whole item becomes a row that opens it), `selected`, `disabled`. One-, two- and three-line heights follow from the content, and a three-line item top-aligns as M3 asks. Its icons are 24px, 20px in a `segmented` list.
|
||||
|
||||
`video` is M3's leading media in landscape: a poster URL, or `<x-slot:video>` for a `<video>` or a thumbnail with a play badge. It is drawn 100×56px in a two-line item and 114×64px in a three-line one (ListTokens' small and large leading video), and always lifts the item to at least the 72px two-line height, since the tallest element sets an item's height.
|
||||
|
||||
A list a person chooses from is `<x-list selectable>` (or `selection="single"` / `selection="multi"`): M3 maps those to a **list box** of **options**, so the container becomes `role="listbox"` (`aria-multiselectable` when multi) and each item an `option` announcing `aria-selected`. A selected option also draws a trailing check — M3 never allows colour as the only cue — which `icon-right` or a leading checkbox replaces. A plain list stays `role="list"`, where `selected` is `aria-current`. `disabled` renders no link and announces `aria-disabled`.
|
||||
|
||||
|
||||
@@ -5,15 +5,27 @@
|
||||
|
||||
`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
|
||||
`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
|
||||
heights (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-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,
|
||||
@@ -33,6 +45,7 @@
|
||||
'icon' => null,
|
||||
'avatar' => null,
|
||||
'image' => null,
|
||||
'video' => null,
|
||||
'trailing' => null,
|
||||
'iconRight' => null,
|
||||
'link' => null,
|
||||
@@ -53,6 +66,15 @@
|
||||
@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);
|
||||
@@ -82,6 +104,18 @@
|
||||
>
|
||||
@isset($leading)
|
||||
<div class="flex shrink-0 items-center">{{ $leading }}</div>
|
||||
@elseif ($hasVideo)
|
||||
<div @class([
|
||||
'shrink-0 overflow-hidden rounded-corner-sm bg-surface-container-highest *:size-full *:object-cover',
|
||||
'h-16 w-28.5' => $videoLarge,
|
||||
'h-14 w-25' => ! $videoLarge,
|
||||
])>
|
||||
@if ($video instanceof \Illuminate\View\ComponentSlot)
|
||||
{{ $video }}
|
||||
@else
|
||||
<img src="{{ $video }}" alt="" />
|
||||
@endif
|
||||
</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>
|
||||
|
||||
@@ -60,6 +60,24 @@
|
||||
<x-list-item title="Thirty days" :selected="false" />
|
||||
</x-list>
|
||||
BLADE,
|
||||
'A list with leading video' => <<<'BLADE'
|
||||
<x-list dividers label="Clips" class="w-full max-w-md">
|
||||
<x-list-item title="Sunrise over the lake" description="2:14 · 48 MB">
|
||||
<x-slot:video>
|
||||
<div class="grid place-items-center bg-linear-to-br from-primary-container to-tertiary-container text-on-primary-container">
|
||||
<x-icon name="play_circle" class="size-6" />
|
||||
</div>
|
||||
</x-slot:video>
|
||||
</x-list-item>
|
||||
<x-list-item overline="Draft" title="Walking the old town" description="Uploaded yesterday · 7:48">
|
||||
<x-slot:video>
|
||||
<div class="grid place-items-center bg-linear-to-br from-secondary-container to-primary-container text-on-secondary-container">
|
||||
<x-icon name="play_circle" class="size-6" />
|
||||
</div>
|
||||
</x-slot:video>
|
||||
</x-list-item>
|
||||
</x-list>
|
||||
BLADE,
|
||||
'Dividers and collapse' => <<<'BLADE'
|
||||
<div class="w-full space-y-4">
|
||||
<x-collapse title="How long do links last?" icon="schedule" open>
|
||||
|
||||
@@ -93,3 +93,20 @@ it('marks a selected item and takes controls in its slots', function () {
|
||||
->toContain('<input type="checkbox">')
|
||||
->toContain('<button>⋮</button>');
|
||||
});
|
||||
|
||||
it('leads with M3\'s video, in the size the item\'s height calls for', function () {
|
||||
expect((string) $this->blade('<x-list-item title="Sunrise" video="/poster.jpg" />'))
|
||||
->toContain('h-14 w-25')
|
||||
->toContain('rounded-corner-sm')
|
||||
->toContain('<img src="/poster.jpg" alt="" />')
|
||||
->toContain('min-h-18 py-2')
|
||||
->and((string) $this->blade('<x-list-item title="Sunrise" overline="Draft" description="7:48" video="/poster.jpg" />'))
|
||||
->toContain('h-16 w-28.5')
|
||||
->toContain('min-h-22 py-3')
|
||||
->and((string) $this->blade('<x-list-item title="Sunrise"><x-slot:video><video src="/clip.mp4"></video></x-slot:video></x-list-item>'))
|
||||
->toContain('<video src="/clip.mp4"></video>')
|
||||
->toContain('h-14 w-25')
|
||||
->and((string) $this->blade('<x-list-item title="Sunrise" />'))
|
||||
->not->toContain('h-14 w-25')
|
||||
->toContain('min-h-14 py-2');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user