Scroll the full-screen carousel down the page, as M3's does
Plan step 19, containment.md C-11. The full-screen layout was another horizontal row with a 28px corner and a mask, where M3's "shows one edge-to-edge large item at a time and scrolls vertically". It is now a vertical scroll-snap column: items fill the row with no corner and no mask, 16dp apart, no end padding, the previous/next buttons point up and down and the arrow keys are Up and Down. The row is capped at the 840px medium window, which with the portrait rule in the header comment is as far as CSS can hold M3's "compact and medium, portrait only". The ported FullScreenCarouselStrategy keylines go with it: nothing measures them any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
9875ba156e
commit
5e58dd5449
@@ -499,7 +499,7 @@ An M3 bottom sheet, bound like `<x-modal>`: modal by default (scrim, inert page,
|
||||
</x-carousel>
|
||||
```
|
||||
|
||||
A row of items that change size between M3's keylines as it scrolls (native scroll snap; items are masked, content keeps its size). `<x-carousel>`: `layout` (`multi-browse` default, `hero`, `uncontained`, `full-screen`), `item-width` (px or any CSS length; the large size multi-browse aims for, the fixed size uncontained keeps, the cap for hero; 186 by default), `height` (205px), `padding` (px at the ends, **16** — M3's specs table; leading only for `uncontained`, none for `full-screen`), `centered` (hero), `label` (the region's name, "Carousel" by default), `controls` (previous/next buttons: default fine pointers only, `true` always, `false` never). `<x-carousel-item>`: slot is an `<img>` (fills and crops) or an element sized `size-full`; `label` overlays a line of text. A `region` of `slide` groups named "n of m", each item a tab stop and the row itself not one, as M3 asks; from a focused item the arrow keys move one item, Home/End go to the ends and Space/Enter opens one that is not fully in view. Works after a Livewire morph, in RTL and under reduced motion. Give items a `wire:key` in a loop.
|
||||
A row of items that change size between M3's keylines as it scrolls (native scroll snap; items are masked, content keeps its size). `<x-carousel>`: `layout` (`multi-browse` default, `hero`, `uncontained`, `full-screen` — one edge-to-edge item at a time scrolled **vertically**, which M3 gives to compact and medium windows in portrait only, and never to landscape), `item-width` (px or any CSS length; the large size multi-browse aims for, the fixed size uncontained keeps, the cap for hero; 186 by default), `height` (205px), `padding` (px at the ends, **16** — M3's specs table; leading only for `uncontained`, none for `full-screen`), `centered` (hero), `label` (the region's name, "Carousel" by default), `controls` (previous/next buttons: default fine pointers only, `true` always, `false` never). `<x-carousel-item>`: slot is an `<img>` (fills and crops) or an element sized `size-full`; `label` overlays a line of text. A `region` of `slide` groups named "n of m", each item a tab stop and the row itself not one, as M3 asks; from a focused item the arrow keys move one item, Home/End go to the ends and Space/Enter opens one that is not fully in view. Works after a Livewire morph, in RTL and under reduced motion. Give items a `wire:key` in a loop.
|
||||
|
||||
### `<x-chip>`
|
||||
|
||||
|
||||
+70
-32
@@ -40,8 +40,9 @@
|
||||
* compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
|
||||
* (carouselItem, calculateMaxScrollOffset, CarouselDefaults)
|
||||
*
|
||||
* The full-screen arrangement follows material-components-android's
|
||||
* FullScreenCarouselStrategy (one large item the size of the container).
|
||||
* The full-screen layout uses none of it: M3 gives it one edge-to-edge item at a time, scrolled
|
||||
* vertically, so the browser's own scroll snap is the whole of it and this script only works out
|
||||
* where each item comes to rest, for the buttons and the keys.
|
||||
*
|
||||
* Copyright 2023-2024 The Android Open Source Project
|
||||
*
|
||||
@@ -547,11 +548,6 @@ const heroKeylineList = (space, maxItemSize, itemSpacing, itemCount, isCentered)
|
||||
: leftAlignedKeylineList(space, itemSpacing, ANCHOR_SIZE, ANCHOR_SIZE, arrangement)
|
||||
}
|
||||
|
||||
const fullScreenKeylineList = (space, itemSpacing) =>
|
||||
space === 0
|
||||
? EMPTY
|
||||
: leftAlignedKeylineList(space, itemSpacing, ANCHOR_SIZE, ANCHOR_SIZE, { priority: 0, smallSize: 0, smallCount: 0, mediumSize: 0, mediumCount: 0, largeSize: space, largeCount: 1 })
|
||||
|
||||
// Strategy.kt ------------------------------------------------------------------------------
|
||||
|
||||
const shiftedKeylineListForContentPadding = (from, space, itemSpacing, contentPadding, pivot, pivotIndex) => {
|
||||
@@ -806,6 +802,7 @@ document.addEventListener('alpine:init', () => {
|
||||
snaps: [],
|
||||
maxScroll: 0,
|
||||
rtl: false,
|
||||
vertical: false,
|
||||
frame: null,
|
||||
target: null,
|
||||
targetAt: 0,
|
||||
@@ -861,14 +858,10 @@ document.addEventListener('alpine:init', () => {
|
||||
refresh() {
|
||||
const root = this.$root
|
||||
const scroller = this.$refs.scroller
|
||||
const space = scroller.clientWidth
|
||||
const itemSpacing = parseFloat(getComputedStyle(scroller).columnGap) || 0
|
||||
const padding = Number(root.dataset.padding) || 0
|
||||
const paddingEnd = Number(root.dataset.paddingEnd) || 0
|
||||
const probe = this.$refs.probe
|
||||
const preferred = probe ? probe.getBoundingClientRect().width : null
|
||||
const style = getComputedStyle(scroller)
|
||||
|
||||
state.rtl = getComputedStyle(scroller).direction === 'rtl'
|
||||
state.rtl = style.direction === 'rtl'
|
||||
state.vertical = root.dataset.materialCarousel === 'full-screen'
|
||||
state.items = [...scroller.children]
|
||||
.filter((element) => element.matches(ITEM))
|
||||
.map((element) => ({
|
||||
@@ -879,11 +872,33 @@ document.addEventListener('alpine:init', () => {
|
||||
written: '',
|
||||
}))
|
||||
|
||||
// M3's full-screen layout is one edge-to-edge item at a time, scrolled
|
||||
// vertically: no keylines, no mask, no end padding — the browser's scroll snap
|
||||
// does the whole of it, and this only works out where each item comes to rest.
|
||||
if (state.vertical) {
|
||||
const space = scroller.clientHeight
|
||||
const spacing = parseFloat(style.rowGap) || 0
|
||||
|
||||
state.strategy = null
|
||||
state.maxScroll = Math.max(0, (space + spacing) * state.items.length - spacing - space)
|
||||
state.snaps = state.items.map((_, index) => clamp(index * (space + spacing), 0, state.maxScroll))
|
||||
|
||||
this.render()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const space = scroller.clientWidth
|
||||
const itemSpacing = parseFloat(style.columnGap) || 0
|
||||
const padding = Number(root.dataset.padding) || 0
|
||||
const paddingEnd = Number(root.dataset.paddingEnd) || 0
|
||||
const probe = this.$refs.probe
|
||||
const preferred = probe ? probe.getBoundingClientRect().width : null
|
||||
|
||||
const count = state.items.length
|
||||
const keylines = {
|
||||
hero: () => heroKeylineList(space, preferred, itemSpacing, count, root.dataset.centered !== undefined),
|
||||
uncontained: () => uncontainedKeylineList(space, preferred ?? 0, itemSpacing),
|
||||
'full-screen': () => fullScreenKeylineList(space, itemSpacing),
|
||||
}[root.dataset.materialCarousel] ?? (() => multiBrowseKeylineList(space, preferred ?? 0, itemSpacing, count))
|
||||
|
||||
const strategy = count === 0 ? createStrategy(EMPTY, space, itemSpacing, 0, 0) : createStrategy(keylines(), space, itemSpacing, padding, paddingEnd)
|
||||
@@ -942,10 +957,10 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
state.target = null
|
||||
|
||||
const left = state.snaps[target]
|
||||
const offset = state.snaps[target]
|
||||
|
||||
if (left !== undefined && Math.abs(this.scrollOffset() - left) > 1) {
|
||||
this.$refs.scroller.scrollTo({ left: state.rtl ? -left : left, behavior: 'instant' })
|
||||
if (offset !== undefined && Math.abs(this.scrollOffset() - offset) > 1) {
|
||||
this.scrollTo(offset, 'instant')
|
||||
}
|
||||
}, SETTLE_MS)
|
||||
},
|
||||
@@ -955,6 +970,14 @@ document.addEventListener('alpine:init', () => {
|
||||
cancelAnimationFrame(state.frame)
|
||||
state.frame = null
|
||||
|
||||
// Nothing is masked in the vertical full-screen layout; only the buttons change.
|
||||
if (state.vertical) {
|
||||
this.buttons()
|
||||
state.mutations?.takeRecords()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const strategy = state.strategy
|
||||
|
||||
if (!strategy?.valid) {
|
||||
@@ -1003,6 +1026,15 @@ document.addEventListener('alpine:init', () => {
|
||||
item.surface.style.setProperty('--material-carousel-label', opacity.toFixed(3))
|
||||
})
|
||||
|
||||
this.buttons()
|
||||
|
||||
state.mutations?.takeRecords()
|
||||
},
|
||||
|
||||
/** A control that would scroll past an end is off. */
|
||||
buttons() {
|
||||
const scroll = this.scrollOffset()
|
||||
|
||||
if (this.$refs.previous) {
|
||||
this.$refs.previous.disabled = scroll <= 1
|
||||
}
|
||||
@@ -1010,13 +1042,21 @@ document.addEventListener('alpine:init', () => {
|
||||
if (this.$refs.next) {
|
||||
this.$refs.next.disabled = scroll >= state.maxScroll - 1
|
||||
}
|
||||
|
||||
state.mutations?.takeRecords()
|
||||
},
|
||||
|
||||
/** The scroll offset from the start edge, positive in both directions. */
|
||||
/** The scroll offset from the start edge, positive in every direction. */
|
||||
scrollOffset() {
|
||||
return clamp(Math.abs(this.$refs.scroller.scrollLeft), 0, state.maxScroll)
|
||||
const scroller = this.$refs.scroller
|
||||
|
||||
return clamp(state.vertical ? scroller.scrollTop : Math.abs(scroller.scrollLeft), 0, state.maxScroll)
|
||||
},
|
||||
|
||||
/** Scrolls to an offset on whichever axis this carousel runs along. */
|
||||
scrollTo(offset, behavior) {
|
||||
this.$refs.scroller.scrollTo({
|
||||
...(state.vertical ? { top: offset } : { left: state.rtl ? -offset : offset }),
|
||||
behavior,
|
||||
})
|
||||
},
|
||||
|
||||
/** The item nearest the current scroll position. */
|
||||
@@ -1052,19 +1092,14 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
scrollToItem(index) {
|
||||
if (!state.strategy?.valid || state.snaps[index] === undefined) {
|
||||
if (state.snaps[index] === undefined || (!state.vertical && !state.strategy?.valid)) {
|
||||
return
|
||||
}
|
||||
|
||||
state.target = index
|
||||
state.targetAt = performance.now()
|
||||
|
||||
const left = state.snaps[index]
|
||||
|
||||
this.$refs.scroller.scrollTo({
|
||||
left: state.rtl ? -left : left,
|
||||
behavior: state.reducedMotion.matches ? 'instant' : 'smooth',
|
||||
})
|
||||
this.scrollTo(state.snaps[index], state.reducedMotion.matches ? 'instant' : 'smooth')
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1093,8 +1128,8 @@ document.addEventListener('alpine:init', () => {
|
||||
return
|
||||
}
|
||||
|
||||
const forward = state.rtl ? 'ArrowLeft' : 'ArrowRight'
|
||||
const backward = state.rtl ? 'ArrowRight' : 'ArrowLeft'
|
||||
const forward = state.vertical ? 'ArrowDown' : state.rtl ? 'ArrowLeft' : 'ArrowRight'
|
||||
const backward = state.vertical ? 'ArrowUp' : state.rtl ? 'ArrowRight' : 'ArrowLeft'
|
||||
|
||||
const to = {
|
||||
[forward]: index + 1,
|
||||
@@ -1138,8 +1173,11 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
},
|
||||
|
||||
/** Whether item `index` is not fully in focus, so a press or focus should bring it there. */
|
||||
isMasked(index) {
|
||||
return parseFloat(state.items[index].surface.style.getPropertyValue('--material-carousel-inset')) > 0.5
|
||||
return state.vertical
|
||||
? Math.abs(state.snaps[index] - this.scrollOffset()) > 1
|
||||
: parseFloat(state.items[index].surface.style.getPropertyValue('--material-carousel-inset')) > 0.5
|
||||
},
|
||||
|
||||
destroy() {
|
||||
|
||||
@@ -19,15 +19,28 @@
|
||||
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>`. --}}
|
||||
item shows unmasked, at its `item-width`. In a `full-screen` carousel it is none of that: the
|
||||
item fills the row, edge to edge, with no corner and no mask. Only inside `<x-carousel>`. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
])
|
||||
|
||||
{{-- The layout of the `<x-carousel>` around it: the full-screen one is a vertical row of
|
||||
edge-to-edge items, which M3 gives no corner and no mask. --}}
|
||||
@aware([
|
||||
'layout' => 'multi-browse',
|
||||
])
|
||||
|
||||
@php
|
||||
$vertical = $layout === 'full-screen';
|
||||
@endphp
|
||||
|
||||
<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',
|
||||
'focus-ring relative h-full shrink-0 snap-start snap-always focus-visible:-outline-offset-3',
|
||||
'w-full' => $vertical,
|
||||
'w-(--material-carousel-slot) max-w-full rounded-corner-xl' => ! $vertical,
|
||||
])
|
||||
->merge([
|
||||
'role' => 'group',
|
||||
@@ -38,7 +51,10 @@
|
||||
]) }}>
|
||||
<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))]"
|
||||
@class([
|
||||
'relative size-full overflow-hidden bg-surface-container-highest text-on-surface',
|
||||
'rounded-corner-xl translate-x-(--material-carousel-shift) [clip-path:inset(0_var(--material-carousel-inset,0px)_round_var(--md-sys-shape-corner-xl))]' => ! $vertical,
|
||||
])
|
||||
>
|
||||
<div data-material-carousel-content class="size-full [&>img]:size-full [&>img]:object-cover">
|
||||
{{ $slot }}
|
||||
|
||||
@@ -19,8 +19,13 @@
|
||||
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).
|
||||
- `full-screen`: one edge-to-edge item at a time, scrolled **vertically** — M3: "this layout
|
||||
works best with content that is taller than it is wide, and scrolls vertically. It only
|
||||
works in portrait orientation in compact and medium breakpoints. Don't use this layout in
|
||||
landscape orientation." Items have no corner and no mask, 16px apart, no end padding, and
|
||||
the row is never wider than the 840px medium window it is meant for. `item-width` and
|
||||
`padding` do not apply; `height` is the height of each item, so give it the room a
|
||||
portrait image wants.
|
||||
`item-width` takes pixels or any CSS length. `height` is the items' height (205px, Compose's
|
||||
sample). `padding` is Compose's `contentPadding` in pixels: the first and last items rest
|
||||
that far in from the edges while items in between scroll to them. M3's specs table gives
|
||||
@@ -66,6 +71,7 @@
|
||||
|
||||
@php
|
||||
$layout = in_array($layout, ['multi-browse', 'hero', 'uncontained', 'full-screen'], true) ? $layout : 'multi-browse';
|
||||
$vertical = $layout === 'full-screen';
|
||||
$controls = $controls === null ? null : filter_var($controls, FILTER_VALIDATE_BOOL);
|
||||
$label ??= __('Carousel');
|
||||
$scrollerId = 'material-carousel-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
|
||||
@@ -82,8 +88,9 @@
|
||||
'hero' => $cssLength($itemWidth),
|
||||
'full-screen' => null,
|
||||
};
|
||||
// The full-screen item is `h-full w-full`: it takes the row, not a slot.
|
||||
$slotWidth = match (true) {
|
||||
$layout === 'full-screen' => '100%',
|
||||
$vertical => null,
|
||||
$preferredWidth === null => 'calc(100% - 64px)',
|
||||
default => $preferredWidth,
|
||||
};
|
||||
@@ -106,8 +113,8 @@
|
||||
// The specs table's leading and trailing padding: 16dp for multi-browse and hero, leading
|
||||
// only for uncontained, none for full-screen, which is edge to edge.
|
||||
$padding = max(0, (float) $padding);
|
||||
$paddingStart = $layout === 'full-screen' ? 0.0 : $padding;
|
||||
$paddingEnd = in_array($layout, ['uncontained', 'full-screen'], true) ? 0.0 : $padding;
|
||||
$paddingStart = $vertical ? 0.0 : $padding;
|
||||
$paddingEnd = $vertical || $layout === 'uncontained' ? 0.0 : $padding;
|
||||
|
||||
$attributes = $attributes
|
||||
->class('relative')
|
||||
@@ -118,7 +125,7 @@
|
||||
'data-padding-end' => (string) $paddingEnd,
|
||||
'style' => implode('; ', array_filter([
|
||||
$preferredWidth ? "--material-carousel-item-width: {$preferredWidth}" : null,
|
||||
"--material-carousel-slot: {$slotWidth}",
|
||||
$slotWidth ? "--material-carousel-slot: {$slotWidth}" : null,
|
||||
'--material-carousel-height: '.($cssLength($height) ?? '205px'),
|
||||
])),
|
||||
], fn ($value): bool => $value !== null));
|
||||
@@ -137,11 +144,11 @@
|
||||
aria-roledescription="{{ __('carousel') }}"
|
||||
aria-label="{{ $label }}"
|
||||
@class([
|
||||
'flex gap-2 overflow-x-auto overflow-y-hidden overscroll-x-contain',
|
||||
'flex',
|
||||
'[scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
|
||||
'h-(--material-carousel-height)' => $layout === 'full-screen',
|
||||
'h-[calc(var(--material-carousel-height)+1rem)] py-2' => $layout !== 'full-screen',
|
||||
'snap-x snap-mandatory' => $layout !== 'uncontained',
|
||||
'mx-auto h-(--material-carousel-height) max-w-210 snap-y snap-mandatory flex-col gap-4 overflow-x-hidden overflow-y-auto overscroll-y-contain' => $vertical,
|
||||
'h-[calc(var(--material-carousel-height)+1rem)] gap-2 overflow-x-auto overflow-y-hidden overscroll-x-contain py-2' => ! $vertical,
|
||||
'snap-x snap-mandatory' => ! $vertical && $layout !== 'uncontained',
|
||||
])
|
||||
>
|
||||
{!! $slides !!}
|
||||
@@ -154,22 +161,22 @@
|
||||
'flex' => $controls === true,
|
||||
])>
|
||||
<x-livewire-material::button
|
||||
icon="chevron_left"
|
||||
:icon="$vertical ? 'keyboard_arrow_up' : 'chevron_left'"
|
||||
variant="tonal"
|
||||
:tooltip="__('Previous')"
|
||||
aria-controls="{{ $scrollerId }}"
|
||||
x-ref="previous"
|
||||
x-on:click="previous()"
|
||||
class="rtl:-scale-x-100"
|
||||
:class="$vertical ? '' : 'rtl:-scale-x-100'"
|
||||
/>
|
||||
<x-livewire-material::button
|
||||
icon="chevron_right"
|
||||
:icon="$vertical ? 'keyboard_arrow_down' : 'chevron_right'"
|
||||
variant="tonal"
|
||||
:tooltip="__('Next')"
|
||||
aria-controls="{{ $scrollerId }}"
|
||||
x-ref="next"
|
||||
x-on:click="next()"
|
||||
class="rtl:-scale-x-100"
|
||||
:class="$vertical ? '' : 'rtl:-scale-x-100'"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
@endforeach
|
||||
</x-carousel>
|
||||
BLADE,
|
||||
'Full-screen' => <<<'BLADE'
|
||||
<x-carousel layout="full-screen" label="Wallpapers" height="240" :controls="true">
|
||||
'Full-screen: one item at a time, scrolled down' => <<<'BLADE'
|
||||
<x-carousel layout="full-screen" label="Wallpapers" height="320" :controls="true">
|
||||
@foreach ([
|
||||
['Morning', 'very-sunny', 'bg-linear-to-br from-primary-container to-tertiary-container text-on-primary-container'],
|
||||
['Noon', 'clam-shell', 'bg-linear-to-br from-secondary-container to-primary-container text-on-secondary-container'],
|
||||
|
||||
@@ -60,7 +60,7 @@ it('browses many by default: 186px items that snap one at a time, 205px high', f
|
||||
->toContain('x-ref="probe"')
|
||||
->toContain('data-padding="16"')
|
||||
->toContain('data-padding-end="16"')
|
||||
->toContain('h-[calc(var(--material-carousel-height)+1rem)] py-2')
|
||||
->toContain('h-[calc(var(--material-carousel-height)+1rem)] gap-2 overflow-x-auto overflow-y-hidden overscroll-x-contain py-2')
|
||||
->toContain('snap-x snap-mandatory')
|
||||
->toContain('snap-start snap-always')
|
||||
->toContain('rounded-corner-xl')
|
||||
@@ -95,15 +95,23 @@ it('lays out a hero that fills the width unless capped, centred on request', fun
|
||||
->not->toContain('data-centered');
|
||||
});
|
||||
|
||||
it('leaves an uncontained carousel unsnapped, and a full-screen one a page wide', function () {
|
||||
it('leaves an uncontained carousel unsnapped, and scrolls a full-screen one down the page', function () {
|
||||
expect((string) $this->blade('<x-carousel layout="uncontained" item-width="180"><x-carousel-item>A</x-carousel-item></x-carousel>'))
|
||||
->toContain('data-material-carousel="uncontained"')
|
||||
->toContain('--material-carousel-item-width: 180px')
|
||||
->not->toContain('snap-mandatory')
|
||||
->and((string) $this->blade('<x-carousel layout="full-screen" centered><x-carousel-item>A</x-carousel-item></x-carousel>'))
|
||||
->and((string) $this->blade('<x-carousel layout="full-screen" centered controls><x-carousel-item label="Morning">A</x-carousel-item></x-carousel>'))
|
||||
->toContain('data-material-carousel="full-screen"')
|
||||
->toContain('--material-carousel-slot: 100%')
|
||||
->toContain('snap-mandatory')
|
||||
// M3: one edge-to-edge item at a time, scrolled vertically, 16px apart, no end padding,
|
||||
// no corner, no mask, and never wider than the medium window it is meant for.
|
||||
->toContain('mx-auto h-(--material-carousel-height) max-w-210 snap-y snap-mandatory flex-col gap-4 overflow-x-hidden overflow-y-auto overscroll-y-contain')
|
||||
->toContain('data-padding="0" data-padding-end="0"')
|
||||
->toContain('aria-label="Previous"')
|
||||
->not->toContain('--material-carousel-slot')
|
||||
->not->toContain('snap-x')
|
||||
->not->toContain('rounded-corner-xl')
|
||||
->not->toContain('clip-path')
|
||||
->not->toContain('rtl:-scale-x-100')
|
||||
->not->toContain('x-ref="probe"')
|
||||
->not->toContain('data-centered')
|
||||
->and((string) $this->blade('<x-carousel layout="sideways"><x-carousel-item>A</x-carousel-item></x-carousel>'))
|
||||
|
||||
Reference in New Issue
Block a user