Skip to content

Commit

Permalink
Carousel mousewheel (#1030)
Browse files Browse the repository at this point in the history
* mousewheel support

* improve docs

* improve example

* changesert
  • Loading branch information
thejackshelton authored Dec 24, 2024
1 parent 0736b84 commit 664b3f1
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-tools-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik-ui/headless': patch
---

feat: carousel now supports mousewheel navigation in vertical mode
55 changes: 38 additions & 17 deletions apps/website/src/routes/docs/headless/carousel/auto-api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,77 +21,98 @@ export const api = {
{
root: [
{
CarouselRootProps: [
PublicCarouselRootProps: [
{
comment: 'The gap between slides',
prop: 'gap?',
prop: 'gap',
type: 'number',
},
{
comment: 'Number of slides to show at once',
prop: 'slidesPerView?',
prop: 'slidesPerView',
type: 'number',
},
{
comment: 'Whether the carousel is draggable',
prop: 'draggable?',
prop: 'draggable',
type: 'boolean',
},
{
comment: 'Alignment of slides within the viewport',
prop: 'align?',
prop: 'align',
type: "'start' | 'center' | 'end'",
},
{
comment: 'Whether the carousel should rewind',
prop: 'rewind?',
prop: 'rewind',
type: 'boolean',
},
{
comment: 'Bind the selected index to a signal',
prop: "'bind:selectedIndex'?",
prop: "'bind:selectedIndex'",
type: 'Signal<number>',
},
{
comment: 'change the initial index of the carousel on render',
prop: 'startIndex?',
prop: 'startIndex',
type: 'number',
},
{
comment:
'@deprecated Use bind:selectedIndex instead\n Bind the current slide index to a signal',
prop: "'bind:currSlideIndex'?",
'@deprecated Use bind:selectedIndex instead\n Bind the current slide index to a signal',
prop: "'bind:currSlideIndex'",
type: 'Signal<number>',
},
{
comment: 'Whether the carousel should autoplay',
prop: "'bind:autoplay'?",
prop: "'bind:autoplay'",
type: 'Signal<boolean>',
},
{
comment: 'the current progress of the carousel',
prop: "'bind:progress'?",
prop: "'bind:progress'",
type: 'Signal<number>',
},
{
comment: 'Time in milliseconds before the next slide plays during autoplay',
prop: 'autoPlayIntervalMs?',
prop: 'autoPlayIntervalMs',
type: 'number',
},
{
comment: '@internal Total number of slides',
prop: '_numSlides?',
prop: '_numSlides',
type: 'number',
},
{
comment: '@internal Whether this carousel has a title',
prop: '_isTitle?',
prop: '_isTitle',
type: 'boolean',
},
{
comment: 'The sensitivity of the carousel dragging',
prop: 'sensitivity?',
type: '{',
prop: 'sensitivity',
type: '{\n mouse?: number;\n touch?: number;\n }',
},
{
comment:
'The amount of slides to move when hitting the next or previous button',
prop: 'move',
type: 'number',
},
{
comment: "The carousel's direction",
prop: 'orientation',
type: "'horizontal' | 'vertical'",
},
{
comment: 'The maximum height of the slides. Needed in vertical carousels',
prop: 'maxSlideHeight',
type: 'number',
},
{
comment: 'Whether the carousel should support mousewheel navigation',
prop: 'mousewheel',
type: 'boolean',
},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.carousel-root {
width: 100%;
position: relative;
}

.carousel-slide {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';

export default component$(() => {
useStyles$(styles);

const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];

useStyles$(`
.mousewheel-bullet {
width: 10px;
height: 10px;
background: hsl(var(--muted));
}
.mousewheel-bullet[data-active] {
background-color: hsl(var(--primary));
}
.mousewheel-pagination {
display: flex;
flex-direction: column;
gap: 4px;
position: absolute;
top: 33%;
right: 8px;
}
`);

return (
<Carousel.Root
class="carousel-root"
gap={30}
orientation="vertical"
maxSlideHeight={160}
mousewheel
>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
<Carousel.Pagination class="mousewheel-pagination">
{colors.map((color) => (
<Carousel.Bullet class="mousewheel-bullet" key={color} />
))}
</Carousel.Pagination>
</Carousel.Root>
);
});

// internal
import styles from './carousel.css?inline';
6 changes: 6 additions & 0 deletions apps/website/src/routes/docs/headless/carousel/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ Both SSR and CSR are supported. In this example, we conditionally render the car

<Showcase name="csr" />

### Mousewheel

The carousel component also supports mousewheel navigation in the case of vertical carousels.

<Showcase name="mousewheel" />

### Rewind

Rewind the carousel by setting the `rewind` prop to `true`.
Expand Down
1 change: 1 addition & 0 deletions packages/kit-headless/src/components/carousel/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type CarouselContext = {
nextButtonRef: Signal<HTMLButtonElement | undefined>;
prevButtonRef: Signal<HTMLButtonElement | undefined>;
isMouseDraggingSig: Signal<boolean>;
isMouseWheelSig: Signal<boolean>;
slideRefsArray: Signal<Array<Signal>>;
bulletRefsArray: Signal<Array<Signal>>;
currentIndexSig: Signal<number>;
Expand Down
5 changes: 5 additions & 0 deletions packages/kit-headless/src/components/carousel/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export type PublicCarouselRootProps = PropsOf<'div'> & {

/** The maximum height of the slides. Needed in vertical carousels */
maxSlideHeight?: number;

/** Whether the carousel should support mousewheel navigation */
mousewheel?: boolean;
};

export const CarouselBase = component$((props: PublicCarouselRootProps) => {
Expand Down Expand Up @@ -133,6 +136,7 @@ export const CarouselBase = component$((props: PublicCarouselRootProps) => {
}
return props.orientation ?? 'horizontal';
});
const isMouseWheelSig = useComputed$(() => props.mousewheel ?? false);

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

Expand All @@ -143,6 +147,7 @@ export const CarouselBase = component$((props: PublicCarouselRootProps) => {
prevButtonRef,
scrollStartRef,
isMouseDraggingSig,
isMouseWheelSig,
slideRefsArray,
bulletRefsArray,
currentIndexSig,
Expand Down
22 changes: 22 additions & 0 deletions packages/kit-headless/src/components/carousel/scroller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import styles from './carousel.css?inline';
import { isServer } from '@builder.io/qwik/build';
import { useDebouncer } from '../../hooks/use-debouncer';
import { useScroller } from './use-scroller';
import { useCarousel } from './use-carousel';

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

const { validIndexesSig } = useCarousel(context);

const {
startPosSig,
transformSig,
Expand Down Expand Up @@ -210,6 +213,23 @@ export const CarouselScroller = component$((props: PropsOf<'div'>) => {
context.currentIndexSig.value !== 0;
});

const handleWheel = $(async (e: WheelEvent) => {
if (!context.isDraggableSig.value || !context.scrollerRef.value) return;
if (!context.isMouseWheelSig.value) return;

const validIndexes = validIndexesSig.value;
const currentIndex = context.currentIndexSig.value;
const currentPosition = validIndexes.indexOf(currentIndex);
const direction = e.deltaY > 0 ? 1 : -1;

// check if in bounds
const newPosition = Math.max(
0,
Math.min(currentPosition + direction, validIndexes.length - 1),
);
context.currentIndexSig.value = validIndexes[newPosition];
});

useTask$(() => {
initialLoadSig.value = false;
});
Expand All @@ -224,6 +244,8 @@ export const CarouselScroller = component$((props: PropsOf<'div'>) => {
preventdefault:touchstart
preventdefault:touchmove
onQVisible$={isNewPosOnLoadSig.value ? setInitialSlidePos : undefined}
onWheel$={handleWheel}
preventdefault:wheel
>
<div
ref={context.scrollerRef}
Expand Down

0 comments on commit 664b3f1

Please sign in to comment.