diff --git a/resources/css/components/card.css b/resources/css/components/card.css index bf6dffeb..7a1c3a4d 100644 --- a/resources/css/components/card.css +++ b/resources/css/components/card.css @@ -6,10 +6,13 @@ * which never morphs (M3 specifies shape morph for buttons, FABs and list items, never cards — * docs/audits/m3-alignment/containment.md, C-23). * - * A row (`data-md-list-row`, `resources/js/list-rows.js`) renders the shared `md-state-layer` and - * `md-focus-ring` classes (foundation/interaction.css) for its tint and its ring; this file adds - * only what a card needs beyond them: + * Every card renders the shared `md-state-layer` class, and a row (`data-md-list-row`, + * `resources/js/list-rows.js`) `md-focus-ring` too (foundation/interaction.css), for its tint and + * its ring; this file adds only what a card needs beyond them: * + * - *no layer* on a card that is neither a row nor dragged: a card that does not open anything + * answers no pointer, but any card can be carried, and the application sets `data-md-dragged` + * from script long after the view rendered; * - per-state *elevation*, which the shared class has no opinion on: elevated 1dp rest → 2dp * hover → 1dp focus/pressed; filled and outlined 0dp rest → 1dp hover → 0dp focus/pressed * (the cross-check table's Hover/Focus/Pressed rows). Pressed and focus fall back to the rest @@ -57,6 +60,10 @@ background-color: var(--md-sys-color-surface); } + [data-md-card]:not([data-md-list-row], [data-md-dragged])::before { + display: none; + } + [data-md-card][data-md-list-row] { cursor: pointer; transition-property: box-shadow; @@ -157,7 +164,7 @@ margin-inline-end: calc(-1 * var(--md-sys-measurement-space100)); } - [data-md-card-header] [data-md-divider] { + [data-md-card-header] > [data-md-divider] { margin-top: var(--md-sys-measurement-space200); } diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php index b898433e..816f588b 100644 --- a/resources/views/components/card.blade.php +++ b/resources/views/components/card.blade.php @@ -22,20 +22,21 @@ reaches the opener. Put `tabindex="-1"` on that opener so the card is the one stop, and leave the card's other actions as they are — they follow it in the tab order. - A row (`data-md-list-row`, `actionable` or not) renders the shared `md-state-layer` and - `md-focus-ring` classes (foundation/interaction.css), which draw the tint and the ring for a - card focused directly; resources/css/components/card.css refines their opacity when the - pointer or the keyboard is on one of the card's *own* buttons instead (which should light only - itself), and adds the `:has([data-md-list-open]:focus-visible)` ring for the non-actionable - row, whose focus never lands on the card itself. Elevation is separate from the tint — M3 - gives each variant its own per-state box-shadow (elevated 1dp rest → 2dp hover → 1dp - focus/pressed; filled and outlined 0dp rest → 1dp hover → 0dp focus/pressed) — so it is drawn - directly on `[data-md-card]`, never inside the shared class. Its corner does not move, because + Every card renders the shared `md-state-layer` class and a row (`data-md-list-row`, + `actionable` or not) `md-focus-ring` too (foundation/interaction.css), which draw the tint and + the ring for a card focused directly; resources/css/components/card.css keeps the layer off a + card that is neither a row nor dragged, refines its opacity when the pointer or the keyboard is + on one of the card's *own* buttons instead (which should light only itself), and adds the + `:has([data-md-list-open]:focus-visible)` ring for the non-actionable row, whose focus never + lands on the card itself. Elevation is separate from the tint — M3 gives each variant its own + per-state box-shadow (elevated 1dp rest → 2dp hover → 1dp focus/pressed; filled and outlined + 0dp rest → 1dp hover → 0dp focus/pressed) — so it is drawn directly on `[data-md-card]`, never + inside the shared class. Its corner does not move, because M3 gives a card one shape (docs/audits/m3-alignment/containment.md, C-13, C-23). - `data-md-dragged` is M3's dragged card: the top of a card's elevation scale — 8dp elevated, - 6dp filled and outlined — under the shared class's own 16% dragged tint (it already reads - `data-md-dragged`, foundation/interaction.css). Nothing in the browser tells a card it is + `data-md-dragged` is M3's dragged card, row or not: the top of a card's elevation scale — 8dp + elevated, 6dp filled and outlined — under the shared class's own 16% dragged tint (it already + reads `data-md-dragged`, foundation/interaction.css). Nothing in the browser tells a card it is being carried, so the application sets the attribute when its drag starts and takes it off on drop. M3 requires a single-pointer alternative beside any drag, so keep the same reorder or delete actions in a menu on the card @@ -71,7 +72,8 @@ 'role' => $role, 'aria-label' => $actionable && filled($title) ? $title : null, ], fn ($value): bool => $value !== null))->class([ - 'md-state-layer' => $isRow, + // Every card: the application sets `data-md-dragged` from script, after this renders. + 'md-state-layer', 'md-focus-ring' => $isRow, ]) }} > diff --git a/tests/Feature/Components/CardTest.php b/tests/Feature/Components/CardTest.php index cfdd7834..c7fca4d5 100644 --- a/tests/Feature/Components/CardTest.php +++ b/tests/Feature/Components/CardTest.php @@ -46,9 +46,13 @@ it('passes row attributes through and renders the shared interaction classes for ->toContain('data-md-list-row') ->toContain('data-md-list-open') ->toContain('class="md-state-layer md-focus-ring"') + // Any card can be carried, so each draws the shared layer, which card.css keeps off one + // that is neither a row nor dragged. ->and((string) $this->blade('Body')) - ->not->toContain('md-state-layer') - ->not->toContain('md-focus-ring'); + ->toContain('class="md-state-layer"') + ->not->toContain('md-focus-ring') + ->and(ComponentStylesheet::read('card')->declarations('[data-md-card]:not([data-md-list-row], [data-md-dragged])::before')) + ->toBe(['display' => 'none']); }); it('makes a directly actionable card the one tab stop, named by its title', function () {