2022-03-22 20:44:27 +00:00
import { useBackHandler } from '@react-native-community/hooks' ;
import * as Haptics from 'expo-haptics' ;
2021-09-13 20:41:05 +00:00
import React , { forwardRef , isValidElement , useEffect , useImperativeHandle , useRef , useState } from 'react' ;
import { Keyboard , Text } from 'react-native' ;
2022-03-22 20:44:27 +00:00
import { HandlerStateChangeEventPayload , State , TapGestureHandler } from 'react-native-gesture-handler' ;
import Animated , { Easing , Extrapolate , interpolateNode , Value } from 'react-native-reanimated' ;
2021-09-13 20:41:05 +00:00
import { useSafeAreaInsets } from 'react-native-safe-area-context' ;
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet' ;
import { themes } from '../../constants/colors' ;
2022-03-22 20:44:27 +00:00
import { useDimensions , useOrientation } from '../../dimensions' ;
import I18n from '../../i18n' ;
import { useTheme } from '../../theme' ;
2021-09-13 20:41:05 +00:00
import { isIOS , isTablet } from '../../utils/deviceInfo' ;
import * as List from '../List' ;
2022-03-22 20:44:27 +00:00
import { Button } from './Button' ;
import { Handle } from './Handle' ;
import { IActionSheetItem , Item } from './Item' ;
import { TActionSheetOptions , TActionSheetOptionsItem } from './Provider' ;
import styles , { ITEM_HEIGHT } from './styles' ;
2021-09-13 20:41:05 +00:00
2022-03-22 20:44:27 +00:00
const getItemLayout = ( data : TActionSheetOptionsItem [ ] | null | undefined , index : number ) = > ( {
length : ITEM_HEIGHT ,
offset : ITEM_HEIGHT * index ,
index
} ) ;
2021-09-13 20:41:05 +00:00
const HANDLE_HEIGHT = isIOS ? 40 : 56 ;
const MAX_SNAP_HEIGHT = 16 ;
const CANCEL_HEIGHT = 64 ;
const ANIMATION_DURATION = 250 ;
const ANIMATION_CONFIG = {
duration : ANIMATION_DURATION ,
// https://easings.net/#easeInOutCubic
easing : Easing.bezier ( 0.645 , 0.045 , 0.355 , 1.0 )
} ;
const ActionSheet = React . memo (
2022-03-22 20:44:27 +00:00
forwardRef ( ( { children } : { children : React.ReactElement } , ref ) = > {
const { theme } = useTheme ( ) ;
const bottomSheetRef = useRef < ScrollBottomSheet < TActionSheetOptionsItem > > ( null ) ;
const [ data , setData ] = useState < TActionSheetOptions > ( { } as TActionSheetOptions ) ;
2021-09-13 20:41:05 +00:00
const [ isVisible , setVisible ] = useState ( false ) ;
2022-03-22 20:44:27 +00:00
const { height } = useDimensions ( ) ;
2021-09-13 20:41:05 +00:00
const { isLandscape } = useOrientation ( ) ;
const insets = useSafeAreaInsets ( ) ;
const maxSnap = Math . max (
2022-03-22 20:44:27 +00:00
height -
2021-09-13 20:41:05 +00:00
// Items height
ITEM_HEIGHT * ( data ? . options ? . length || 0 ) -
// Handle height
HANDLE_HEIGHT -
// Custom header height
( data ? . headerHeight || 0 ) -
// Insets bottom height (Notch devices)
insets . bottom -
// Cancel button height
( data ? . hasCancel ? CANCEL_HEIGHT : 0 ) ,
MAX_SNAP_HEIGHT
) ;
/ *
* if the action sheet cover more
* than 60 % of the whole screen
* and it ' s not at the landscape mode
* we ' ll provide more one snap
* that point 50 % of the whole screen
* /
2022-03-22 20:44:27 +00:00
const snaps = height - maxSnap > height * 0.6 && ! isLandscape ? [ maxSnap , height * 0.5 , height ] : [ maxSnap , height ] ;
2021-09-13 20:41:05 +00:00
const openedSnapIndex = snaps . length > 2 ? 1 : 0 ;
const closedSnapIndex = snaps . length - 1 ;
const toggleVisible = ( ) = > setVisible ( ! isVisible ) ;
const hide = ( ) = > {
bottomSheetRef . current ? . snapTo ( closedSnapIndex ) ;
} ;
2022-03-22 20:44:27 +00:00
const show = ( options : TActionSheetOptions ) = > {
2021-09-13 20:41:05 +00:00
setData ( options ) ;
toggleVisible ( ) ;
} ;
2022-03-22 20:44:27 +00:00
const onBackdropPressed = ( { nativeEvent } : { nativeEvent : HandlerStateChangeEventPayload } ) = > {
2021-09-13 20:41:05 +00:00
if ( nativeEvent . oldState === State . ACTIVE ) {
hide ( ) ;
}
} ;
useBackHandler ( ( ) = > {
if ( isVisible ) {
hide ( ) ;
}
return isVisible ;
} ) ;
useEffect ( ( ) = > {
if ( isVisible ) {
Keyboard . dismiss ( ) ;
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light ) ;
bottomSheetRef . current ? . snapTo ( openedSnapIndex ) ;
}
} , [ isVisible ] ) ;
// Hides action sheet when orientation changes
useEffect ( ( ) = > {
setVisible ( false ) ;
} , [ isLandscape ] ) ;
useImperativeHandle ( ref , ( ) = > ( {
showActionSheet : show ,
hideActionSheet : hide
} ) ) ;
const renderHandle = ( ) = > (
< >
2022-03-22 20:44:27 +00:00
< Handle / >
2021-09-13 20:41:05 +00:00
{ isValidElement ( data ? . customHeader ) ? data.customHeader : null }
< / >
) ;
const renderFooter = ( ) = >
data ? . hasCancel ? (
2021-12-02 13:19:15 +00:00
< Button
onPress = { hide }
style = { [ styles . button , { backgroundColor : themes [ theme ] . auxiliaryBackground } ] }
2022-03-22 20:44:27 +00:00
// TODO: Remove when migrate Touch
2021-12-02 13:19:15 +00:00
theme = { theme }
accessibilityLabel = { I18n . t ( 'Cancel' ) } >
2021-09-13 20:41:05 +00:00
< Text style = { [ styles . text , { color : themes [ theme ] . bodyText } ] } > { I18n . t ( 'Cancel' ) } < / Text >
< / Button >
) : null ;
2022-03-22 20:44:27 +00:00
const renderItem = ( { item } : { item : IActionSheetItem [ 'item' ] } ) = > < Item item = { item } hide = { hide } / > ;
2021-09-13 20:41:05 +00:00
const animatedPosition = React . useRef ( new Value ( 0 ) ) ;
2021-10-01 20:34:25 +00:00
const opacity = interpolateNode ( animatedPosition . current , {
2021-09-13 20:41:05 +00:00
inputRange : [ 0 , 1 ] ,
outputRange : [ 0 , themes [ theme ] . backdropOpacity ] ,
extrapolate : Extrapolate.CLAMP
2022-03-22 20:44:27 +00:00
} ) as any ; // The function's return differs from the expected type of opacity, however this problem is something related to lib, maybe when updating the types will be fixed.
const bottomSheet = isLandscape || isTablet ? styles . bottomSheet : { } ;
2021-09-13 20:41:05 +00:00
return (
< >
{ children }
{ isVisible && (
< >
< TapGestureHandler onHandlerStateChange = { onBackdropPressed } >
< Animated.View
testID = 'action-sheet-backdrop'
style = { [
styles . backdrop ,
{
backgroundColor : themes [ theme ] . backdropColor ,
opacity
}
] }
/ >
< / TapGestureHandler >
2022-03-22 20:44:27 +00:00
< ScrollBottomSheet < TActionSheetOptionsItem >
2021-09-13 20:41:05 +00:00
testID = 'action-sheet'
ref = { bottomSheetRef }
componentType = 'FlatList'
snapPoints = { snaps }
initialSnapIndex = { closedSnapIndex }
renderHandle = { renderHandle }
onSettle = { index = > index === closedSnapIndex && toggleVisible ( ) }
animatedPosition = { animatedPosition . current }
2022-03-22 20:44:27 +00:00
containerStyle = { { . . . styles . container , . . . bottomSheet , backgroundColor : themes [ theme ] . focusedBackground } }
2021-09-13 20:41:05 +00:00
animationConfig = { ANIMATION_CONFIG }
2022-03-22 20:44:27 +00:00
data = { data . options }
2021-09-13 20:41:05 +00:00
renderItem = { renderItem }
2022-03-22 20:44:27 +00:00
keyExtractor = { item = > item . title }
2021-09-13 20:41:05 +00:00
style = { { backgroundColor : themes [ theme ] . focusedBackground } }
contentContainerStyle = { styles . content }
ItemSeparatorComponent = { List . Separator }
ListHeaderComponent = { List . Separator }
ListFooterComponent = { renderFooter }
getItemLayout = { getItemLayout }
removeClippedSubviews = { isIOS }
/ >
< / >
) }
< / >
) ;
} )
) ;
export default ActionSheet ;