-
-
Save noahsark769/cab946c5bd07b75f4adf389c775fec95 to your computer and use it in GitHub Desktop.
A custom React Typescript hook for advanced touch gestures in UI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useRef } from "react"; | |
type GestureType = | |
| "swipeUp" | |
| "swipeDown" | |
| "swipeLeft" | |
| "swipeRight" | |
| "tap" | |
| "pinch" | |
| "zoom"; | |
interface GestureConfig { | |
gesture: GestureType; | |
touchCount: number; | |
callback: () => void; | |
elementRef?: React.RefObject<HTMLElement>; | |
} | |
const useGesture = (config: GestureConfig) => { | |
// Use a function to lazily get the target element in a client-side environment | |
const getTargetElement = () => { | |
if (typeof window !== "undefined") { | |
return config.elementRef?.current || document; | |
} | |
// Return a dummy object for SSR | |
return { | |
addEventListener: () => {}, | |
removeEventListener: () => {}, | |
}; | |
}; | |
const targetElement = getTargetElement(); | |
const gestureStateRef = useRef({ | |
touchStartX: 0, | |
touchStartY: 0, | |
touchEndX: 0, | |
touchEndY: 0, | |
touchTime: 0, | |
initialDistance: 0, | |
finalDistance: 0, | |
gestureTriggered: false, | |
}); | |
useEffect(() => { | |
const onTouchStart = (e: Event) => { | |
const touchEvent = e as TouchEvent; | |
if (touchEvent.touches.length === config.touchCount) { | |
e.preventDefault(); | |
const touch = touchEvent.touches[0]; | |
if (!touch) return; | |
gestureStateRef.current = { | |
...gestureStateRef.current, | |
touchStartX: touch.clientX, | |
touchStartY: touch.clientY, | |
touchEndX: touch.clientX, | |
touchEndY: touch.clientY, | |
touchTime: Date.now(), | |
gestureTriggered: false, | |
}; | |
if (config.gesture === "pinch" || config.gesture === "zoom") { | |
const touch2 = touchEvent.touches[1]; | |
if (!touch2) return; | |
gestureStateRef.current.initialDistance = Math.hypot( | |
touch2.clientX - touch.clientX, | |
touch2.clientY - touch.clientY, | |
); | |
} | |
} | |
}; | |
const onTouchMove = (e: Event) => { | |
const touchEvent = e as TouchEvent; | |
if ( | |
touchEvent.touches.length === config.touchCount && | |
!gestureStateRef.current.gestureTriggered | |
) { | |
e.preventDefault(); | |
const touch = touchEvent.touches[0]; | |
if (!touch) return; | |
gestureStateRef.current.touchEndX = touch.clientX; | |
gestureStateRef.current.touchEndY = touch.clientY; | |
if (config.gesture === "pinch" || config.gesture === "zoom") { | |
const touch2 = touchEvent.touches[1]; | |
if (!touch2) return; | |
gestureStateRef.current.finalDistance = Math.hypot( | |
touch2.clientX - touch.clientX, | |
touch2.clientY - touch.clientY, | |
); | |
} | |
} | |
}; | |
const triggerGesture = () => { | |
config.callback(); | |
}; | |
const onTouchEnd = () => { | |
handleGesture(); | |
}; | |
const handleGesture = () => { | |
if (gestureStateRef.current.gestureTriggered) return; | |
const { | |
touchStartX, | |
touchStartY, | |
touchEndX, | |
touchEndY, | |
touchTime, | |
initialDistance, | |
finalDistance, | |
} = gestureStateRef.current; | |
const dx = touchEndX - touchStartX; | |
const dy = touchEndY - touchStartY; | |
const timeDiff = Date.now() - touchTime; | |
const distance = Math.hypot(dx, dy); | |
switch (config.gesture) { | |
case "swipeUp": | |
if (dy < -50 && Math.abs(dx) < 50) triggerGesture(); | |
break; | |
case "swipeDown": | |
if (dy > 50 && Math.abs(dx) < 50) triggerGesture(); | |
break; | |
case "swipeLeft": | |
if (dx < -50 && Math.abs(dy) < 50) triggerGesture(); | |
break; | |
case "swipeRight": | |
if (dx > 50 && Math.abs(dy) < 50) triggerGesture(); | |
break; | |
case "tap": | |
if (distance < 30 && timeDiff < 200) triggerGesture(); | |
break; | |
case "pinch": | |
if (finalDistance < initialDistance) triggerGesture(); | |
break; | |
case "zoom": | |
if (finalDistance > initialDistance) triggerGesture(); | |
break; | |
} | |
gestureStateRef.current.gestureTriggered = true; | |
}; | |
// Only attach event listeners in the browser environment | |
if (typeof window !== "undefined" || typeof document !== "undefined") { | |
targetElement.addEventListener("touchstart", onTouchStart, { | |
passive: false, | |
}); | |
targetElement.addEventListener("touchmove", onTouchMove, { | |
passive: false, | |
}); | |
targetElement.addEventListener("touchend", onTouchEnd, { | |
passive: false, | |
}); | |
return () => { | |
targetElement.removeEventListener("touchstart", onTouchStart); | |
targetElement.removeEventListener("touchmove", onTouchMove); | |
targetElement.removeEventListener("touchend", onTouchEnd); | |
}; | |
} | |
}, [config, targetElement]); | |
return gestureStateRef.current; | |
}; | |
export default useGesture; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Added code to reset touchEnd state on touchStart, which helps when the container being swiped on scrolls as a result of the swipe