Fill a selected row written by hand in secondary-container again

1.x drew `data-selected` on any `data-list-row` that was not a card in
secondary-container with on-secondary-container ink, whatever element
the row was. 2.0.0 kept that only for `<x-list-item selected>` and a
table row's `aria-selected="true"`, so an application's own `<li>`,
`<div>` or `<tr>` with `data-md-list-row` and `data-md-selected` - the
contract list-rows.js still describes - lost its selected state without
a word. list-item.css now fills such a row, cards and list items
excluded, and names the fill `--md-list-row-fill`, so hover, focus and
press tint it as they tint a selected list item rather than replacing
it (1.x's selected fill hid them). In `<x-table>`, `data-md-selected`
selects a plain row too, beside `aria-selected="true"`, which ARIA
allows on a row only in a grid. A browser test compares a hand-made
selected `<li>` and a plain selected `<tr>` with the role pair in the
light and dark themes, checks that a selected card keeps its own fill
and that hover tints the row's fill; it fails without the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-17 04:18:10 +02:00
co-authored by Claude Opus 5
parent 613ac20013
commit b7e16476cd
7 changed files with 102 additions and 6 deletions
@@ -514,7 +514,7 @@ The `illustration` slot draws the application's own artwork in place of the shap
`variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`, `heading` (the title's element, `h3` by default: pass `h2` for a card straight under the page's `h1`); slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). A card holds content and actions about one subject (M3); a table of many rows or a group of settings is a headed section, not a card. Choose its fill with `variant`, not a background of your own.
A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `<a>` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move.
A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `<a>` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. `data-md-selected` on a row that is not a card — your own `<li>`, `<div>` or `<tr>`, the one whose sheet is open — fills it secondary-container with on-secondary-container ink, the state layer over the fill (`<x-list-item selected>` writes it itself); a card takes no selected fill.
```blade
<x-card variant="outlined" data-md-list-row wire:key="share-{{ $share->id }}">
@@ -1141,7 +1141,7 @@ A choice of colour profile (see Colour profiles): a swatch per generated profile
### `<x-table>`, `<x-sort-header>`
A data table: write plain `<thead>`, `<tr>`, `<th>`, `<td>` inside `<x-table>`; the text classes on a cell (`md-text-end`, `md-nowrap`, `md-tabular`) always win over the table's own alignment. Rows are 52px — a target a finger can hit. `dense` tightens them to 36px and `size="xs"` is for a table inside a panel inside a panel (32px rows); M3 says density is always an opt-in, so neither is a default and both are yours to justify. Scrolling is yours: wrap it in an element that scrolls sideways (`overflow-x: auto` in your own CSS). A row that opens something is `data-md-list-row` with one `data-md-list-open` control; a selected row is `aria-selected="true"`.
A data table: write plain `<thead>`, `<tr>`, `<th>`, `<td>` inside `<x-table>`; the text classes on a cell (`md-text-end`, `md-nowrap`, `md-tabular`) always win over the table's own alignment. Rows are 52px — a target a finger can hit. `dense` tightens them to 36px and `size="xs"` is for a table inside a panel inside a panel (32px rows); M3 says density is always an opt-in, so neither is a default and both are yours to justify. Scrolling is yours: wrap it in an element that scrolls sideways (`overflow-x: auto` in your own CSS). A row that opens something is `data-md-list-row` with one `data-md-list-open` control; a selected row is `data-md-selected`, or `aria-selected="true"` in a table that is a grid — the only place ARIA allows it on a row.
`<x-sort-header column="size" :sort-by="$sortBy">Size</x-sort-header>` sorts through the Livewire property `sortBy` (`['column' => …, 'direction' => 'asc'|'desc']`; `model` names another), with `aria-sort`.
+12 -1
View File
@@ -15,7 +15,9 @@
* 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
* container's own colour takes over by inheritance. The same attribute on any other row that is
* not a card — an application's own `<li>`, `<div>` or `<tr>` — draws the same fill and ink, as
* 1.x's `data-selected` did; a card keeps its own container. 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).
@@ -93,6 +95,15 @@
color: var(--md-sys-color-on-secondary-container);
}
/* A row written by hand — list-rows.js's contract, not an `<x-list-item>` — selected the same
way. The state layers below are mixed over this fill, as they are over an item's. */
[data-md-list-row][data-md-selected]:not([data-md-card], [data-md-list-item]) {
--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;
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
+3 -2
View File
@@ -7,7 +7,8 @@
* always wins, whatever layer it comes from. Header cells in title-small on-surface-variant over an
* outline-variant rule, body cells in body-medium between outline-variant rules — the role itself,
* 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`
* (`data-md-selected`, or `aria-selected="true"` where the table is a grid, the only place ARIA
* allows it on a row) 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>`); 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.
@@ -84,7 +85,7 @@
border-bottom: 0;
}
[data-md-table] :where(tbody tr[aria-selected='true']) {
[data-md-table] :where(tbody tr:is([aria-selected='true'], [data-md-selected])) {
--md-list-row-fill: var(--md-sys-color-secondary-container);
background-color: var(--md-list-row-fill);
+5
View File
@@ -9,6 +9,11 @@
* keep their own clicks. How a row looks while that happens is
* resources/css/components/card.css and list-item.css.
*
* `data-md-selected` on a row that is not a card — the one whose sheet is open, say — draws M3's
* selected state, secondary-container under on-secondary-container ink, with the state layers over
* it (list-item.css; `<x-list-item selected>` writes it itself, and in `<x-table>` a plain row takes
* it too, table.css). A card keeps its own container.
*
* Not a stretched link (`::after { inset: 0 }`): Safari makes no containing block of a <tr>, so in
* a table every overlay would cover the whole table; and not one <button> around the row, which
* could hold no other buttons. The listeners sit on `document` and are added once.