Draw M3's dragged state on a card
Plan step 23, containment.md § Missing ("Cards: the dragged state").
`data-dragged` on `<x-card>` takes the top of M3's card elevation scale
— Level4 (8dp) elevated, Level3 (6dp) filled and outlined — under the
16% dragged state layer from tokens/state.css. The row's hover and press
rules now exclude a dragged card, so the card a pointer is holding stays
at its dragged level. The application owns the attribute: nothing in the
browser tells a card it is being carried.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
927e439064
commit
b1f64771f3
@@ -432,6 +432,8 @@ A card or list item that opens something is a **row**: `data-list-row` on it and
|
||||
</x-card>
|
||||
```
|
||||
|
||||
`data-dragged` on the card draws M3's dragged state — elevation 8dp elevated / 6dp filled and outlined, under the 16% dragged state layer. The application sets it when its drag starts and removes it on drop; pair any drag with a single-pointer alternative (a menu with the same actions), as M3 requires.
|
||||
|
||||
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.
|
||||
|
||||
### `<x-list>`, `<x-list-item>`
|
||||
|
||||
@@ -86,23 +86,45 @@
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-card][data-list-row]:hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):hover)) {
|
||||
[data-card][data-list-row]:not([data-dragged]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):hover)) {
|
||||
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)) {
|
||||
[data-card='elevated'][data-list-row]:not([data-dragged]):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);
|
||||
}
|
||||
}
|
||||
|
||||
[data-card][data-list-row]:active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):active)) {
|
||||
[data-card][data-list-row]:not([data-dragged]):active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):active)) {
|
||||
box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
||||
}
|
||||
|
||||
[data-card='elevated'][data-list-row]:active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):active)) {
|
||||
[data-card='elevated'][data-list-row]:not([data-dragged]):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);
|
||||
}
|
||||
|
||||
/*
|
||||
* M3's dragged card (`<x-card data-dragged>`): the highest elevation a card ever takes, under the
|
||||
* 16% dragged state layer. The token files' per-state elevation — elevated Level4 (8dp), filled
|
||||
* and outlined Level3 (6dp) — and md.sys.state.dragged-state-layer-opacity from tokens/state.css
|
||||
* (docs/reference/m3/components-actions-communication-containment.md § Cards → Specs). Written as
|
||||
* one box-shadow with the row's other states, and named in the hover and press rules above so the
|
||||
* card a pointer is holding stays at its dragged level rather than falling back to a press.
|
||||
*
|
||||
* The application owns the attribute: it sets `data-dragged` when its drag starts and takes it off
|
||||
* on drop, since nothing in the browser tells a card it is being carried. M3 asks for a
|
||||
* single-pointer alternative beside any drag, so give the card a menu with the same actions.
|
||||
*/
|
||||
[data-card][data-dragged] {
|
||||
box-shadow: var(--md-sys-elevation-3),
|
||||
inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-dragged-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
[data-card='elevated'][data-dragged] {
|
||||
box-shadow: var(--md-sys-elevation-4),
|
||||
inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-dragged-state-layer-opacity) * 100%), transparent);
|
||||
}
|
||||
|
||||
/* Focus is on the card itself when the card is the tab stop (`<x-card actionable>`). */
|
||||
[data-card][data-list-row]:is(:focus-visible, :has([data-list-open]:focus-visible)) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
filled and outlined 0 → 1); its corner does not move, because M3 gives a card one shape.
|
||||
Never a stretched link, and never a whole-card `<a>` around buttons.
|
||||
|
||||
`data-dragged` is M3's dragged card: the top of a card's elevation scale — 8dp elevated, 6dp
|
||||
filled and outlined — under the 16% dragged state layer, and it holds while the pointer is
|
||||
down rather than falling back to the press state. 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
|
||||
(docs/reference/m3/components-actions-communication-containment.md § Cards → Specs,
|
||||
Accessibility; resources/css/components/list.css draws it).
|
||||
|
||||
Do not pass a `bg-*` class to change its fill — it races the card's own in Tailwind's emit
|
||||
order; use `variant`, or colour a wrapper inside. --}}
|
||||
|
||||
|
||||
@@ -35,6 +35,14 @@
|
||||
</x-slot:actions>
|
||||
</x-card>
|
||||
BLADE,
|
||||
'A card being dragged' => <<<'BLADE'
|
||||
<div x-data="{ dragged: false }" class="w-full max-w-sm space-y-4">
|
||||
<x-button label="Pick it up or put it down" variant="tonal" x-on:click="dragged = ! dragged" />
|
||||
<x-card variant="elevated" title="holiday-photos.zip" subtitle="248 MB" x-bind:data-dragged="dragged || null">
|
||||
Elevation 4 and the 16% state layer, for as long as the application says the card is being carried.
|
||||
</x-card>
|
||||
</div>
|
||||
BLADE,
|
||||
'Lists' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 medium:grid-cols-2">
|
||||
<x-list dividers label="Files">
|
||||
|
||||
@@ -60,3 +60,18 @@ it('makes a directly actionable card the one tab stop, named by its title', func
|
||||
->not->toContain('data-list-actionable')
|
||||
->not->toContain('tabindex');
|
||||
});
|
||||
|
||||
it('draws M3\'s dragged state while the application says the card is being carried', function () {
|
||||
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/list.css');
|
||||
|
||||
expect((string) $this->blade('<x-card data-dragged variant="elevated">Body</x-card>'))
|
||||
->toContain('data-dragged')
|
||||
->and($css)
|
||||
->toContain('[data-card][data-dragged] {')
|
||||
->toContain('var(--md-sys-elevation-3),')
|
||||
->toContain("[data-card='elevated'][data-dragged] {")
|
||||
->toContain('var(--md-sys-elevation-4),')
|
||||
->toContain('calc(var(--md-sys-state-dragged-state-layer-opacity) * 100%)')
|
||||
->toContain(':not([data-dragged]):hover')
|
||||
->toContain(':not([data-dragged]):active');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user