Skip to content

Commit d535539

Browse files
committed
fix(Carousel): remove gap and peek props, measure layout from DOM
gap and peek are styling concerns that break the headless contract. Consumers now control these via CSS (gap, padding utilities) and slide sizing (flex/width classes). The viewport measures actual slide offsets from the DOM for scroll-snap math instead of calculating from prop values.
1 parent 778b101 commit d535539

File tree

7 files changed

+21
-51
lines changed

7 files changed

+21
-51
lines changed

apps/docs/src/examples/components/carousel/basic.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<Carousel.Item
1010
v-for="i in 5"
1111
:key="i"
12-
class="flex items-center justify-center h-48 rounded-lg text-lg font-medium bg-surface-variant text-on-surface-variant"
12+
class="flex items-center justify-center h-48 rounded-lg text-lg font-medium bg-surface-variant text-on-surface-variant w-full shrink-0"
1313
:value="i"
1414
>
1515
Slide {{ i }}

apps/docs/src/examples/components/carousel/multi-slide.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
</script>
1313

1414
<template>
15-
<Carousel.Root circular :gap="12" :per-view="3">
16-
<Carousel.Viewport class="rounded-lg">
15+
<Carousel.Root circular :per-view="3">
16+
<Carousel.Viewport class="rounded-lg gap-3">
1717
<Carousel.Item
1818
v-for="item in items"
1919
:key="item.id"
20-
class="flex items-center justify-center h-32 rounded-lg text-sm font-medium bg-surface-variant text-on-surface-variant"
20+
class="flex items-center justify-center h-32 rounded-lg text-sm font-medium bg-surface-variant text-on-surface-variant flex-[0_0_calc((100%-1.5rem)/3)]"
2121
:value="item.id"
2222
>
2323
{{ item.label }}

apps/docs/src/examples/components/carousel/peek.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
</script>
1111

1212
<template>
13-
<Carousel.Root :gap="16" :peek="48" :per-view="1">
14-
<Carousel.Viewport class="rounded-lg">
13+
<Carousel.Root :per-view="1">
14+
<Carousel.Viewport class="rounded-lg gap-4 px-12">
1515
<Carousel.Item
1616
v-for="slide in slides"
1717
:key="slide.id"
18-
class="flex items-center justify-center h-40 rounded-lg text-lg font-medium"
18+
class="flex items-center justify-center h-40 rounded-lg text-lg font-medium flex-[0_0_100%]"
1919
:class="slide.color"
2020
:value="slide.id"
2121
>

packages/0/src/components/Carousel/CarouselItem.vue

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,9 @@
103103
return index >= selected && index < selected + perView
104104
})
105105
106-
const slideStyle = toRef(() => {
107-
const pv = carousel.perView.value
108-
const g = carousel.gap.value
109-
const p = carousel.peek.value
110-
// Each slide width accounts for gaps between visible slides and peek on both sides
111-
const gapTotal = (pv - 1) * g
112-
const peekTotal = 2 * p
113-
return {
114-
'scroll-snap-align': 'start',
115-
'flex': `0 0 calc((100% - ${gapTotal}px - ${peekTotal}px) / ${pv})`,
116-
}
117-
})
106+
const slideStyle = {
107+
'scroll-snap-align': 'start' as const,
108+
}
118109
119110
const slotProps = toRef((): CarouselItemSlotProps => ({
120111
id: String(ticket.id),
@@ -134,7 +125,7 @@
134125
'data-active': isActive.value || undefined,
135126
'data-disabled': isDisabled.value || undefined,
136127
'data-index': ticket.index,
137-
'style': slideStyle.value,
128+
'style': slideStyle,
138129
},
139130
}))
140131
</script>

