Created
February 7, 2024 10:27
-
-
Save KristofferEriksson/4a406501889d7fb768ed8de101f7be72 to your computer and use it in GitHub Desktop.
A custom React Typescript hook for advanced touch gestures in UI
This file contains hidden or 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, | |
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
much appreciated! your example really helped me to resolve a problem I'd been vexed by for half a day