Give a list item M3's gaps, alignment and expressive icon size
Plan step 19, containment.md C-14, C-15 and C-26. The leading and trailing gap was 12px where ListItem.kt's LeadingContentEndPadding and TrailingContentStartPadding are 16dp; a three-line item was middle-aligned and padded 10px where M3 top-aligns an 88dp item and Compose pads it 12; and a segmented list's icons stayed at the baseline 24dp instead of ListTokens' expressive 20dp, which the item now reads from its list with `@aware` and draws from the 20 optical cut. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
dcd28707dd
commit
26a9230a48
@@ -431,7 +431,7 @@ A card or list item that opens something is a **row**: `data-list-row` on it and
|
||||
|
||||
### `<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.
|
||||
`<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.
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
`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).
|
||||
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).
|
||||
|
||||
`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
|
||||
@@ -38,10 +42,12 @@
|
||||
'disabled' => false,
|
||||
])
|
||||
|
||||
{{-- Whether the `<x-list>` around it is one a person chooses from, and how many it takes. --}}
|
||||
{{-- 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
|
||||
@@ -50,6 +56,11 @@
|
||||
$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 ? 'size-5' : 'size-6';
|
||||
$optical = $segmented ? '20' : '24';
|
||||
@endphp
|
||||
|
||||
<div
|
||||
@@ -60,10 +71,12 @@
|
||||
@if ($selected) data-selected @endif
|
||||
@if ($disabled) aria-disabled="true" @endif
|
||||
{{ $attributes->class([
|
||||
'relative flex items-center gap-3 px-4 text-on-surface',
|
||||
'relative flex gap-4 px-4 text-on-surface',
|
||||
'items-center' => $lines !== 2,
|
||||
'items-start' => $lines === 2,
|
||||
'min-h-14 py-2' => $lines === 0,
|
||||
'min-h-18 py-2.5' => $lines === 1,
|
||||
'min-h-22 py-2.5' => $lines === 2,
|
||||
'min-h-18 py-2' => $lines === 1,
|
||||
'min-h-22 py-3' => $lines === 2,
|
||||
'pointer-events-none text-on-surface/38' => $disabled,
|
||||
]) }}
|
||||
>
|
||||
@@ -78,7 +91,7 @@
|
||||
@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])" />
|
||||
<x-livewire-material::icon :name="$icon" :optical="$optical" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'text-on-surface-variant' => ! $selected && ! $disabled])" />
|
||||
@endif
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
@@ -111,8 +124,8 @@
|
||||
@endif
|
||||
|
||||
@if ($iconRight)
|
||||
<x-livewire-material::icon :name="$iconRight" :class="\Illuminate\Support\Arr::toCssClasses(['size-6', 'text-on-surface-variant' => ! $selected && ! $disabled])" />
|
||||
<x-livewire-material::icon :name="$iconRight" :optical="$optical" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'text-on-surface-variant' => ! $selected && ! $disabled])" />
|
||||
@elseif ($check)
|
||||
<x-livewire-material::icon name="check" class="size-6" />
|
||||
<x-livewire-material::icon name="check" :optical="$optical" :class="$iconSize" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -18,10 +18,25 @@ it('draws M3 Expressive\'s segmented list', function () {
|
||||
->toContain('gap-0.5');
|
||||
});
|
||||
|
||||
it('grows an item from one to three lines', function () {
|
||||
expect((string) $this->blade('<x-list-item title="a.zip" />'))->toContain('min-h-14')
|
||||
->and((string) $this->blade('<x-list-item title="a.zip" description="248 MB" />'))->toContain('min-h-18')->toContain('248 MB')
|
||||
->and((string) $this->blade('<x-list-item title="a.zip" overline="Protected" description="248 MB" />'))->toContain('min-h-22')->toContain('Protected');
|
||||
it('grows an item from one to three lines, top-aligning the tallest', function () {
|
||||
expect((string) $this->blade('<x-list-item title="a.zip" />'))->toContain('items-center')->toContain('min-h-14 py-2')
|
||||
->and((string) $this->blade('<x-list-item title="a.zip" description="248 MB" />'))->toContain('items-center')->toContain('min-h-18 py-2')->toContain('248 MB')
|
||||
->and((string) $this->blade('<x-list-item title="a.zip" overline="Protected" description="248 MB" />'))
|
||||
->toContain('items-start')
|
||||
->toContain('min-h-22 py-3')
|
||||
->toContain('Protected')
|
||||
->not->toContain('items-center');
|
||||
});
|
||||
|
||||
it('leaves 16px between the item and what leads or trails it', function () {
|
||||
expect((string) $this->blade('<x-list-item title="a.zip" icon="folder_zip" />'))->toContain('flex gap-4 px-4');
|
||||
});
|
||||
|
||||
it('draws the expressive icon size in a segmented list', function () {
|
||||
expect((string) $this->blade('<x-list segmented><x-list-item title="a" icon="settings" icon-right="chevron_right" /></x-list>'))
|
||||
->toContain('size-5')
|
||||
->not->toContain('size-6')
|
||||
->and((string) $this->blade('<x-list><x-list-item title="a" icon="settings" /></x-list>'))->toContain('size-6');
|
||||
});
|
||||
|
||||
it('leads with an icon, an avatar or an image, and trails with text or an icon', function () {
|
||||
|
||||
Reference in New Issue
Block a user