Files
livewire-material/resources/views/components/card.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 c298b19e75 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
2026-09-14 22:00:49 +02:00

118 lines
5.7 KiB
PHP

{{-- An M3 card: content and actions about one subject.
`variant` is M3's three (FilledCardTokens, ElevatedCardTokens, OutlinedCardTokens, androidx
Compose Material 3, Apache-2.0): `filled` (surface-container-highest, the default),
`elevated` (surface-container-low at elevation 1) and `outlined` (surface, an outline-variant
edge); all with a medium corner. maryUI's props keep their meaning: `title`, `subtitle`,
`separator` (a divider under the header), and the `menu` (top-end, beside the title),
`figure` (full-bleed media on top) and `actions` (end-aligned, under the content) slots.
M3 draws two kinds of card a person can press, and they differ in the keyboard: on a
**non-actionable card with actionable elements** Tab moves through each control inside before
the next card; on a **directly actionable card** Tab moves to the card, then to the next card
(docs/reference/m3/components-actions-communication-containment.md § Cards Accessibility).
Both are a row: give the card `data-md-list-row` and one `data-md-list-open` control inside
(the title link or a button), and a press anywhere on it reaches that control while its other
buttons keep their own (resources/js/list-rows.js). That is the first kind the opener is
the tab stop, the accessible name and the owner of the `href` or the `wire:click`.
`actionable` makes it the second: the card takes `tabindex="0"` and `role="button"` (pass
`role="link"` where it goes somewhere), is named by its `title`, and Enter or Space on it
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.
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, 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
(docs/reference/m3/components-actions-communication-containment.md § Cards → Specs,
Accessibility).
Do not pass a `bg-*` class to change its fill — the package's own rule for the variant sits in
`material.components`, and an application's unlayered CSS is the place to override it; use
`variant`, or colour a wrapper inside. --}}
@props([
'title' => null,
'subtitle' => null,
'variant' => 'filled',
'separator' => false,
'actionable' => false,
])
@php
$variant = in_array($variant, ['filled', 'elevated', 'outlined'], true) ? $variant : 'filled';
$role = $actionable ? ($attributes->get('role') ?: 'button') : null;
$isRow = $actionable || $attributes->has('data-md-list-row');
$attributes = $actionable ? $attributes->except('role') : $attributes;
@endphp
<div
{{ $attributes->merge(array_filter([
'data-md-card' => $variant,
'data-md-list-row' => $actionable ? true : null,
'data-md-list-actionable' => $actionable ? true : null,
'tabindex' => $actionable ? '0' : null,
'role' => $role,
'aria-label' => $actionable && filled($title) ? $title : null,
], fn ($value): bool => $value !== null))->class([
// Every card: the application sets `data-md-dragged` from script, after this renders.
'md-state-layer',
'md-focus-ring' => $isRow,
]) }}
>
@isset($figure)
<div data-md-card-figure>{{ $figure }}</div>
@endisset
<div data-md-card-body>
@if ($title || $subtitle || isset($menu))
<div data-md-card-header>
<div data-md-card-header-row>
<div data-md-card-heading>
@if ($title)
<h3 data-md-card-title>{{ $title }}</h3>
@endif
@if ($subtitle)
<p data-md-card-subtitle>{{ $subtitle }}</p>
@endif
</div>
@isset($menu)
<div data-md-card-menu>{{ $menu }}</div>
@endisset
</div>
@if ($separator)
<x-livewire-material::divider />
@endif
</div>
@endif
@if ($slot->isNotEmpty())
<div data-md-card-content>{{ $slot }}</div>
@endif
@isset($actions)
<div data-md-card-actions>{{ $actions }}</div>
@endisset
</div>
</div>