Keep a list row's fill under its state layer

Plan step 36 review of 9ff5a7a8. The rewrite dropped the unlayered
selected rule into material.components beside the segmented fill and the
row tint, so on equal or greater weight a selected item in a segmented
list drew surface instead of secondary-container, and hovering, pressing
or focusing any selected row (or a table's selected row) swapped its
fill for a translucent tint. Each fill now names itself
--md-list-row-fill, and the row's layer is its ink mixed over that fill
at the state tokens, which is also C-08's second suggested fix.

A disabled item inks the whole row at the disabled-content token, as the
old root colour did, so the check, the leading slot and the end slot dim
with the text; the three-line padding is a literal 12px like alert.css;
list-rows.js named list.css.

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 22:00:03 +02:00
co-authored by Claude Opus 5
parent a8c2d88fbb
commit 7d3abdd506
4 changed files with 68 additions and 20 deletions
+34 -17
View File
@@ -7,13 +7,17 @@
* `ItemLeadingSpace`/`ItemTrailingSpace`/container padding, C-14).
*
* `data-md-list-row` (`resources/js/list-rows.js`, shared with `<x-card>` and an application's own
* table rows) and `[data-md-list-open]:focus-visible` answer with an 8%/10% on-surface state layer
* and a 3px secondary inset ring excluded on `[data-md-card]`, which draws its own tint through
* the shared `md-state-layer` class instead (card.css). `data-md-selected` is M3's selected item,
* secondary-container filled, which also dims the description/overline/trailing/icon colour rules
* below since they exclude it — the container's own colour takes over by inheritance. A selected
* option draws a second cue, a trailing check, so selection is never colour alone (C-03).
* `aria-disabled` drops every colour to 38% on-surface and blocks the pointer; the view drops the
* table rows) and `[data-md-list-open]:focus-visible` answer with the state layer — the row's ink at
* the hover 8% and focus/press 10% state tokens — and a 3px secondary inset ring; excluded on
* `[data-md-card]`, which draws its own tint through the shared `md-state-layer` class instead
* (card.css). The layer is mixed over the row's own fill, `--md-list-row-fill` (a segmented tile's
* surface, a selected item's secondary-container, table.css's selected row), so a hovered, pressed
* or focused row keeps its fill under the tint instead of trading it for a translucent one (C-08's
* second fix). `data-md-selected` is M3's selected item, secondary-container filled, which also
* lifts the description/overline/trailing/icon colour rules below since they exclude it — the
* container's own colour takes over by inheritance. A selected option draws a second cue, a
* trailing check, so selection is never colour alone (C-03). `aria-disabled` inks the whole item
* on-surface at the disabled-content opacity (38%) and blocks the pointer; the view drops the
* item's link entirely rather than leaving a focusable, activatable control behind it (C-02).
*
* `data-md-list="segmented"` items (M3 Expressive) are `surface` tiles (`ItemSegmentedContainerColor`,
@@ -58,21 +62,35 @@
[data-md-list-item][data-md-lines='3'] {
min-height: 88px;
padding-block: calc((var(--md-sys-measurement-space100) + var(--md-sys-measurement-space200)) / 2);
padding-block: 12px;
align-items: flex-start;
}
/* A row's fill, under its state layer. Declared on every row at no weight, so a row nested in a
filled one starts from nothing rather than inheriting its parent's fill. */
:where([data-md-list-row]) {
--md-list-row-fill: transparent;
}
/* M3 Expressive's segmented list gives each item its own container: ListTokens.kt:201,
`ItemSegmentedContainerColor get() = ColorSchemeKeyTokens.Surface`. Before the selected
rule, which outranks it on the same weight. */
[data-md-list='segmented'] > [data-md-list-item] {
--md-list-row-fill: var(--md-sys-color-surface);
background-color: var(--md-list-row-fill);
}
[data-md-list-item][data-md-selected] {
background-color: var(--md-sys-color-secondary-container);
--md-list-row-fill: var(--md-sys-color-secondary-container);
background-color: var(--md-list-row-fill);
color: var(--md-sys-color-on-secondary-container);
}
[data-md-list-item][aria-disabled='true'] {
pointer-events: none;
}
[data-md-list-item][aria-disabled='true'] :is([data-md-list-item-title], [data-md-list-item-overline], [data-md-list-item-description], [data-md-list-item-trailing], [data-md-list-item-icon]) {
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
[data-md-list-item]:not([data-md-selected], [aria-disabled='true']) :is([data-md-list-item-overline], [data-md-list-item-description], [data-md-list-item-trailing], [data-md-list-item-icon]) {
@@ -192,18 +210,18 @@
@media (hover: hover) {
[data-md-list-row]:not([data-md-card]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover)) {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
background-color: color-mix(in srgb, currentColor calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent));
}
}
[data-md-list-row]:not([data-md-card]):has([data-md-list-open]:focus-visible) {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
background-color: color-mix(in srgb, currentColor calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent));
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: -3px;
}
[data-md-list-row]:not([data-md-card]):active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):active)) {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
background-color: color-mix(in srgb, currentColor calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent));
}
[data-md-list-row] [data-md-list-open]:focus-visible {
@@ -211,7 +229,6 @@
}
[data-md-list='segmented'] > [data-md-list-item] {
background-color: var(--md-sys-color-surface);
border-radius: var(--md-sys-shape-corner-xs);
}
+5 -2
View File
@@ -9,7 +9,8 @@
* never a fraction of it, since M3 reserves opacity for state layers and disabled. A selected row
* (`aria-selected="true"`) is secondary-container. A row that opens something is `data-md-list-row`
* and answers a pointer as a list row does (components/list-item.css, shared with `<x-card>` and
* `<x-list-item>`).
* `<x-list-item>`); a selected one names its fill `--md-list-row-fill`, so that state layer is laid
* over the secondary-container rather than in place of it.
*
* A row is 52px: 16px above and below a body-medium line, 12px beside each cell. Density is never
* applied by itself — "don't apply density by default; offer an explicit density opt-in instead,
@@ -84,7 +85,9 @@
}
[data-md-table] :where(tbody tr[aria-selected='true']) {
background-color: var(--md-sys-color-secondary-container);
--md-list-row-fill: var(--md-sys-color-secondary-container);
background-color: var(--md-list-row-fill);
color: var(--md-sys-color-on-secondary-container);
}
}