Rocket.Chat.ReactNative/app/containers/ActionSheet/ActionSheet.tsx

164 lines
4.8 KiB
TypeScript

import { useBackHandler } from '@react-native-community/hooks';
import * as Haptics from 'expo-haptics';
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState, useCallback } from 'react';
import { Keyboard, useWindowDimensions } from 'react-native';
import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { useTheme } from '../../theme';
import { isIOS, isTablet } from '../../lib/methods/helpers';
import { Handle } from './Handle';
import { TActionSheetOptions } from './Provider';
import BottomSheetContent from './BottomSheetContent';
import styles from './styles';
export const ACTION_SHEET_ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = {
duration: ACTION_SHEET_ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};
const ActionSheet = React.memo(
forwardRef(({ children }: { children: React.ReactElement }, ref) => {
const { colors } = useTheme();
const bottomSheetRef = useRef<BottomSheet>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false);
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
const animatedContentHeight = useSharedValue(0);
const animatedHandleHeight = useSharedValue(0);
const animatedDataSnaps = useSharedValue<TActionSheetOptions['snaps']>([]);
const animatedSnapPoints = useDerivedValue(() => {
if (animatedDataSnaps.value?.length) {
return animatedDataSnaps.value;
}
const contentWithHandleHeight = animatedContentHeight.value + animatedHandleHeight.value;
// Bottom sheet requires a default value to work
if (contentWithHandleHeight === 0) {
return ['25%'];
}
return [contentWithHandleHeight];
}, [data]);
const handleContentLayout = useCallback(
({
nativeEvent: {
layout: { height }
}
}) => {
animatedContentHeight.value = height;
},
[animatedContentHeight]
);
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
bottomSheetRef.current?.close();
};
const show = (options: TActionSheetOptions) => {
setData(options);
if (options.snaps?.length) {
animatedDataSnaps.value = options.snaps;
}
toggleVisible();
};
useBackHandler(() => {
if (isVisible) {
hide();
}
return isVisible;
});
useEffect(() => {
if (isVisible) {
Keyboard.dismiss();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
}, [isVisible]);
// Hides action sheet when orientation changes
useEffect(() => {
setVisible(false);
}, [isLandscape]);
useImperativeHandle(ref, () => ({
showActionSheet: show,
hideActionSheet: hide
}));
const renderHandle = () => (
<>
<Handle />
{isValidElement(data?.customHeader) ? data.customHeader : null}
</>
);
const onClose = () => {
toggleVisible();
data?.onClose && data?.onClose();
animatedDataSnaps.value = [];
};
const renderBackdrop = useCallback(
props => (
<BottomSheetBackdrop
{...props}
appearsOnIndex={0}
// Backdrop should be visible all the time bottom sheet is open
disappearsOnIndex={-1}
opacity={colors.backdropOpacity}
/>
),
[]
);
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
// Must need this prop to avoid keyboard dismiss
// when is android tablet and the input text is focused
const androidTablet: any = isTablet && isLandscape && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {};
return (
<>
{children}
{isVisible && (
<BottomSheet
ref={bottomSheetRef}
snapPoints={animatedSnapPoints}
handleHeight={animatedHandleHeight}
// We need undefined to enable vertical swipe gesture inside the bottom sheet like in reaction picker
contentHeight={data.snaps?.length ? undefined : animatedContentHeight}
animationConfigs={ANIMATION_CONFIG}
animateOnMount={true}
backdropComponent={renderBackdrop}
handleComponent={renderHandle}
enablePanDownToClose
style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && onClose()}
// We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker
enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet}
>
<BottomSheetContent
options={data?.options}
hide={hide}
children={data?.children}
hasCancel={data?.hasCancel}
onLayout={handleContentLayout}
/>
</BottomSheet>
)}
</>
);
})
);
export default ActionSheet;