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:
co-authored by
Claude Opus 5
parent
613ac20013
commit
b7e16476cd
@@ -17,6 +17,12 @@
|
||||
family now too: `border-collapse`, `border-spacing-*`, `from-10%`, `via-none`, `fill-none`,
|
||||
`stroke-*`, `text-shadow-*`, `text-inherit`, and Tailwind 4.2's `mauve`, `olive`, `mist` and
|
||||
`taupe` palettes. A test that expects no findings may fail on them.
|
||||
- **A selected row written by hand** — `data-md-list-row` with `data-md-selected` on an `<li>`, a
|
||||
`<div>` or a `<tr>`, not `<x-list-item>` or `<x-card>` — is drawn in `secondary-container` with
|
||||
`on-secondary-container` ink again, as 1.x's `data-list-row` with `data-selected` was; 2.0.0
|
||||
dropped the fill without saying so. Hover, focus and press now tint the fill instead of hiding
|
||||
it. In `<x-table>` a plain row takes `data-md-selected` too, beside `aria-selected="true"`. A
|
||||
card still takes no selected fill.
|
||||
|
||||
## From 2.0.0 to 2.1.0
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -966,6 +966,79 @@ it('keeps a selected segmented row\'s fill under the hover and focus tint', func
|
||||
->and($selectedFocusBackground)->not->toBe($selectedHoverBackground);
|
||||
});
|
||||
|
||||
/**
|
||||
* An element's background and ink, "background / color", once the transitions a change started on
|
||||
* it have run: a row moves its background on the effects springs.
|
||||
*/
|
||||
function settledPaint(string $element): string
|
||||
{
|
||||
return "(async () => { const element = {$element}; await Promise.allSettled(element.getAnimations().map((animation) => animation.finished)); const style = getComputedStyle(element); return style.backgroundColor + ' / ' + style.color })()";
|
||||
}
|
||||
|
||||
it('fills a selected row written by hand in secondary-container in both themes, but not a card', function () {
|
||||
Route::middleware('web')->get('/hand-made-selected-row-probe', fn () => Blade::render(<<<'BLADE'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<x-theme-script />
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body style="background-color: var(--md-sys-color-surface);">
|
||||
<div style="padding: var(--md-sys-measurement-space200); max-width: 400px;">
|
||||
<p id="swatch" style="background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container);">The role pair</p>
|
||||
<ul>
|
||||
<li id="selected-row" data-md-list-row data-md-selected><a href="#selected" data-md-list-open>Selected</a></li>
|
||||
<li id="plain-row" data-md-list-row><a href="#plain" data-md-list-open>Plain</a></li>
|
||||
</ul>
|
||||
<x-table>
|
||||
<tbody>
|
||||
<tr id="selected-table-row" data-md-selected><td>Selected, opening nothing</td></tr>
|
||||
</tbody>
|
||||
</x-table>
|
||||
<x-card id="selected-card" data-md-list-row data-md-selected>
|
||||
<a href="#card" data-md-list-open>A card</a>
|
||||
</x-card>
|
||||
</div>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
BLADE));
|
||||
|
||||
$page = visit('/hand-made-selected-row-probe')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
|
||||
|
||||
$swatch = settledPaint("document.getElementById('swatch')");
|
||||
$row = settledPaint("document.getElementById('selected-row')");
|
||||
$tableRow = settledPaint("document.getElementById('selected-table-row')");
|
||||
$light = $page->script($swatch);
|
||||
|
||||
// The role pair itself, on an `<li>` and on a plain `<tr>` in a table; a card keeps its own
|
||||
// container, and a row that is not selected has no fill.
|
||||
expect($page->script($row))->toBe($light)
|
||||
->and($page->script($tableRow))->toBe($light)
|
||||
->and($page->script("getComputedStyle(document.getElementById('plain-row')).backgroundColor"))->toBe('rgba(0, 0, 0, 0)')
|
||||
->and($page->script("getComputedStyle(document.getElementById('selected-card')).backgroundColor"))->not->toBe(explode(' / ', $light)[0]);
|
||||
|
||||
$page->script("document.documentElement.setAttribute('data-theme', 'dark')");
|
||||
$dark = $page->script($swatch);
|
||||
|
||||
expect($dark)->not->toBe($light)
|
||||
->and($page->script($row))->toBe($dark)
|
||||
->and($page->script($tableRow))->toBe($dark);
|
||||
|
||||
// The hover tint is laid over the fill, so it is neither the fill nor a plain row's hover.
|
||||
$page->hover('#selected-row');
|
||||
$selectedHover = explode(' / ', $page->script($row))[0];
|
||||
|
||||
$page->hover('#plain-row');
|
||||
$plainHover = explode(' / ', $page->script(settledPaint("document.getElementById('plain-row')")))[0];
|
||||
|
||||
expect($selectedHover)->not->toBe(explode(' / ', $dark)[0])
|
||||
->and($selectedHover)->not->toBe($plainHover)
|
||||
->and($page->script($row))->toBe($dark);
|
||||
});
|
||||
|
||||
/**
|
||||
* Collapses to watch move: one on its own with a paragraph under it, one bound to Alpine, and a
|
||||
* `name` group of two with the first open. Each body is tall enough for a height caught part-way
|
||||
|
||||
@@ -25,7 +25,7 @@ it('draws 52px rows, 36px dense ones, from its stylesheet in the components laye
|
||||
expect($css)->toContain('@layer material.components')
|
||||
->toMatch('/\\[data-md-table\\] \\{\\s*--cell-x: 12px;\\s*--cell-y: var\\(--md-sys-measurement-space200\\);/')
|
||||
->toMatch('/\\[data-md-table\\]\\[data-md-dense\\] \\{\\s*--cell-y: var\\(--md-sys-measurement-space100\\);/')
|
||||
->toContain('[data-md-table] :where(tbody tr[aria-selected=\'true\'])')
|
||||
->toContain('[data-md-table] :where(tbody tr:is([aria-selected=\'true\'], [data-md-selected]))')
|
||||
->and((string) file_get_contents(__DIR__.'/../../../resources/css/all.css'))->toContain("@import './components/table.css';");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user