import type { ComponentProps, ReactNode } from 'react'; import React, { useCallback, useEffect, useRef } from 'react'; import { Animated, useWindowDimensions } from 'react-native'; import { GestureHandlerRootView, RectButton, Swipeable as RnghSwipeable, } from 'react-native-gesture-handler'; import SwipeableAction from './SwipeableAction'; import { useTheme } from '../../theme'; import Box from '../Box'; type State = 'closed' | 'leftOpen' | 'rightOpen'; // We are supporting both v1 and v2 of RNGH at the same time. // SwipeableProps is only exported in v2, so we have to use ComponentProps. type RnghSwipeableProps = ComponentProps; export interface SwipeableProps extends Pick< RnghSwipeableProps, | 'enableTrackpadTwoFingerGesture' | 'friction' | 'leftThreshold' | 'rightThreshold' | 'overshootLeft' | 'overshootRight' | 'overshootFriction' | 'useNativeAnimations' | 'containerStyle' | 'childrenContainerStyle' > { /** * React node that is swipeable. */ children: ReactNode; /** * State of the component. */ state?: 'closed' | 'leftOpen' | 'rightOpen'; /** * Callback when the state of the component changes. */ onStateChange?: (state: State) => void; /** * Action panel that is going to be revealed from the left side when user swipes right. */ leftActions?: ReactNode; /** * Width of the left action panel. * By default, it will take up the whole width of the device. */ leftActionsWidth?: number; /** * Action panel that is going to be revealed from the right side when user swipes left. */ rightActions?: ReactNode; /** * Width of the right action panel. * By default, it will take up the whole width of the device. */ rightActionsWidth?: number; /** * Testing ID of the component */ testID?: string; /** * Variant of the component. */ variant?: 'card' | 'full-width'; } const renderActions = ( actions: ReactNode, width: number, progress: Animated.AnimatedInterpolation, direction: 'left' | 'right' ) => { const trans = progress.interpolate({ inputRange: [0, 1], outputRange: direction === 'left' ? [-width, 0] : [width, 0], extrapolate: 'clamp', }); return ( {actions} ); }; const Swipeable = ({ children, state, onStateChange, leftActions, leftActionsWidth, rightActions, rightActionsWidth, variant = 'card', ...swipeableProps }: SwipeableProps) => { const theme = useTheme(); const { width } = useWindowDimensions(); const swipeableRef = useRef(null); const [containerWidth, setContainerWidth] = React.useState(0); const renderLeftActions = useCallback( (progress: Animated.AnimatedInterpolation) => renderActions(leftActions, leftActionsWidth || width, progress, 'left'), [leftActions, leftActionsWidth, width] ); const renderRightActions = useCallback( (progress: Animated.AnimatedInterpolation) => renderActions( rightActions, rightActionsWidth || width, progress, 'right' ), [rightActions, rightActionsWidth, width] ); useEffect(() => { if (swipeableRef.current === null) return; switch (state) { case 'leftOpen': swipeableRef.current.openLeft(); break; case 'rightOpen': swipeableRef.current.openRight(); break; case 'closed': swipeableRef.current.close(); break; } }, [state]); return ( setContainerWidth(e.nativeEvent.layout.width)}> onStateChange?.('leftOpen')} onSwipeableRightOpen={() => onStateChange?.('rightOpen')} onSwipeableClose={() => onStateChange?.('closed')} containerStyle={{ borderRadius: variant === 'card' ? theme.__hd__.swipeable.radii.swipeableContainer : 0, }} childrenContainerStyle={{ backgroundColor: theme.__hd__.swipeable.colors.defaultContainerBackground, position: 'relative', width: containerWidth + theme.__hd__.swipeable.space.containerExtraWidth, borderRadius: theme.__hd__.swipeable.radii.swipeableContainer, }} > {children} ); }; export default Object.assign(Swipeable, { Action: SwipeableAction, Content: RectButton, });