diff --git a/resources/css/components/list-item.css b/resources/css/components/list-item.css index d73cbaa5..d48db7ec 100644 --- a/resources/css/components/list-item.css +++ b/resources/css/components/list-item.css @@ -7,13 +7,17 @@ * `ItemLeadingSpace`/`ItemTrailingSpace`/container padding, C-14). * * `data-md-list-row` (`resources/js/list-rows.js`, shared with `` 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); } diff --git a/resources/css/components/table.css b/resources/css/components/table.css index 3092bdf2..fde8b65c 100644 --- a/resources/css/components/table.css +++ b/resources/css/components/table.css @@ -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 `` and - * ``). + * ``); 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); } } diff --git a/resources/js/list-rows.js b/resources/js/list-rows.js index 1b6dbe5d..8587f31e 100644 --- a/resources/js/list-rows.js +++ b/resources/js/list-rows.js @@ -142,5 +142,5 @@ document.addEventListener('auxclick', (event) => { // iOS Safari paints `:active` on an element that is not a link or a button only // when some touch listener exists, and a row is neither — without this the press -// layer in list.css never shows on an iPhone. +// layer in list-item.css and card.css never shows on an iPhone. document.addEventListener('touchstart', () => {}, { passive: true }) diff --git a/tests/Feature/Components/ListTest.php b/tests/Feature/Components/ListTest.php index 0f03656d..eb47fa13 100644 --- a/tests/Feature/Components/ListTest.php +++ b/tests/Feature/Components/ListTest.php @@ -1,5 +1,6 @@ not->toContain('data-md-list-item-video') ->toContain('data-md-lines="1"'); }); + +it('lays a row\'s state layer over its fill, so a selected or segmented row keeps its colour under the tint', function () { + $css = ComponentStylesheet::read('list-item'); + $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))'; + $press = '[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))'; + + expect($css->declarations($hover, ['@media (hover: hover)'])) + ->toBe(['background-color' => 'color-mix(in srgb, currentColor calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent))']) + ->and($css->declarations($press)) + ->toBe(['background-color' => 'color-mix(in srgb, currentColor calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent))']) + ->and($css->declarations('[data-md-list-row]:not([data-md-card]):has([data-md-list-open]:focus-visible)')) + ->toHaveKey('background-color', 'color-mix(in srgb, currentColor calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent))') + ->and($css->declarations(':where([data-md-list-row])'))->toBe(['--md-list-row-fill' => 'transparent']) + ->and($css->declarations("[data-md-list='segmented'] > [data-md-list-item]"))->toHaveKey('--md-list-row-fill', 'var(--md-sys-color-surface)') + ->and($css->declarations('[data-md-list-item][data-md-selected]'))->toHaveKey('--md-list-row-fill', 'var(--md-sys-color-secondary-container)') + ->and(File::get(ComponentStylesheet::path('table')))->toContain('--md-list-row-fill: var(--md-sys-color-secondary-container);'); + + // The segmented fill comes first: on the same weight, a selected tile's secondary-container wins. + expect(strpos($css->css, "[data-md-list='segmented'] > [data-md-list-item] {"))->toBeLessThan(strpos($css->css, '[data-md-list-item][data-md-selected] {')); +}); + +it('inks a disabled item as a whole at the disabled-content opacity', function () { + expect(ComponentStylesheet::read('list-item')->declarations("[data-md-list-item][aria-disabled='true']"))->toBe([ + 'pointer-events' => 'none', + 'color' => 'color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)', + ]); +});