Skip to content

Commit 3ddebb2

Browse files
committed
main 🧊 add use swipe
1 parent 25ddc84 commit 3ddebb2

6 files changed

Lines changed: 948 additions & 0 deletions

File tree

‎packages/core/src/bundle/hooks/sensors.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from './useScroll/useScroll';
2727
export * from './useScrollIntoView/useScrollIntoView';
2828
export * from './useScrollTo/useScrollTo';
2929
export * from './useSize/useSize';
30+
export * from './useSwipe/useSwipe';
3031
export * from './useTextSelection/useTextSelection';
3132
export * from './useVisibility/useVisibility';
3233
export * from './useWindowEvent/useWindowEvent';
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { isTarget } from '@/utils/helpers';
3+
import { useRefState } from '../useRefState/useRefState';
4+
import { useRerender } from '../useRerender/useRerender';
5+
const DEFAULT_SWIPE_THRESHOLD = 50;
6+
const getCoords = (event) => {
7+
if ('touches' in event) {
8+
const touch = event.touches[0] ?? event.changedTouches[0];
9+
if (!touch) return;
10+
return {
11+
x: touch.clientX,
12+
y: touch.clientY
13+
};
14+
}
15+
return {
16+
x: event.clientX,
17+
y: event.clientY
18+
};
19+
};
20+
/**
21+
* @name useSwipe
22+
* @description - Hook that tracks swipe gestures for touch and pointer events
23+
* @category Sensors
24+
* @usage low
25+
*
26+
* @overload
27+
* @param {HookTarget} target The target element to track swipe on
28+
* @param {UseSwipeCallback} [callback] Swipe move callback
29+
* @returns {UseSwipeReturn} Swipe state
30+
*
31+
* @example
32+
* const swipe = useSwipe(ref, (value) => console.log(value.direction));
33+
*
34+
* @overload
35+
* @template Target The target element
36+
* @param {UseSwipeCallback} [callback] Swipe move callback
37+
* @returns {UseSwipeReturn & { ref: StateRef<Target> }} Swipe state and ref
38+
*
39+
* @example
40+
* const swipe = useSwipe<HTMLDivElement>((value) => console.log(value.direction));
41+
*
42+
* @overload
43+
* @param {HookTarget} target The target element to track swipe on
44+
* @param {UseSwipeOptions} [options] Swipe options
45+
* @returns {UseSwipeReturn} Swipe state
46+
*
47+
* @example
48+
* const swipe = useSwipe(ref);
49+
*
50+
* @overload
51+
* @template Target The target element
52+
* @param {UseSwipeOptions} [options] Swipe options
53+
* @returns {UseSwipeReturn & { ref: StateRef<Target> }} Swipe state and ref
54+
*
55+
* @example
56+
* const swipe = useSwipe<HTMLDivElement>();
57+
*/
58+
export const useSwipe = (...params) => {
59+
const target = isTarget(params[0]) ? params[0] : undefined;
60+
const options = target
61+
? typeof params[1] === 'function'
62+
? { ...params[2], onMove: params[1] }
63+
: params[1]
64+
: typeof params[0] === 'function'
65+
? { ...params[1], onMove: params[0] }
66+
: params[0];
67+
const [swiping, setSwiping] = useState(false);
68+
const internalRef = useRefState();
69+
const snapshotRef = useRef({
70+
direction: 'none',
71+
lengthX: 0,
72+
lengthY: 0
73+
});
74+
const swipingRef = useRef(false);
75+
const watchingRef = useRef(false);
76+
const rerender = useRerender();
77+
const optionsRef = useRef(options);
78+
optionsRef.current = options;
79+
const watch = () => {
80+
watchingRef.current = true;
81+
return snapshotRef.current;
82+
};
83+
useEffect(() => {
84+
if (!target && !internalRef.state) return;
85+
const element = target ? isTarget.getElement(target) : internalRef.current;
86+
if (!element) return;
87+
let start;
88+
const getCurrentDirection = (x, y) => {
89+
const absX = Math.abs(x);
90+
const absY = Math.abs(y);
91+
const maxLength = Math.max(absX, absY);
92+
const threshold = optionsRef.current?.threshold ?? DEFAULT_SWIPE_THRESHOLD;
93+
if (maxLength < threshold) return 'none';
94+
if (absX > absY) return x > 0 ? 'left' : 'right';
95+
return y > 0 ? 'up' : 'down';
96+
};
97+
const onStart = (event) => {
98+
if (swipingRef.current) return;
99+
const coords = getCoords(event);
100+
if (!coords) return;
101+
start = coords;
102+
const nextValue = {
103+
direction: 'none',
104+
lengthX: 0,
105+
lengthY: 0
106+
};
107+
swipingRef.current = true;
108+
setSwiping(true);
109+
snapshotRef.current = nextValue;
110+
optionsRef.current?.onStart?.(nextValue, event);
111+
if (watchingRef.current) rerender();
112+
};
113+
const onMove = (event) => {
114+
if (!swipingRef.current || !start) return;
115+
const coords = getCoords(event);
116+
if (!coords) return;
117+
const nextLengthX = start.x - coords.x;
118+
const nextLengthY = start.y - coords.y;
119+
snapshotRef.current = {
120+
direction: getCurrentDirection(nextLengthX, nextLengthY),
121+
lengthX: nextLengthX,
122+
lengthY: nextLengthY
123+
};
124+
optionsRef.current?.onMove?.(snapshotRef.current, event);
125+
if (watchingRef.current) rerender();
126+
};
127+
const onEnd = (event) => {
128+
if (!swipingRef.current || !start) return;
129+
const coords = getCoords(event);
130+
const x = coords ? start.x - coords.x : snapshotRef.current.lengthX;
131+
const y = coords ? start.y - coords.y : snapshotRef.current.lengthY;
132+
const nextValue = {
133+
direction: getCurrentDirection(x, y),
134+
lengthX: x,
135+
lengthY: y
136+
};
137+
swipingRef.current = false;
138+
setSwiping(false);
139+
snapshotRef.current = nextValue;
140+
optionsRef.current?.onEnd?.(nextValue, event);
141+
if (watchingRef.current) rerender();
142+
start = undefined;
143+
};
144+
const onPointerStart = (event) => {
145+
if (!event.isPrimary) return;
146+
onStart(event);
147+
};
148+
const onPointerMove = (event) => onMove(event);
149+
const onPointerEnd = (event) => onEnd(event);
150+
const onTouchStart = (event) => onStart(event);
151+
const onTouchMove = (event) => onMove(event);
152+
const onTouchEnd = (event) => onEnd(event);
153+
element.addEventListener('pointerdown', onPointerStart);
154+
window.addEventListener('pointermove', onPointerMove);
155+
window.addEventListener('pointerup', onPointerEnd);
156+
window.addEventListener('pointercancel', onPointerEnd);
157+
element.addEventListener('touchstart', onTouchStart);
158+
window.addEventListener('touchmove', onTouchMove);
159+
window.addEventListener('touchend', onTouchEnd);
160+
window.addEventListener('touchcancel', onTouchEnd);
161+
return () => {
162+
element.removeEventListener('pointerdown', onPointerStart);
163+
window.removeEventListener('pointermove', onPointerMove);
164+
window.removeEventListener('pointerup', onPointerEnd);
165+
window.removeEventListener('pointercancel', onPointerEnd);
166+
element.removeEventListener('touchstart', onTouchStart);
167+
window.removeEventListener('touchmove', onTouchMove);
168+
window.removeEventListener('touchend', onTouchEnd);
169+
window.removeEventListener('touchcancel', onTouchEnd);
170+
};
171+
}, [target && isTarget.getRawElement(target), internalRef.state]);
172+
if (target) return { swiping, snapshot: snapshotRef.current, watch };
173+
return { ref: internalRef, swiping, snapshot: snapshotRef.current, watch };
174+
};

