Draw the dragged layer on a card that is not a row

Plan step 36 review of f04e31b3. The card rendered md-state-layer only
on a row, so a card the application marks data-md-dragged without being
a row, as the showcase's "A card being dragged" is, lost the 16% dragged
layer its old inset shadow drew and its header still promises. Every
card now renders the class, since the attribute arrives from script
after render, and card.css keeps the layer off a card that is neither a
row nor dragged. The divider under the header is matched as its child.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 22:00:49 +02:00
co-authored by Claude Opus 5
parent 7d3abdd506
commit c298b19e75
3 changed files with 32 additions and 19 deletions
+11 -4
View File
@@ -6,10 +6,13 @@
* which never morphs (M3 specifies shape morph for buttons, FABs and list items, never cards — * which never morphs (M3 specifies shape morph for buttons, FABs and list items, never cards —
* docs/audits/m3-alignment/containment.md, C-23). * 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 * Every card renders the shared `md-state-layer` class, and a row (`data-md-list-row`,
* `md-focus-ring` classes (foundation/interaction.css) for its tint and its ring; this file adds * `resources/js/list-rows.js`) `md-focus-ring` too (foundation/interaction.css), for its tint and
* only what a card needs beyond them: * 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 * - 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 * 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 * (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); 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] { [data-md-card][data-md-list-row] {
cursor: pointer; cursor: pointer;
transition-property: box-shadow; transition-property: box-shadow;
@@ -157,7 +164,7 @@
margin-inline-end: calc(-1 * var(--md-sys-measurement-space100)); 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); margin-top: var(--md-sys-measurement-space200);
} }
+15 -13
View File
@@ -22,20 +22,21 @@
reaches the opener. Put `tabindex="-1"` on that opener so the card is the one stop, and leave 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. 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 Every card renders the shared `md-state-layer` class and a row (`data-md-list-row`,
`md-focus-ring` classes (foundation/interaction.css), which draw the tint and the ring for a `actionable` or not) `md-focus-ring` too (foundation/interaction.css), which draw the tint and
card focused directly; resources/css/components/card.css refines their opacity when the the ring for a card focused directly; resources/css/components/card.css keeps the layer off a
pointer or the keyboard is on one of the card's *own* buttons instead (which should light only card that is neither a row nor dragged, refines its opacity when the pointer or the keyboard is
itself), and adds the `:has([data-md-list-open]:focus-visible)` ring for the non-actionable on one of the card's *own* buttons instead (which should light only itself), and adds the
row, whose focus never lands on the card itself. Elevation is separate from the tint M3 `:has([data-md-list-open]:focus-visible)` ring for the non-actionable row, whose focus never
gives each variant its own per-state box-shadow (elevated 1dp rest 2dp hover 1dp lands on the card itself. Elevation is separate from the tint M3 gives each variant its own
focus/pressed; filled and outlined 0dp rest 1dp hover 0dp focus/pressed) so it is drawn per-state box-shadow (elevated 1dp rest 2dp hover 1dp focus/pressed; filled and outlined
directly on `[data-md-card]`, never inside the shared class. Its corner does not move, because 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). 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, `data-md-dragged` is M3's dragged card, row or not: the top of a card's elevation scale 8dp
6dp filled and outlined under the shared class's own 16% dragged tint (it already reads elevated, 6dp filled and outlined under the shared class's own 16% dragged tint (it already
`data-md-dragged`, foundation/interaction.css). Nothing in the browser tells a card it is 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 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 drop. M3 requires a single-pointer alternative beside any drag, so keep the same reorder or
delete actions in a menu on the card delete actions in a menu on the card
@@ -71,7 +72,8 @@
'role' => $role, 'role' => $role,
'aria-label' => $actionable && filled($title) ? $title : null, 'aria-label' => $actionable && filled($title) ? $title : null,
], fn ($value): bool => $value !== null))->class([ ], 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, 'md-focus-ring' => $isRow,
]) }} ]) }}
> >
+6 -2
View File
@@ -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-row')
->toContain('data-md-list-open') ->toContain('data-md-list-open')
->toContain('class="md-state-layer md-focus-ring"') ->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('<x-card title="Share">Body</x-card>')) ->and((string) $this->blade('<x-card title="Share">Body</x-card>'))
->not->toContain('md-state-layer') ->toContain('class="md-state-layer"')
->not->toContain('md-focus-ring'); ->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 () { it('makes a directly actionable card the one tab stop, named by its title', function () {