From ce0e5940d4d0cb33412cc3f728be864dec03a4c8 Mon Sep 17 00:00:00 2001 From: Charlie Brown Date: Mon, 7 Oct 2024 16:31:31 -0500 Subject: [PATCH 1/3] Improve the stability of swiping --- .changeset/wet-schools-remember.md | 5 ++ docs/api/swiping.mdx | 28 ++++++++- packages/nuka/src/Carousel/Carousel.tsx | 59 ++++++++++++++----- packages/nuka/src/hooks/use-debounced.tsx | 20 ------- packages/nuka/src/types.ts | 1 + packages/nuka/src/utils/index.ts | 3 +- packages/nuka/src/utils/mouse.ts | 5 ++ .../components/landing/landing-features.tsx | 5 +- 8 files changed, 89 insertions(+), 37 deletions(-) create mode 100644 .changeset/wet-schools-remember.md delete mode 100644 packages/nuka/src/hooks/use-debounced.tsx create mode 100644 packages/nuka/src/utils/mouse.ts diff --git a/.changeset/wet-schools-remember.md b/.changeset/wet-schools-remember.md new file mode 100644 index 00000000..75087d9b --- /dev/null +++ b/.changeset/wet-schools-remember.md @@ -0,0 +1,5 @@ +--- +'nuka-carousel': patch +--- + +Improve the stability of swiping diff --git a/docs/api/swiping.mdx b/docs/api/swiping.mdx index 1f4d20cd..a54571ab 100644 --- a/docs/api/swiping.mdx +++ b/docs/api/swiping.mdx @@ -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"`
@@ -39,6 +40,31 @@ By default the carousel will allow you to drag the carousel to the next slide. Y ``` +### Enabled with `scrollDistance="screen"` + + +
+
+
+
+
+
+
+
+
+
+ + +#### Code + +```tsx + + + + + +``` + ### Disabled diff --git a/packages/nuka/src/Carousel/Carousel.tsx b/packages/nuka/src/Carousel/Carousel.tsx index 1477a25b..f9de81cf 100644 --- a/packages/nuka/src/Carousel/Carousel.tsx +++ b/packages/nuka/src/Carousel/Carousel.tsx @@ -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'; @@ -22,6 +29,7 @@ const defaults = { dots: , id: 'nuka-carousel', keyboard: true, + minSwipeDistance: 50, scrollDistance: 'screen', showArrows: false, showDots: false, @@ -44,6 +52,7 @@ export const Carousel = forwardRef( dots, id, keyboard, + minSwipeDistance, scrollDistance, showArrows, showDots, @@ -69,16 +78,36 @@ export const Carousel = forwardRef( }); // -- handle touch scroll events - const onContainerScroll = useDebounced(() => { - if (!containerRef.current) return; + const [touchStart, setTouchStart] = useState(null); + const [touchEnd, setTouchEnd] = useState(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({ @@ -147,10 +176,12 @@ export const Carousel = forwardRef(
void; - -export function useDebounced(callback: CallbackFunction, delay: number) { - const timerRef = useRef(); - - return useCallback( - (...args: never[]) => { - if (timerRef.current) { - clearTimeout(timerRef.current); - } - - timerRef.current = setTimeout(() => { - callback(...args); - }, delay); - }, - [callback, delay], - ); -} diff --git a/packages/nuka/src/types.ts b/packages/nuka/src/types.ts index de94a65a..4bdc292d 100644 --- a/packages/nuka/src/types.ts +++ b/packages/nuka/src/types.ts @@ -18,6 +18,7 @@ export type CarouselProps = CarouselCallbacks & { dots?: ReactNode; id?: string; keyboard?: boolean; + minSwipeDistance?: number; scrollDistance?: ScrollDistanceType; showArrows?: ShowArrowsOption; showDots?: boolean; diff --git a/packages/nuka/src/utils/index.ts b/packages/nuka/src/utils/index.ts index 9202f5a3..78aa3dc1 100644 --- a/packages/nuka/src/utils/index.ts +++ b/packages/nuka/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './array'; -export * from './css'; export * from './browser'; +export * from './css'; +export * from './mouse'; diff --git a/packages/nuka/src/utils/mouse.ts b/packages/nuka/src/utils/mouse.ts new file mode 100644 index 00000000..62ce53e7 --- /dev/null +++ b/packages/nuka/src/utils/mouse.ts @@ -0,0 +1,5 @@ +export function isMouseEvent( + e: React.MouseEvent | React.TouchEvent, +): e is React.MouseEvent { + return 'clientX' && 'clientY' in e; +} diff --git a/website/src/components/landing/landing-features.tsx b/website/src/components/landing/landing-features.tsx index e7deceb9..7473f1ef 100644 --- a/website/src/components/landing/landing-features.tsx +++ b/website/src/components/landing/landing-features.tsx @@ -15,7 +15,10 @@ export const LandingFeatures = ({

{heading}

    {list.map(({ alt, body, imgSrc, title }) => ( -
  • +
  • {alt} {title} {body} From ea67b5d2864a393389147447621045f4f0f17d48 Mon Sep 17 00:00:00 2001 From: Charlie Brown Date: Mon, 7 Oct 2024 21:17:18 -0500 Subject: [PATCH 2/3] Remove unused function --- packages/nuka/src/utils/array.test.ts | 9 +-------- packages/nuka/src/utils/array.ts | 10 ---------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/nuka/src/utils/array.test.ts b/packages/nuka/src/utils/array.test.ts index 3c97b520..44fc6950 100644 --- a/packages/nuka/src/utils/array.test.ts +++ b/packages/nuka/src/utils/array.test.ts @@ -1,4 +1,4 @@ -import { arraySeq, arraySum, nint } from '.'; +import { arraySeq, arraySum } from '.'; describe('utils', () => { describe('arraySeq', () => { @@ -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); - }); - }); }); diff --git a/packages/nuka/src/utils/array.ts b/packages/nuka/src/utils/array.ts index e222457d..4ddcf1b8 100644 --- a/packages/nuka/src/utils/array.ts +++ b/packages/nuka/src/utils/array.ts @@ -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, - ); -} From 3d98e7ee36e593330b31b1404821d94d8ea7e438 Mon Sep 17 00:00:00 2001 From: Charlie Brown Date: Tue, 8 Oct 2024 12:42:52 -0500 Subject: [PATCH 3/3] Allow vertical scrolling only --- docs/api/swiping.mdx | 2 +- packages/nuka/src/Carousel/Carousel.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/swiping.mdx b/docs/api/swiping.mdx index a54571ab..e1451372 100644 --- a/docs/api/swiping.mdx +++ b/docs/api/swiping.mdx @@ -12,7 +12,7 @@ By default the carousel will allow you to drag the carousel to the next slide. Y | Prop Name | Type | Default Value | | :--------- | :-------- | :------------ | -| `minSwipeDistance` | `number` | 10 | +| `minSwipeDistance` | `number` | 50 | | `swiping` | `boolean` | `true` | ### Enabled with `scrollDistance="slide"` diff --git a/packages/nuka/src/Carousel/Carousel.tsx b/packages/nuka/src/Carousel/Carousel.tsx index f9de81cf..db0fbb24 100644 --- a/packages/nuka/src/Carousel/Carousel.tsx +++ b/packages/nuka/src/Carousel/Carousel.tsx @@ -181,7 +181,7 @@ export const Carousel = forwardRef( onTouchStart={onTouchStart} id="nuka-overflow" data-testid="nuka-overflow" - style={{ touchAction: 'none' }} + style={{ touchAction: 'pan-y' }} >