Skip to content

Commit 664b3f1

Browse files
Carousel mousewheel (#1030)
* mousewheel support * improve docs * improve example * changesert
1 parent 0736b84 commit 664b3f1

File tree

8 files changed

+138
-17
lines changed

8 files changed

+138
-17
lines changed

.changeset/wild-tools-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik-ui/headless': patch
3+
---
4+
5+
feat: carousel now supports mousewheel navigation in vertical mode

apps/website/src/routes/docs/headless/carousel/auto-api/api.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,77 +21,98 @@ export const api = {
2121
{
2222
root: [
2323
{
24-
CarouselRootProps: [
24+
PublicCarouselRootProps: [
2525
{
2626
comment: 'The gap between slides',
27-
prop: 'gap?',
27+
prop: 'gap',
2828
type: 'number',
2929
},
3030
{
3131
comment: 'Number of slides to show at once',
32-
prop: 'slidesPerView?',
32+
prop: 'slidesPerView',
3333
type: 'number',
3434
},
3535
{
3636
comment: 'Whether the carousel is draggable',
37-
prop: 'draggable?',
37+
prop: 'draggable',
3838
type: 'boolean',
3939
},
4040
{
4141
comment: 'Alignment of slides within the viewport',
42-
prop: 'align?',
42+
prop: 'align',
4343
type: "'start' | 'center' | 'end'",
4444
},
4545
{
4646
comment: 'Whether the carousel should rewind',
47-
prop: 'rewind?',
47+
prop: 'rewind',
4848
type: 'boolean',
4949
},
5050
{
5151
comment: 'Bind the selected index to a signal',
52-
prop: "'bind:selectedIndex'?",
52+
prop: "'bind:selectedIndex'",
5353
type: 'Signal<number>',
5454
},
5555
{
5656
comment: 'change the initial index of the carousel on render',
57-
prop: 'startIndex?',
57+
prop: 'startIndex',
5858
type: 'number',
5959
},
6060
{
6161
comment:
62-
'@deprecated Use bind:selectedIndex instead\n Bind the current slide index to a signal',
63-
prop: "'bind:currSlideIndex'?",
62+
'@deprecated Use bind:selectedIndex instead\n Bind the current slide index to a signal',
63+
prop: "'bind:currSlideIndex'",
6464
type: 'Signal<number>',
6565
},
6666
{
6767
comment: 'Whether the carousel should autoplay',
68-
prop: "'bind:autoplay'?",
68+
prop: "'bind:autoplay'",
6969
type: 'Signal<boolean>',
7070
},
7171
{
7272
comment: 'the current progress of the carousel',
73-
prop: "'bind:progress'?",
73+
prop: "'bind:progress'",
7474
type: 'Signal<number>',
7575
},
7676
{
7777
comment: 'Time in milliseconds before the next slide plays during autoplay',
78-
prop: 'autoPlayIntervalMs?',
78+
prop: 'autoPlayIntervalMs',
7979
type: 'number',
8080
},
8181
{
8282
comment: '@internal Total number of slides',
83-
prop: '_numSlides?',
83+
prop: '_numSlides',
8484
type: 'number',
8585
},
8686
{
8787
comment: '@internal Whether this carousel has a title',
88-
prop: '_isTitle?',
88+
prop: '_isTitle',
8989
type: 'boolean',
9090
},
9191
{
9292
comment: 'The sensitivity of the carousel dragging',
93-
prop: 'sensitivity?',
94-
type: '{',
93+
prop: 'sensitivity',
94+
type: '{\n mouse?: number;\n touch?: number;\n }',
95+
},
96+
{
97+
comment:
98+
'The amount of slides to move when hitting the next or previous button',
99+
prop: 'move',
100+
type: 'number',
101+
},
102+
{
103+
comment: "The carousel's direction",
104+
prop: 'orientation',
105+
type: "'horizontal' | 'vertical'",
106+
},
107+
{
108+
comment: 'The maximum height of the slides. Needed in vertical carousels',
109+
prop: 'maxSlideHeight',
110+
type: 'number',
111+
},
112+
{
113+
comment: 'Whether the carousel should support mousewheel navigation',
114+
prop: 'mousewheel',
115+
type: 'boolean',
95116
},
96117
],
97118
},

apps/website/src/routes/docs/headless/carousel/examples/carousel.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.carousel-root {
22
width: 100%;
3+
position: relative;
34
}
45

56
.carousel-slide {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { component$, useStyles$ } from '@builder.io/qwik';
2+
import { Carousel } from '@qwik-ui/headless';
3+
4+
export default component$(() => {
5+
useStyles$(styles);
6+
7+
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
8+
9+
useStyles$(`
10+
.mousewheel-bullet {
11+
width: 10px;
12+
height: 10px;
13+
background: hsl(var(--muted));
14+
}
15+
16+
.mousewheel-bullet[data-active] {
17+
background-color: hsl(var(--primary));
18+
}
19+
20+
.mousewheel-pagination {
21+
display: flex;
22+
flex-direction: column;
23+
gap: 4px;
24+
position: absolute;
25+
top: 33%;
26+
right: 8px;
27+
}
28+
29+
`);
30+
31+
return (
32+
<Carousel.Root
33+
class="carousel-root"
34+
gap={30}
35+
orientation="vertical"
36+
maxSlideHeight={160}
37+
mousewheel
38+
>
39+
<div class="carousel-buttons">
40+
<Carousel.Previous>Prev</Carousel.Previous>
41+
<Carousel.Next>Next</Carousel.Next>
42+
</div>
43+
<Carousel.Scroller class="carousel-scroller">
44+
{colors.map((color) => (
45+
<Carousel.Slide key={color} class="carousel-slide">
46+
{color}
47+
</Carousel.Slide>
48+
))}
49+
</Carousel.Scroller>
50+
<Carousel.Pagination class="mousewheel-pagination">
51+
{colors.map((color) => (
52+
<Carousel.Bullet class="mousewheel-bullet" key={color} />
53+
))}
54+
</Carousel.Pagination>
55+
</Carousel.Root>
56+
);
57+
});
58+
59+
// internal
60+
import styles from './carousel.css?inline';

apps/website/src/routes/docs/headless/carousel/index.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ Both SSR and CSR are supported. In this example, we conditionally render the car
184184

185185
<Showcase name="csr" />
186186

187+
### Mousewheel
188+
189+
The carousel component also supports mousewheel navigation in the case of vertical carousels.
190+
191+
<Showcase name="mousewheel" />
192+
187193
### Rewind
188194

189195
Rewind the carousel by setting the `rewind` prop to `true`.

packages/kit-headless/src/components/carousel/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type CarouselContext = {
1010
nextButtonRef: Signal<HTMLButtonElement | undefined>;
1111
prevButtonRef: Signal<HTMLButtonElement | undefined>;
1212
isMouseDraggingSig: Signal<boolean>;
13+
isMouseWheelSig: Signal<boolean>;
1314
slideRefsArray: Signal<Array<Signal>>;
1415
bulletRefsArray: Signal<Array<Signal>>;
1516
currentIndexSig: Signal<number>;

packages/kit-headless/src/components/carousel/root.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export type PublicCarouselRootProps = PropsOf<'div'> & {
7070

7171
/** The maximum height of the slides. Needed in vertical carousels */
7272
maxSlideHeight?: number;
73+
74+
/** Whether the carousel should support mousewheel navigation */
75+
mousewheel?: boolean;
7376
};
7477

7578
export const CarouselBase = component$((props: PublicCarouselRootProps) => {
@@ -133,6 +136,7 @@ export const CarouselBase = component$((props: PublicCarouselRootProps) => {
133136
}
134137
return props.orientation ?? 'horizontal';
135138
});
139+
const isMouseWheelSig = useComputed$(() => props.mousewheel ?? false);
136140

137141
const titleId = `${localId}-title`;
138142

@@ -143,6 +147,7 @@ export const CarouselBase = component$((props: PublicCarouselRootProps) => {
143147
prevButtonRef,
144148
scrollStartRef,
145149
isMouseDraggingSig,
150+
isMouseWheelSig,
146151
slideRefsArray,
147152
bulletRefsArray,
148153
currentIndexSig,

packages/kit-headless/src/components/carousel/scroller.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import styles from './carousel.css?inline';
1414
import { isServer } from '@builder.io/qwik/build';
1515
import { useDebouncer } from '../../hooks/use-debouncer';
1616
import { useScroller } from './use-scroller';
17+
import { useCarousel } from './use-carousel';
1718

1819
export const CarouselScroller = component$((props: PropsOf<'div'>) => {
1920
useStyles$(styles);
@@ -27,6 +28,8 @@ export const CarouselScroller = component$((props: PropsOf<'div'>) => {
2728
const initialLoadSig = useSignal(true);
2829
const isNewPosOnLoadSig = useSignal(false);
2930

31+
const { validIndexesSig } = useCarousel(context);
32+
3033
const {
3134
startPosSig,
3235
transformSig,
@@ -210,6 +213,23 @@ export const CarouselScroller = component$((props: PropsOf<'div'>) => {
210213
context.currentIndexSig.value !== 0;
211214
});
212215

216+
const handleWheel = $(async (e: WheelEvent) => {
217+
if (!context.isDraggableSig.value || !context.scrollerRef.value) return;
218+
if (!context.isMouseWheelSig.value) return;
219+
220+
const validIndexes = validIndexesSig.value;
221+
const currentIndex = context.currentIndexSig.value;
222+
const currentPosition = validIndexes.indexOf(currentIndex);
223+
const direction = e.deltaY > 0 ? 1 : -1;
224+
225+
// check if in bounds
226+
const newPosition = Math.max(
227+
0,
228+
Math.min(currentPosition + direction, validIndexes.length - 1),
229+
);
230+
context.currentIndexSig.value = validIndexes[newPosition];
231+
});
232+
213233
useTask$(() => {
214234
initialLoadSig.value = false;
215235
});
@@ -224,6 +244,8 @@ export const CarouselScroller = component$((props: PropsOf<'div'>) => {
224244
preventdefault:touchstart
225245
preventdefault:touchmove
226246
onQVisible$={isNewPosOnLoadSig.value ? setInitialSlidePos : undefined}
247+
onWheel$={handleWheel}
248+
preventdefault:wheel
227249
>
228250
<div
229251
ref={context.scrollerRef}

0 commit comments

Comments
 (0)