119 lines
3.7 KiB
TypeScript
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,
|
|
}
|
|
}
|