Files
livewire-material/resources/views/components/carousel-item.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 9875ba156e Put the carousel's tab stop on its items, as M3 asks
Plan step 19, containment.md C-18. The row was the focusable `region`
and the items were not focusable at all, which is the thing M3's
accessibility page draws a Don't for: "use Tab to place initial focus on
the first carousel item", "avoid focusing on the carousel container".
Each item is now `tabindex="0"` with the focus ring drawn inside it, the
row is out of the tab order, and from a focused item the arrows move one
item (moving focus with them), Home and End go to the ends, and Space or
Enter opens one that is not fully in view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:14:34 +02:00

57 lines
2.8 KiB
PHP

{{-- One item of an `<x-carousel>`: a slide of art an `<img>`, which fills and crops to the
item, or any element sized `size-full`.
<x-carousel-item label="Lake Constance">
<img src="{{ $photo->url }}" alt="Lake Constance at dusk" />
</x-carousel-item>
`label` lays a short line of text over the bottom of the art, on a scrim, pinned to the
item's visible edge and fading out as the item narrows — Compose's carousel sample, which
fades its label chip in once the mask is wide enough to hold it. The scrim is what M3 asks
for under text on an image; the text itself is `inverse-on-surface`, a role a scheme and a
contrast profile follow, rather than a literal white.
A focusable `group` with `aria-roledescription="slide"`, named "n of m" by the carousel
around it. M3 puts the tab stop on the item, not on the row: Tab reaches the first item, the
arrow keys move between them and Space or Enter opens the focused one
(docs/reference/m3/components-actions-communication-containment.md § Carousel
Accessibility). The item is laid out at the carousel's large size and masked:
the surface inside is clipped by `--material-carousel-inset` from both sides with M3's
extra-large corner (28px, CarouselDefaults' item shape) and moved by
`--material-carousel-shift`, both written by resources/js/carousel.js. Without script the
item shows unmasked, at its `item-width`. Only inside `<x-carousel>`. --}}
@props([
'label' => null,
])
<div {{ $attributes
->class([
'focus-ring relative h-full w-(--material-carousel-slot) max-w-full shrink-0 snap-start snap-always rounded-corner-xl focus-visible:-outline-offset-3',
])
->merge([
'role' => 'group',
'aria-roledescription' => __('slide'),
'aria-label' => '[material-carousel-position]',
'data-material-carousel-item' => true,
'tabindex' => '0',
]) }}>
<div
data-material-carousel-surface
class="relative size-full overflow-hidden rounded-corner-xl bg-surface-container-highest text-on-surface translate-x-(--material-carousel-shift) [clip-path:inset(0_var(--material-carousel-inset,0px)_round_var(--md-sys-shape-corner-xl))]"
>
<div data-material-carousel-content class="size-full [&>img]:size-full [&>img]:object-cover">
{{ $slot }}
</div>
@if (filled($label))
<div
data-material-carousel-label
class="pointer-events-none absolute inset-x-0 bottom-0 flex bg-linear-to-t from-scrim/60 to-transparent px-4 pt-10 pb-4 opacity-(--material-carousel-label)"
>
<span class="truncate type-title-md text-inverse-on-surface translate-x-(--material-carousel-label-shift)">{{ $label }}</span>
</div>
@endif
</div>
</div>