{{-- M3 Expressive's carousel: a row of ``s that grow and shrink as they scroll. @foreach ($photos as $photo) {{ $photo->alt }} @endforeach `layout` is one of M3's four: - `multi-browse` (the default): large items at the start, then a medium and a small one, for browsing many — HorizontalMultiBrowseCarousel. `item-width` is the width large items would like to be (186px, Compose's sample); the carousel adjusts it so a whole arrangement fits, small items between 40 and 56px. - `hero`: one large item and a small one after it, `centered` between two small ones — HorizontalCenteredHeroCarousel and material-components-android's start-aligned hero. The large item fills the width unless `item-width` caps it, and more large items fit when it does. - `uncontained`: items keep `item-width`; the one cut off at the end narrows as it leaves — HorizontalUncontainedCarousel. No snapping, as Compose's uncontained fling. - `full-screen`: one item the width of the carousel at a time (FullScreenCarouselStrategy). `item-width` takes pixels or any CSS length. `height` is the items' height (205px, Compose's sample). `padding` is Compose's `contentPadding` in pixels (0): the first and last items rest that far in from the edges while items in between scroll to them. Items are 8px apart with M3's extra-large corner. The row is a native scroll container with CSS scroll snap, one item per swipe, as Compose's single-advance fling; touch, trackpad and Shift with the wheel scroll it. resources/js/ carousel.js ports Compose's keylines (Arrangement, Keylines, KeylineList, Strategy, KeylineSnapPosition and Carousel.kt at androidx commit 7ac433e44e797de53af85226797862687f37735f, Apache-2.0) and masks each item on every scroll frame, content at full size, so items change size between the keylines. Without script the row still scrolls and snaps, unmasked. WAI-ARIA's carousel pattern: the row is a focusable `region` with `aria-roledescription="carousel"`, named by `label` ("Carousel" by default); each item is a `group` with `aria-roledescription="slide"` named "n of m". With the row focused the arrow keys move one item, Home and End to the ends; focus moving into an item, or a press on one that is not fully open, brings it into focus. `controls` adds previous and next icon buttons under the row — by default only where the pointer is fine (a mouse or trackpad); `true` always, `false` never. Under reduced motion they scroll instantly and content no longer slides inside its mask. RTL mirrors the keylines, keys and buttons. Re-measures itself when resized, when a Livewire morph resets its styles and when items come and go. The row's id, which the buttons control, is new with every render; the row carries a `wire:key` (see ``), so a morph patches it in place — its scroll position and listeners kept — rather than swapping in a copy. --}} @props([ 'layout' => 'multi-browse', 'itemWidth' => null, 'height' => null, 'padding' => 0, 'centered' => false, 'label' => null, 'controls' => null, ]) @php $layout = in_array($layout, ['multi-browse', 'hero', 'uncontained', 'full-screen'], true) ? $layout : 'multi-browse'; $controls = $controls === null ? null : filter_var($controls, FILTER_VALIDATE_BOOL); $label ??= __('Carousel'); $scrollerId = 'material-carousel-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10)); $cssLength = fn (mixed $value): ?string => match (true) { $value === null, $value === '' => null, is_numeric($value) => ((float) $value).'px', default => (string) $value, }; // What the keylines are asked for, and the width items take before (or without) script. $preferredWidth = match ($layout) { 'multi-browse', 'uncontained' => $cssLength($itemWidth) ?? '186px', 'hero' => $cssLength($itemWidth), 'full-screen' => null, }; $slotWidth = match (true) { $layout === 'full-screen' => '100%', $preferredWidth === null => 'calc(100% - 64px)', default => $preferredWidth, }; // "n of m": every item rendered in the slot leaves a placeholder for its position. An inner // carousel has already replaced its own by the time this one renders. $slides = $slot->toHtml(); $slideCount = substr_count($slides, '[material-carousel-position]'); $slidePosition = 0; $slides = preg_replace_callback( '/\[material-carousel-position\]/', function () use (&$slidePosition, $slideCount): string { $slidePosition++; return e(__(':position of :count', ['position' => $slidePosition, 'count' => $slideCount])); }, $slides, ); $attributes = $attributes ->class('relative') ->merge(array_filter([ 'data-material-carousel' => $layout, 'data-centered' => $layout === 'hero' && $centered ? true : null, 'data-padding' => (string) max(0, (float) $padding), 'style' => implode('; ', array_filter([ $preferredWidth ? "--material-carousel-item-width: {$preferredWidth}" : null, "--material-carousel-slot: {$slotWidth}", '--material-carousel-height: '.($cssLength($height) ?? '205px'), ])), ], fn ($value): bool => $value !== null)); @endphp
@if ($preferredWidth) @endif
'material-carousel']) }} id="{{ $scrollerId }}" role="region" aria-roledescription="{{ __('carousel') }}" aria-label="{{ $label }}" tabindex="0" @class([ 'focus-ring flex h-(--material-carousel-height) gap-2 overflow-x-auto overflow-y-hidden overscroll-x-contain', '[scrollbar-width:none] [&::-webkit-scrollbar]:hidden', 'snap-x snap-mandatory' => $layout !== 'uncontained', ]) > {!! $slides !!}
@if ($controls !== false)
$controls === null, 'flex' => $controls === true, ])>
@endif