‎packages/core/src/hooks/sensors.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from './useScroll/useScroll';
2727
export * from './useScrollIntoView/useScrollIntoView';
2828
export * from './useScrollTo/useScrollTo';
2929
export * from './useSize/useSize';
30+
export * from './useSwipe/useSwipe';
3031
export * from './useTextSelection/useTextSelection';
3132
export * from './useVisibility/useVisibility';
3233
export * from './useWindowEvent/useWindowEvent';
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useSwipe } from '@siberiacancode/reactuse';
2+
import { useRef } from 'react';
3+
4+
const Demo = () => {
5+
const cardRef = useRef<HTMLDivElement>(null);
6+
7+
const setCardStyles = (x: number) => {
8+
if (!cardRef.current) return;
9+
cardRef.current.style.transform = `translateX(${x}px)`;
10+
};
11+
12+
const swipe = useSwipe<HTMLDivElement>({
13+
onStart: () => {
14+
document.body.style.userSelect = 'none';
15+
},
16+
onMove: (value) => {
17+
if (!cardRef.current) return;
18+
19+
if (value.lengthX >= 0) {
20+
setCardStyles(0);
21+
return;
22+
}
23+
24+
const distance = Math.abs(value.lengthX);
25+
setCardStyles(distance);
26+
},
27+
onEnd: (value) => {
28+
if (!cardRef.current) return;
29+
30+
const width = cardRef.current.offsetWidth ?? 0;
31+
const swipeDistance = value.lengthX < 0 ? Math.abs(value.lengthX) : 0;
32+
const shouldDismiss = width > 0 && swipeDistance / width >= 0.5;
33+
34+
if (shouldDismiss) {
35+
setCardStyles(width);
36+
} else {
37+
setCardStyles(0);
38+
}
39+
}
40+
});
41+
42+
const cardClassName = [
43+
'absolute inset-0 flex select-none items-center justify-center rounded-xl bg-[var(--vp-c-brand-1)]',
44+
swipe.swiping ? '' : 'transition-all duration-200 ease-linear'
45+
]
46+
.filter(Boolean)
47+
.join(' ');
48+
49+
return (
50+
<div
51+
ref={swipe.ref}
52+
className='relative mx-auto flex h-[96px] w-full max-w-md items-center justify-center overflow-hidden rounded-xl border-2 border-dashed border-white select-none'
53+
>
54+
<button
55+
className='rounded-md bg-white px-3 py-1 text-sm font-medium'
56+
type='button'
57+
onClick={() => {
58+
if (!cardRef.current) return;
59+
setCardStyles(0);
60+
}}
61+
>
62+
Reset
63+
</button>
64+
65+
<div
66+
ref={cardRef}
67+
style={{
68+
transform: 'translateX(0)',
69+
opacity: 1,
70+
touchAction: 'pan-y'
71+
}}
72+
className={cardClassName}
73+
>
74+
<p className='shrink-0 whitespace-nowrap text-white'>Swipe left to right</p>
75+
</div>
76+
</div>
77+
);
78+
};
79+
80+
export default Demo;

0 commit comments

Comments
 (0)