From 675cfc806aaa52cc02bdc1bb2a73c645d041ab37 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 06:07:14 +0200 Subject: [PATCH] Give a card per-state elevation and M3's two keyboard models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step 19, containment.md C-13, C-23 and the directly actionable card of C-13/C-18. A card had one elevation and never changed it, and its hover replaced the elevated card's shadow with the state layer; it now names its variant in `data-card` and moves a step on hover (elevated 1 to 2, filled and outlined 0 to 1) with the layer over it. The 12 to 16 corner morph on hover is gone: M3 gives a card one shape and lists shape morph for buttons, FABs and list items only. New `actionable` prop for M3's directly actionable card — the card is the one tab stop, with `role="button"` (or `link`), its `title` as the name, and Enter or Space reaching the opener from resources/js/list-rows.js. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../livewire-material-development/SKILL.md | 4 ++- resources/css/components/list.css | 25 ++++++++++--- resources/js/list-rows.js | 26 ++++++++++++++ resources/views/components/card.blade.php | 35 +++++++++++++++---- .../showcase/sections/containment.blade.php | 9 +++++ tests/Browser/ContainmentTest.php | 22 ++++++++++-- tests/Feature/Components/CardTest.php | 28 +++++++++++++-- 7 files changed, 131 insertions(+), 18 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 6f1b901a..361a3660 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -420,7 +420,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`; slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). Do not pass `bg-*`; use `variant`. -A card or list item that opens something is a **row**: `data-list-row` on it and `data-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 `` or use a stretched link. +A card or list item that opens something is a **row**: `data-list-row` on it and `data-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 `` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. ```blade @@ -429,6 +429,8 @@ A card or list item that opens something is a **row**: `data-list-row` on it and ``` +That is M3's *non-actionable card with actionable elements*: Tab walks the controls inside. For M3's *directly actionable card*, where Tab lands on the card and then moves to the next card, add `actionable` (and `role="link"` where it goes somewhere) and put `tabindex="-1"` on the opener — the card is then the tab stop, named by its `title`, and Enter or Space on it reaches the opener while the card's other actions follow it. + ### ``, `` ``: `label`, `dividers`, `segmented` (M3 Expressive: separate tiles 2px apart). ``: `title` (or slot), `overline`, `description`, leading `icon` / `avatar` (image URL or initials) / `image` / `leading` slot, trailing `trailing` text / `icon-right` / `end` slot, `link` (the whole item becomes a row that opens it), `selected`, `disabled`. One-, two- and three-line heights follow from the content, and a three-line item top-aligns as M3 asks. Its icons are 24px, 20px in a `segmented` list. diff --git a/resources/css/components/list.css b/resources/css/components/list.css index 27431622..a0454748 100644 --- a/resources/css/components/list.css +++ b/resources/css/components/list.css @@ -71,17 +71,27 @@ color: var(--md-sys-color-on-secondary-container); } -/* A card that opens answers with its container one tone up and its corner opening a step. */ +/* + * A card that opens answers with M3's state layer and one step of elevation, both written as one + * box-shadow because the elevated card's rest level is a `shadow-elevation-1` utility that only + * an unlayered rule can replace. Per-state elevation from the token files (the specs page's + * Hovered / Focused / Pressed rows): elevated 1 → 2 hovered → 1 focused and pressed; filled and + * outlined 0 → 1 hovered → 0. Its corner does not move: M3 gives a card one shape and specifies + * shape morph for buttons, FABs and list items, never for cards. + */ [data-card][data-list-row] { - transition-property: border-radius, background-color, box-shadow; + transition-property: background-color, box-shadow; transition-duration: var(--md-sys-motion-spatial-default-duration); transition-timing-function: var(--md-sys-motion-spatial-default); } @media (hover: hover) { [data-card][data-list-row]:hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):hover)) { - border-radius: var(--md-sys-shape-corner-lg); - box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); + box-shadow: var(--md-sys-elevation-1), inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); + } + + [data-card='elevated'][data-list-row]:hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):hover)) { + box-shadow: var(--md-sys-elevation-2), inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); } } @@ -89,7 +99,12 @@ box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent); } -[data-card][data-list-row]:has([data-list-open]:focus-visible) { +[data-card='elevated'][data-list-row]:active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):active)) { + box-shadow: var(--md-sys-elevation-1), inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent); +} + +/* Focus is on the card itself when the card is the tab stop (``). */ +[data-card][data-list-row]:is(:focus-visible, :has([data-list-open]:focus-visible)) { outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; } diff --git a/resources/js/list-rows.js b/resources/js/list-rows.js index 98669913..a9f624eb 100644 --- a/resources/js/list-rows.js +++ b/resources/js/list-rows.js @@ -11,6 +11,11 @@ * Not a stretched link (`::after { inset: 0 }`): Safari makes no containing block of a , so in * a table every overlay would cover the whole table; and not one