Skip to content
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

Improve the stability of swiping #1077

Merged
merged 3 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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/wet-schools-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'nuka-carousel': patch
---

Improve the stability of swiping
28 changes: 27 additions & 1 deletion docs/api/swiping.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ By default the carousel will allow you to drag the carousel to the next slide. Y

| Prop Name | Type | Default Value |
| :--------- | :-------- | :------------ |
| `minSwipeDistance` | `number` | 10 |
| `swiping` | `boolean` | `true` |

### Enabled (default)
### Enabled with `scrollDistance="slide"`

<Carousel scrollDistance="slide" showDots>
<div className="demo-slide bg-green-500" />
Expand All @@ -39,6 +40,31 @@ By default the carousel will allow you to drag the carousel to the next slide. Y
</Carousel>
```

### Enabled with `scrollDistance="screen"`

<Carousel scrollDistance="screen" showDots>
<div className="demo-slide bg-green-500" />
<div className="demo-slide bg-red-500" />
<div className="demo-slide bg-blue-500" />
<div className="demo-slide bg-yellow-500" />
<div className="demo-slide bg-gray-500" />
<div className="demo-slide bg-orange-500" />
<div className="demo-slide bg-blue-700" />
<div className="demo-slide bg-red-900" />
<div className="demo-slide bg-gray-800" />
<div className="demo-slide bg-green-500" />
</Carousel>

#### Code

```tsx
<Carousel scrollDistance="screen" showDots>
<img src="pexels-01.jpg" />
<img src="pexels-02.jpg" />
<img src="pexels-03.jpg" />
</Carousel>
```

### Disabled

<Carousel scrollDistance="slide" showDots swiping={false}>
Expand Down
59 changes: 45 additions & 14 deletions packages/nuka/src/Carousel/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import {
MouseEvent,
TouchEvent,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';

import { useInterval } from '../hooks/use-interval';
import { usePaging } from '../hooks/use-paging';
import { useDebounced } from '../hooks/use-debounced';
import { useMeasurement } from '../hooks/use-measurement';
import { useHover } from '../hooks/use-hover';
import { useKeyboard } from '../hooks/use-keyboard';
import { useReducedMotion } from '../hooks/use-reduced-motion';
import { CarouselProvider } from '../hooks/use-carousel';
import { CarouselProps, SlideHandle } from '../types';
import { cls, nint } from '../utils';
import { cls, isMouseEvent } from '../utils';
import { NavButtons } from './NavButtons';
import { PageIndicators } from './PageIndicators';

Expand All @@ -22,6 +29,7 @@ const defaults = {
dots: <PageIndicators />,
id: 'nuka-carousel',
keyboard: true,
minSwipeDistance: 50,
scrollDistance: 'screen',
showArrows: false,
showDots: false,
Expand All @@ -44,6 +52,7 @@ export const Carousel = forwardRef<SlideHandle, CarouselProps>(
dots,
id,
keyboard,
minSwipeDistance,
scrollDistance,
showArrows,
showDots,
Expand All @@ -69,16 +78,36 @@ export const Carousel = forwardRef<SlideHandle, CarouselProps>(
});

// -- handle touch scroll events
const onContainerScroll = useDebounced(() => {
if (!containerRef.current) return;
const [touchStart, setTouchStart] = useState<null | number>(null);
const [touchEnd, setTouchEnd] = useState<null | number>(null);

const onTouchStart = (e: MouseEvent | TouchEvent) => {
if (!swiping) return;
setTouchEnd(null);
setTouchStart(isMouseEvent(e) ? e.clientX : e.targetTouches[0].clientX);
};

const onTouchMove = (e: MouseEvent | TouchEvent) => {
if (!swiping) return;
setTouchEnd(isMouseEvent(e) ? e.clientX : e.targetTouches[0].clientX);
};

// find the closest page index based on the scroll position
const scrollLeft = containerRef.current.scrollLeft;
const closestPageIndex = scrollOffset.indexOf(
nint(scrollOffset, scrollLeft),
);
goToPage(closestPageIndex);
}, 100);
const onTouchEnd = () => {
if (!swiping) return;
if (!containerRef.current) return;
if (!touchStart || !touchEnd) return;

const distance = touchStart - touchEnd;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
if (isLeftSwipe || isRightSwipe) {
if (isLeftSwipe) {
goForward();
} else {
goBack();
}
}
};

// -- keyboard nav
useKeyboard({
Expand Down Expand Up @@ -147,10 +176,12 @@ export const Carousel = forwardRef<SlideHandle, CarouselProps>(
<div
className="nuka-overflow"
ref={containerRef}
onTouchMove={onContainerScroll}
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
onTouchStart={onTouchStart}
id="nuka-overflow"
data-testid="nuka-overflow"
style={{ touchAction: swiping ? 'pan-x' : 'none' }}
style={{ touchAction: 'none' }}
>
<div
className="nuka-wrapper"
Expand Down
20 changes: 0 additions & 20 deletions packages/nuka/src/hooks/use-debounced.tsx

This file was deleted.

1 change: 1 addition & 0 deletions packages/nuka/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type CarouselProps = CarouselCallbacks & {
dots?: ReactNode;
id?: string;
keyboard?: boolean;
minSwipeDistance?: number;
scrollDistance?: ScrollDistanceType;
showArrows?: ShowArrowsOption;
showDots?: boolean;
Expand Down
9 changes: 1 addition & 8 deletions packages/nuka/src/utils/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { arraySeq, arraySum, nint } from '.';
import { arraySeq, arraySum } from '.';

describe('utils', () => {
describe('arraySeq', () => {
Expand All @@ -14,11 +14,4 @@ describe('utils', () => {
expect(result).toEqual([0, 1, 3, 6, 10]);
});
});

describe('nint', () => {
it('should return the closest number in an array to a target', () => {
const result = nint([0, 1, 2, 3, 4], 2.6);
expect(result).toEqual(3);
});
});
});
10 changes: 0 additions & 10 deletions packages/nuka/src/utils/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,3 @@ export function arraySum(values: number[]): number[] {
let sum = 0;
return values.map((value) => (sum += value));
}

/**
* Finds the nearest number in an array to a target number
* @returns A number
*/
export function nint(array: number[], target: number): number {
return array.reduce((prev, curr) =>
Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev,
);
}
3 changes: 2 additions & 1 deletion packages/nuka/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './array';
export * from './css';
export * from './browser';
export * from './css';
export * from './mouse';
5 changes: 5 additions & 0 deletions packages/nuka/src/utils/mouse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function isMouseEvent(
e: React.MouseEvent | React.TouchEvent,
): e is React.MouseEvent {
return 'clientX' && 'clientY' in e;
}
5 changes: 4 additions & 1 deletion website/src/components/landing/landing-features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ export const LandingFeatures = ({
<h2 className="my-8 text-4xl font-semibold">{heading}</h2>
<ul className="grid grid-cols-3 items-start content-start justify-items-start justify-between gap-12 list-none pl-0">
{list.map(({ alt, body, imgSrc, title }) => (
<li className="col-span-3 md:col-span-1 flex flex-col items-center text-center">
<li
className="col-span-3 md:col-span-1 flex flex-col items-center text-center"
key={title}
>
<img src={imgSrc} alt={alt} className="max-h-72" />
<span className="mt-8 text-2xl font-semibold">{title}</span>
<span className="mt-2 text-lg leading-8 mx-3">{body}</span>
Expand Down