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

185 lines
5.6 KiB
TypeScript
Raw Normal View History

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 { useSafeAreaInsets } from 'react-native-safe-area-context';
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, { ITEM_HEIGHT } from './styles';
export const ACTION_SHEET_ANIMATION_DURATION = 250;
const HANDLE_HEIGHT = 28;
const CANCEL_HEIGHT = 64;
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 { height: windowHeight } = useWindowDimensions();
const { bottom } = useSafeAreaInsets();
const bottomSheetRef = useRef<BottomSheet>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false);
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 }
}
}) => {
/**
* This logic is only necessary to prevent the action sheet from
* occupying the entire screen when the dynamic content is too big.
*/
animatedContentHeight.value = Math.min(height, windowHeight * 0.8);
},
[animatedContentHeight, windowHeight]
);
const maxSnap = Math.min(
(ITEM_HEIGHT + 0.5) * (data?.options?.length || 0) +
HANDLE_HEIGHT +
// Custom header height
(data?.headerHeight || 0) +
// Insets bottom height (Notch devices)
bottom +
// Cancel button height
(data?.hasCancel ? CANCEL_HEIGHT : 0),
windowHeight * 0.8
);
/*
* if the action sheet cover more than 60% of the screen height,
* we'll provide more one snap of 50%
*/
const snaps = maxSnap > windowHeight * 0.6 && !data.snaps ? ['50%', maxSnap] : [maxSnap];
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]);
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 = 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 && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {};
return (
<>
{children}
{isVisible && (
<BottomSheet
ref={bottomSheetRef}
// If data.options exist, we calculate snaps to be precise, otherwise we cal
snapPoints={data.options?.length ? snaps : animatedSnapPoints}
handleHeight={animatedHandleHeight}
// We need undefined to enable vertical swipe gesture inside the bottom sheet like in reaction picker
contentHeight={data.snaps?.length || data.options?.length ? undefined : animatedContentHeight}
animationConfigs={ANIMATION_CONFIG}
animateOnMount={true}
backdropComponent={renderBackdrop}
handleComponent={renderHandle}
enablePanDownToClose
style={{ ...styles.container, ...bottomSheet }}
feat: mobile color normalization (#5616) * chore: remove auxiliaryText color * chore: remove titleText * chore: password colors change * chore: use fontDefault on ActionSheet item * wip: type * chore: set custom icon default color * remove tintActive color * only set color when checked * remove icon color * remove tintActive * chore: remove STATUS_COLORS * chore: remove mentions colors * chore: remove switch color * chore: background color * chore: auxiliaryBackground * chore: one local colors * wip: some colors * wip: colors * wip: colors * wip: end colors * test: update * chore: fix some colors * chore: fix lint * chore: back to text props * chore: fix ts errors * revert * chore: fix lint * test: update snapshot * update storybook * cocoapods * fix app theme color * remove unused color * fix login service button color * remove unused color * unused backgroundColor * fix background color * fix transparent color * fix background color * wip: key * fix color * chore: revert to 1 tick * test: update * chore: use same color as front end * test: update * fix radius * fix background color * fix wrong color * change some colors * chore: update stories * chore: fix button style * chore: fix item color * lint * update snapshot * link * remove background color * change send to channel color * call icons * video conf colors * fix app default color * bottom sheet * remove background * two factor color * update tests * feat: add force-logout stream listener * remove icon colors * improve badge color * update tests * fix header colors * fix collapsible icon color * imagePreview color * wip * update test * lint --------- Co-authored-by: Diego Mello <diegolmello@gmail.com>
2024-04-18 10:19:54 +00:00
backgroundStyle={{ backgroundColor: colors.surfaceLight }}
onChange={index => index === -1 && onClose()}
// We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker
2022-08-03 16:32:16 +00:00
enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet}
>
<BottomSheetContent
options={data?.options}
hide={hide}
children={data?.children}
hasCancel={data?.hasCancel}
onLayout={handleContentLayout}
/>
</BottomSheet>
)}
</>
);
})
);
export default ActionSheet;