expo-popcore-app/hooks/use-swipe-navigation.ts

119 lines
3.7 KiB
TypeScript

/**
* @file use-swipe-navigation.ts
* @description Hook for handling swipe gestures to navigate between tabs
*
* This hook provides gesture handlers for PanGestureHandler that detect
* horizontal swipes and trigger navigation callbacks.
*/
import { useCallback, useRef } from 'react'
import { Dimensions } from 'react-native'
import type { PanGestureHandlerGestureEvent, PanGestureHandlerStateChangeEvent } from 'react-native-gesture-handler'
const SCREEN_WIDTH = Dimensions.get('window').width
const DEFAULT_THRESHOLD = SCREEN_WIDTH * 0.2 // 20% of screen width
const DEFAULT_VELOCITY_THRESHOLD = 300 // pixels per second
// Gesture handler state constants (to avoid import issues in tests)
const GESTURE_STATE_END = 5
export interface UseSwipeNavigationOptions {
/** Callback when user swipes left (to go to next tab) */
onSwipeLeft: () => void
/** Callback when user swipes right (to go to previous tab) */
onSwipeRight: () => void
/** Minimum distance to trigger swipe (default: 20% of screen width) */
threshold?: number
/** Minimum velocity to trigger swipe regardless of distance (default: 300) */
velocityThreshold?: number
/** Whether swipe navigation is enabled (default: true) */
enabled?: boolean
/** Whether user can swipe left (default: true) */
canSwipeLeft?: boolean
/** Whether user can swipe right (default: true) */
canSwipeRight?: boolean
}
export interface UseSwipeNavigationReturn {
/** Handler for gesture events (for tracking) */
handleGestureEvent: (event: PanGestureHandlerGestureEvent) => void
/** Handler for gesture state changes (for triggering navigation) */
handleGestureStateChange: (event: PanGestureHandlerStateChangeEvent) => void
}
/**
* Hook for handling swipe gestures to navigate between tabs
*
* @example
* ```tsx
* const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({
* onSwipeLeft: () => setActiveTab(prev => prev + 1),
* onSwipeRight: () => setActiveTab(prev => prev - 1),
* canSwipeLeft: activeTab < tabs.length - 1,
* canSwipeRight: activeTab > 0,
* })
*
* return (
* <PanGestureHandler
* onGestureEvent={handleGestureEvent}
* onHandlerStateChange={handleGestureStateChange}
* >
* <View>{children}</View>
* </PanGestureHandler>
* )
* ```
*/
export function useSwipeNavigation({
onSwipeLeft,
onSwipeRight,
threshold = DEFAULT_THRESHOLD,
velocityThreshold = DEFAULT_VELOCITY_THRESHOLD,
enabled = true,
canSwipeLeft = true,
canSwipeRight = true,
}: UseSwipeNavigationOptions): UseSwipeNavigationReturn {
// Track translation for potential animation use
const translationX = useRef(0)
const handleGestureEvent = useCallback(
(event: PanGestureHandlerGestureEvent) => {
if (!enabled) return
translationX.current = event.nativeEvent.translationX
},
[enabled]
)
const handleGestureStateChange = useCallback(
(event: PanGestureHandlerStateChangeEvent) => {
if (!enabled) return
const { state, translationX: tx, velocityX } = event.nativeEvent
// Only process when gesture ends
if (state !== GESTURE_STATE_END) return
const isSwipeLeft = tx < 0
const isSwipeRight = tx > 0
const absTranslation = Math.abs(tx)
const absVelocity = Math.abs(velocityX)
// Check if swipe meets threshold (distance OR velocity)
const meetsThreshold = absTranslation >= threshold || absVelocity >= velocityThreshold
if (!meetsThreshold) return
if (isSwipeLeft && canSwipeLeft) {
onSwipeLeft()
} else if (isSwipeRight && canSwipeRight) {
onSwipeRight()
}
},
[enabled, threshold, velocityThreshold, canSwipeLeft, canSwipeRight, onSwipeLeft, onSwipeRight]
)
return {
handleGestureEvent,
handleGestureStateChange,
}
}