|
| 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 | +}; |
0 commit comments