diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 375e67c7..54463cb9 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -432,6 +432,8 @@ A card or list item that opens something is a **row**: `data-list-row` on it and ``` +`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. ### ``, `` diff --git a/resources/css/components/list.css b/resources/css/components/list.css index a0454748..0122f1ca 100644 --- a/resources/css/components/list.css +++ b/resources/css/components/list.css @@ -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 (``): 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 (``). */ [data-card][data-list-row]:is(:focus-visible, :has([data-list-open]:focus-visible)) { outline: 3px solid var(--md-sys-color-secondary); diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php index 9bf2ff5e..f4a092bf 100644 --- a/resources/views/components/card.blade.php +++ b/resources/views/components/card.blade.php @@ -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 `` 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. --}} diff --git a/resources/views/showcase/sections/containment.blade.php b/resources/views/showcase/sections/containment.blade.php index 0b2bc5a9..e29f1ff1 100644 --- a/resources/views/showcase/sections/containment.blade.php +++ b/resources/views/showcase/sections/containment.blade.php @@ -35,6 +35,14 @@ BLADE, + 'A card being dragged' => <<<'BLADE' +
+ + + Elevation 4 and the 16% state layer, for as long as the application says the card is being carried. + +
+ BLADE, 'Lists' => <<<'BLADE'
diff --git a/tests/Feature/Components/CardTest.php b/tests/Feature/Components/CardTest.php index 043069e0..9785767c 100644 --- a/tests/Feature/Components/CardTest.php +++ b/tests/Feature/Components/CardTest.php @@ -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('Body')) + ->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'); +});