packages/0/src/components/Carousel/CarouselRoot.vue

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@
4646
orientation?: CarouselOrientation
4747
/** Number of slides visible at once */
4848
perView?: number
49-
/** Gap between slides in pixels */
50-
gap?: number
51-
/** Amount of adjacent slide visible in pixels (peek) */
52-
peek?: number
5349
/** Autoplay interval in milliseconds. 0 disables autoplay. */
5450
autoplay?: number
5551
}
@@ -91,10 +87,6 @@
9187
orientation: Ref<CarouselOrientation>
9288
/** Number of slides visible at once */
9389
perView: Ref<number>
94-
/** Gap between slides in pixels */
95-
gap: Ref<number>
96-
/** Amount of adjacent slide visible in pixels */
97-
peek: Ref<number>
9890
/** Whether navigation wraps around */
9991
circular: Ref<boolean>
10092
/** Root ID for generating sub-component IDs */
@@ -132,8 +124,6 @@
132124
circular = false,
133125
orientation = 'horizontal',
134126
perView = 1,
135-
gap = 0,
136-
peek = 0,
137127
autoplay = 0,
138128
} = defineProps<CarouselRootProps>()
139129
@@ -193,8 +183,6 @@
193183
resume,
194184
orientation: toRef(() => orientation),
195185
perView: toRef(() => perView),
196-
gap: toRef(() => gap),
197-
peek: toRef(() => peek),
198186
circular: toRef(() => circular),
199187
rootId,
200188
viewportEl,

packages/0/src/components/Carousel/CarouselViewport.vue

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,15 @@
8484
8585
const { width } = useElementSize(el)
8686
87-
// Compute the width of a single slide step (slide width + gap)
87+
// Measure actual slide step from DOM layout (slide width + gap)
8888
const slideStep = toRef(() => {
89-
if (width.value === 0) return 0
90-
const pv = carousel.perView.value
91-
const g = carousel.gap.value
92-
const p = carousel.peek.value
93-
const slideWidth = (width.value - (pv - 1) * g - 2 * p) / pv
94-
return slideWidth + g
89+
const viewport = el.value
90+
if (!viewport || width.value === 0) return 0
91+
const first = viewport.children[0] as HTMLElement | undefined
92+
const second = viewport.children[1] as HTMLElement | undefined
93+
if (!first) return 0
94+
if (!second) return first.offsetWidth
95+
return second.offsetLeft - first.offsetLeft
9596
})
9697
9798
// Scroll → Selection sync: when user scrolls (drag/swipe), update step selection
@@ -217,8 +218,6 @@
217218
218219
const viewportStyle = toRef(() => {
219220
const isVertical = carousel.orientation.value === 'vertical'
220-
const g = carousel.gap.value
221-
const p = carousel.peek.value
222221
223222
return {
224223
'display': 'flex',
@@ -229,8 +228,6 @@
229228
'scrollbar-width': 'none',
230229
'cursor': snapDisabled.value ? 'grabbing' : 'grab',
231230
...(snapDisabled.value ? { 'user-select': 'none' } : {}),
232-
...(g > 0 ? { gap: `${g}px` } : {}),
233-
...(p > 0 ? { [isVertical ? 'padding-block' : 'padding-inline']: `${p}px` } : {}),
234231
} as Record<string, string | number>
235232
})
236233

packages/0/src/components/Carousel/index.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -620,15 +620,10 @@ describe('carousel', () => {
620620
expect(slideProps.attrs.style['scroll-snap-align']).toBe('start')
621621
})
622622

623-
it('should compute flex basis from perView and gap', async () => {
623+
it('should not include flex basis (consumer controls sizing)', async () => {
624624
let slideProps: any
625625

626626
mount(Carousel.Root, {
627-
props: {
628-
perView: 3,
629-
gap: 16,
630-
peek: 20,
631-
},
632627
slots: {
633628
default: () =>
634629
h(Carousel.Item as any, { value: 'a' }, {
@@ -642,8 +637,7 @@ describe('carousel', () => {
642637

643638
await nextTick()
644639

645-
// (perView - 1) * gap = 2 * 16 = 32, 2 * peek = 40
646-
expect(slideProps.attrs.style.flex).toBe('0 0 calc((100% - 32px - 40px) / 3)')
640+
expect(slideProps.attrs.style.flex).toBeUndefined()
647641
})
648642
})
649643
})

0 commit comments

Comments
 (0)