Skip to content

Commit

Permalink
Improve the stability of swiping (#1077)
Browse files Browse the repository at this point in the history
  • Loading branch information
carbonrobot authored Oct 8, 2024
2 parents afb70e1 + 3d98e7e commit e960459
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 55 deletions.
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` | 50 |
| `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: 'pan-y' }}
>
<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

0 comments on commit e960459

Please sign in to comment.