fix: dynamic action sheet taking whole screen (#5249)
* Fix dynamic action sheet taking whole screen * Bring back maxSnap logic based on options * Rollback enableContentPanningGesture * Fix e2e tests
This commit is contained in:
parent
37c58eb3ba
commit
a1172f8bf5
|
@ -4,15 +4,18 @@ import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useR
|
|||
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 from './styles';
|
||||
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,
|
||||
|
@ -23,11 +26,11 @@ const ANIMATION_CONFIG = {
|
|||
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 { width, height } = useWindowDimensions();
|
||||
const isLandscape = width > height;
|
||||
const animatedContentHeight = useSharedValue(0);
|
||||
const animatedHandleHeight = useSharedValue(0);
|
||||
const animatedDataSnaps = useSharedValue<TActionSheetOptions['snaps']>([]);
|
||||
|
@ -49,11 +52,33 @@ const ActionSheet = React.memo(
|
|||
layout: { height }
|
||||
}
|
||||
}) => {
|
||||
animatedContentHeight.value = 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]
|
||||
[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 = () => {
|
||||
|
@ -82,11 +107,6 @@ const ActionSheet = React.memo(
|
|||
}
|
||||
}, [isVisible]);
|
||||
|
||||
// Hides action sheet when orientation changes
|
||||
useEffect(() => {
|
||||
setVisible(false);
|
||||
}, [isLandscape]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
showActionSheet: show,
|
||||
hideActionSheet: hide
|
||||
|
@ -118,11 +138,11 @@ const ActionSheet = React.memo(
|
|||
[]
|
||||
);
|
||||
|
||||
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
|
||||
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 && isLandscape && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {};
|
||||
const androidTablet: any = isTablet && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -130,10 +150,11 @@ const ActionSheet = React.memo(
|
|||
{isVisible && (
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
snapPoints={animatedSnapPoints}
|
||||
// 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 ? undefined : animatedContentHeight}
|
||||
contentHeight={data.snaps?.length || data.options?.length ? undefined : animatedContentHeight}
|
||||
animationConfigs={ANIMATION_CONFIG}
|
||||
animateOnMount={true}
|
||||
backdropComponent={renderBackdrop}
|
||||
|
|
|
@ -43,12 +43,12 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onL
|
|||
data={options}
|
||||
refreshing={false}
|
||||
keyExtractor={item => item.title}
|
||||
bounces={true}
|
||||
bounces={false}
|
||||
renderItem={renderItem}
|
||||
style={{ backgroundColor: colors.focusedBackground, paddingBottom: bottom }}
|
||||
style={{ backgroundColor: colors.focusedBackground }}
|
||||
keyboardDismissMode='interactive'
|
||||
indicatorStyle='black'
|
||||
contentContainerStyle={styles.content}
|
||||
contentContainerStyle={{ paddingBottom: bottom }}
|
||||
ItemSeparatorComponent={List.Separator}
|
||||
ListHeaderComponent={List.Separator}
|
||||
ListFooterComponent={renderFooter}
|
||||
|
@ -57,7 +57,7 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onL
|
|||
);
|
||||
}
|
||||
return (
|
||||
<BottomSheetView testID='action-sheet' style={[styles.contentContainer, { paddingBottom: bottom }]} onLayout={onLayout}>
|
||||
<BottomSheetView testID='action-sheet' style={styles.contentContainer} onLayout={onLayout}>
|
||||
{children}
|
||||
</BottomSheetView>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useTheme } from '../../theme';
|
|||
export const Handle = React.memo(() => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<View style={[styles.handle]} testID='action-sheet-handle'>
|
||||
<View style={styles.handle} testID='action-sheet-handle'>
|
||||
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -15,10 +15,12 @@ export type TActionSheetOptionsItem = {
|
|||
|
||||
export type TActionSheetOptions = {
|
||||
options?: TActionSheetOptionsItem[];
|
||||
headerHeight?: number;
|
||||
customHeader?: React.ReactElement | null;
|
||||
hasCancel?: boolean;
|
||||
type?: string;
|
||||
// children can both use snaps or dynamic
|
||||
children?: React.ReactElement | null;
|
||||
/** Required if your action sheet needs vertical scroll */
|
||||
snaps?: (string | number)[];
|
||||
onClose?: () => void;
|
||||
enableContentPanningGesture?: boolean;
|
||||
|
|
|
@ -19,9 +19,6 @@ export default StyleSheet.create({
|
|||
separator: {
|
||||
marginHorizontal: 16
|
||||
},
|
||||
content: {
|
||||
paddingTop: 16
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1
|
||||
},
|
||||
|
|
|
@ -31,6 +31,7 @@ interface THeaderFooter {
|
|||
theme: TSupportedThemes;
|
||||
}
|
||||
|
||||
export const HEADER_HEIGHT = 54;
|
||||
const ITEM_SIZE = 36;
|
||||
const CONTAINER_MARGIN = 8;
|
||||
const ITEM_MARGIN = 8;
|
||||
|
@ -38,7 +39,8 @@ const ITEM_MARGIN = 8;
|
|||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
marginHorizontal: CONTAINER_MARGIN
|
||||
marginHorizontal: CONTAINER_MARGIN,
|
||||
paddingBottom: 16
|
||||
},
|
||||
headerItem: {
|
||||
height: ITEM_SIZE,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { LISTENER } from '../Toast';
|
|||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import { showConfirmationAlert } from '../../lib/methods/helpers/info';
|
||||
import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet';
|
||||
import Header, { IHeader } from './Header';
|
||||
import Header, { HEADER_HEIGHT, IHeader } from './Header';
|
||||
import events from '../../lib/methods/helpers/log/events';
|
||||
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
|
||||
import { getPermalinkMessage } from '../../lib/methods';
|
||||
|
@ -511,6 +511,7 @@ const MessageActions = React.memo(
|
|||
await getPermissions();
|
||||
showActionSheet({
|
||||
options: getOptions(message),
|
||||
headerHeight: HEADER_HEIGHT,
|
||||
customHeader:
|
||||
!isReadOnly || room.reactWhenReadOnly ? (
|
||||
<Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Camera, CameraType } from 'expo-camera';
|
|||
import React, { useState } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { useAppSelector } from '..';
|
||||
import { cancelCall, initVideoCall } from '../../../actions/videoConf';
|
||||
|
@ -19,6 +20,7 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R
|
|||
const [mic, setMic] = useState(true);
|
||||
const [cam, setCam] = useState(false);
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
|
||||
const username = useAppSelector(state => getUserSelector(state).username);
|
||||
const calling = useAppSelector(state => state.videoConf.calling);
|
||||
|
@ -36,7 +38,10 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R
|
|||
);
|
||||
|
||||
return (
|
||||
<View style={style.actionSheetContainer} onLayout={e => setContainerWidth(e.nativeEvent.layout.width / 2)}>
|
||||
<View
|
||||
style={[style.actionSheetContainer, { paddingBottom: bottom }]}
|
||||
onLayout={e => setContainerWidth(e.nativeEvent.layout.width / 2)}
|
||||
>
|
||||
{calling ? <Ringer ringer={ERingerSounds.DIALTONE} /> : null}
|
||||
<CallHeader
|
||||
title={calling && user.direct ? i18n.t('Calling') : i18n.t('Start_a_call')}
|
||||
|
|
|
@ -478,7 +478,7 @@ describe('Room screen', () => {
|
|||
.toExist()
|
||||
.withTimeout(2000);
|
||||
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||
await element(by.id('action-sheet')).swipe('up', 'fast', 0.5);
|
||||
await sleep(300); // wait for animation
|
||||
await waitFor(element(by[textMatcher]('Delete')))
|
||||
.toExist()
|
||||
|
|
|
@ -237,7 +237,7 @@ describe('Threads', () => {
|
|||
.withTimeout(5000);
|
||||
await element(by.id(`message-thread-button-${thread}`)).tap();
|
||||
await tryTapping(element(by[textMatcher]('replied')).atIndex(0), 2000, true);
|
||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||
await element(by.id('action-sheet')).swipe('up', 'fast', 0.5);
|
||||
await sleep(300); // wait for animation
|
||||
await element(by[textMatcher]('Delete')).atIndex(0).tap();
|
||||
await element(by[textMatcher]('Delete').and(by.type(alertButtonType))).tap();
|
||||
|
|
Loading…
Reference in New Issue