Let a segmented row answer, and inset a list's dividers

Plan step 19, containment.md C-08, C-21, C-22 and C-16. The segmented
fill was declared unlayered, so it beat every state layer in
@layer components and a segmented row lit up for nothing: it now sits in
the layer, `:where()`d to weigh nothing, ahead of the state rules. Its
tone is `surface`, which is ListTokens' ItemSegmentedContainerColor, and
a keyboard-focused row morphs to the large corner like a pressed one.
`<x-list dividers>` draws ListTokens' 16dp-inset rule instead of a
full-bleed `divide-y`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:03:12 +02:00
co-authored by Claude Opus 5
parent 02d908c526
commit dcd28707dd
3 changed files with 43 additions and 13 deletions
+32 -7
View File
@@ -9,9 +9,36 @@
*
* Unlayered where a component paints its fill as a utility (`<x-card>`): anything in a @layer loses
* to a utility whatever its specificity.
*
* Every selector that paints a row is `:where()`, so they all weigh nothing and the later rule
* wins: the segmented list's own fill is declared first and a row's state layer, further down,
* paints over it. Written any other way — outside the layer, or with its real specificity — the
* fill would beat the state layer and a segmented row would answer nothing.
*/
@layer components {
/*
* M3 Expressive's segmented list gives each item its own container. ListTokens.kt:201 —
* `ItemSegmentedContainerColor get() = ColorSchemeKeyTokens.Surface`.
*/
:where([data-list='segmented'] > [data-list-item]) {
background-color: var(--md-sys-color-surface);
}
/*
* ListTokens.kt:30,36 — `DividerLeadingSpace` and `DividerTrailingSpace` are 16dp, so a
* list's dividers are inset from both ends rather than full-bleed (`<x-list dividers>`).
*/
:where([data-list][data-dividers] > [data-list-item]:not(:last-child))::after {
content: '';
position: absolute;
inset-inline: 16px;
bottom: 0;
height: 1px;
background-color: var(--md-sys-color-outline-variant);
pointer-events: none;
}
:where([data-list-row]) {
cursor: pointer;
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast),
@@ -39,10 +66,6 @@
outline: none;
}
[data-list='segmented'] > [data-list-item] {
background-color: var(--md-sys-color-surface-container);
}
:is([data-list-row], [data-list-item])[data-selected]:not([data-card]) {
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
@@ -73,8 +96,10 @@
/*
* M3 Expressive's segmented list (`<x-list segmented>`): each item its own surface, 2px apart,
* extra-small corners that open to large at the ends, and to large while hovered, pressed or
* selected (ListTokens, androidx Compose Material 3, Apache-2.0).
* extra-small corners that open to medium while hovered and to large at the ends and while
* focused, pressed or selected — the specs page's interaction-state expressive shapes, and
* ListTokens' `ItemPressedContainerExpressiveShape` / `ItemDraggedContainerExpressiveShape`
* (androidx Compose Material 3, Apache-2.0).
*/
[data-list='segmented'] > [data-list-item] {
border-radius: var(--md-sys-shape-corner-xs);
@@ -96,6 +121,6 @@
}
}
[data-list='segmented'] > [data-list-item]:is([data-selected], [data-list-row]:active) {
[data-list='segmented'] > [data-list-item]:is([data-selected], [data-list-row]:active, [data-list-row]:has([data-list-open]:focus-visible)) {
border-radius: var(--md-sys-shape-corner-lg);
}
+6 -4
View File
@@ -1,9 +1,11 @@
{{-- An M3 list: `<x-list-item>`s, one under another.
Plain by default the items sit on the surface around them, with `dividers` between them if
asked. `segmented` is M3 Expressive's list: each item its own surface-container tile, 2px
apart, with small corners that open to large at the ends and while hovered, pressed or
selected (ListTokens, androidx Compose Material 3, Apache-2.0).
asked, inset 16px from both ends as ListTokens' `DividerLeadingSpace`/`DividerTrailingSpace`
say (resources/css/components/list.css draws them). `segmented` is M3 Expressive's list: each
item its own surface tile, 2px apart, with small corners that open to large at the ends and
while hovered, pressed, focused or selected (ListTokens, androidx Compose Material 3,
Apache-2.0).
A plain list is a `role="list"` of `listitem`s. `selectable` (or `selection="single"` /
`selection="multi"`) makes it the list a person chooses from: M3 maps single- and
@@ -38,11 +40,11 @@
role="{{ $selection === null ? 'list' : 'listbox' }}"
@if ($selection === 'multi') aria-multiselectable="true" @endif
data-list="{{ $segmented ? 'segmented' : 'plain' }}"
@if ($dividers && ! $segmented) data-dividers @endif
@if ($label) aria-label="{{ $label }}" @endif
{{ $attributes->class([
'flex flex-col',
'gap-0.5' => $segmented,
'divide-y divide-outline-variant' => $dividers && ! $segmented,
]) }}
>
{{ $slot }}
+5 -2
View File
@@ -5,8 +5,11 @@ it('draws a plain list, with dividers on request', function () {
->toContain('role="list"')
->toContain('aria-label="Files"')
->toContain('data-list="plain"')
->toContain('divide-y divide-outline-variant')
->toContain('role="listitem"');
->toContain('data-dividers')
->not->toContain('divide-y')
->toContain('role="listitem"')
->and((string) $this->blade('<x-list><x-list-item title="a.zip" /></x-list>'))->not->toContain('data-dividers')
->and((string) $this->blade('<x-list segmented dividers><x-list-item title="a.zip" /></x-list>'))->not->toContain('data-dividers');
});
it('draws M3 Expressive\'s segmented list', function () {