Skip to content

Carousel mousewheel #1030

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading