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:
Diego Mello 2023-10-03 09:57:49 -03:00 committed by GitHub
parent 37c58eb3ba
commit a1172f8bf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 56 additions and 28 deletions

View File

@ -4,15 +4,18 @@ import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useR
import { Keyboard, useWindowDimensions } from 'react-native'; import { Keyboard, useWindowDimensions } from 'react-native';
import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated'; import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { isIOS, isTablet } from '../../lib/methods/helpers'; import { isIOS, isTablet } from '../../lib/methods/helpers';
import { Handle } from './Handle'; import { Handle } from './Handle';
import { TActionSheetOptions } from './Provider'; import { TActionSheetOptions } from './Provider';
import BottomSheetContent from './BottomSheetContent'; import BottomSheetContent from './BottomSheetContent';
import styles from './styles'; import styles, { ITEM_HEIGHT } from './styles';
export const ACTION_SHEET_ANIMATION_DURATION = 250; export const ACTION_SHEET_ANIMATION_DURATION = 250;
const HANDLE_HEIGHT = 28;
const CANCEL_HEIGHT = 64;
const ANIMATION_CONFIG = { const ANIMATION_CONFIG = {
duration: ACTION_SHEET_ANIMATION_DURATION, duration: ACTION_SHEET_ANIMATION_DURATION,
@ -23,11 +26,11 @@ const ANIMATION_CONFIG = {
const ActionSheet = React.memo( const ActionSheet = React.memo(
forwardRef(({ children }: { children: React.ReactElement }, ref) => { forwardRef(({ children }: { children: React.ReactElement }, ref) => {
const { colors } = useTheme(); const { colors } = useTheme();
const { height: windowHeight } = useWindowDimensions();
const { bottom } = useSafeAreaInsets();
const bottomSheetRef = useRef<BottomSheet>(null); const bottomSheetRef = useRef<BottomSheet>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions); const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
const animatedContentHeight = useSharedValue(0); const animatedContentHeight = useSharedValue(0);
const animatedHandleHeight = useSharedValue(0); const animatedHandleHeight = useSharedValue(0);
const animatedDataSnaps = useSharedValue<TActionSheetOptions['snaps']>([]); const animatedDataSnaps = useSharedValue<TActionSheetOptions['snaps']>([]);
@ -49,11 +52,33 @@ const ActionSheet = React.memo(
layout: { height } 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 toggleVisible = () => setVisible(!isVisible);
const hide = () => { const hide = () => {
@ -82,11 +107,6 @@ const ActionSheet = React.memo(
} }
}, [isVisible]); }, [isVisible]);
// Hides action sheet when orientation changes
useEffect(() => {
setVisible(false);
}, [isLandscape]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
showActionSheet: show, showActionSheet: show,
hideActionSheet: hide 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 // Must need this prop to avoid keyboard dismiss
// when is android tablet and the input text is focused // 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 ( return (
<> <>
@ -130,10 +150,11 @@ const ActionSheet = React.memo(
{isVisible && ( {isVisible && (
<BottomSheet <BottomSheet
ref={bottomSheetRef} 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} handleHeight={animatedHandleHeight}
// We need undefined to enable vertical swipe gesture inside the bottom sheet like in reaction picker // 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} animationConfigs={ANIMATION_CONFIG}
animateOnMount={true} animateOnMount={true}
backdropComponent={renderBackdrop} backdropComponent={renderBackdrop}

View File

@ -43,12 +43,12 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onL
data={options} data={options}
refreshing={false} refreshing={false}
keyExtractor={item => item.title} keyExtractor={item => item.title}
bounces={true} bounces={false}
renderItem={renderItem} renderItem={renderItem}
style={{ backgroundColor: colors.focusedBackground, paddingBottom: bottom }} style={{ backgroundColor: colors.focusedBackground }}
keyboardDismissMode='interactive' keyboardDismissMode='interactive'
indicatorStyle='black' indicatorStyle='black'
contentContainerStyle={styles.content} contentContainerStyle={{ paddingBottom: bottom }}
ItemSeparatorComponent={List.Separator} ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator} ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
@ -57,7 +57,7 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onL
); );
} }
return ( return (
<BottomSheetView testID='action-sheet' style={[styles.contentContainer, { paddingBottom: bottom }]} onLayout={onLayout}> <BottomSheetView testID='action-sheet' style={styles.contentContainer} onLayout={onLayout}>
{children} {children}
</BottomSheetView> </BottomSheetView>
); );

View File

@ -8,7 +8,7 @@ import { useTheme } from '../../theme';
export const Handle = React.memo(() => { export const Handle = React.memo(() => {
const { theme } = useTheme(); const { theme } = useTheme();
return ( 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 style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View> </View>
); );

View File

@ -15,10 +15,12 @@ export type TActionSheetOptionsItem = {
export type TActionSheetOptions = { export type TActionSheetOptions = {
options?: TActionSheetOptionsItem[]; options?: TActionSheetOptionsItem[];
headerHeight?: number;
customHeader?: React.ReactElement | null; customHeader?: React.ReactElement | null;
hasCancel?: boolean; hasCancel?: boolean;
type?: string; // children can both use snaps or dynamic
children?: React.ReactElement | null; children?: React.ReactElement | null;
/** Required if your action sheet needs vertical scroll */
snaps?: (string | number)[]; snaps?: (string | number)[];
onClose?: () => void; onClose?: () => void;
enableContentPanningGesture?: boolean; enableContentPanningGesture?: boolean;

View File

@ -19,9 +19,6 @@ export default StyleSheet.create({
separator: { separator: {
marginHorizontal: 16 marginHorizontal: 16
}, },
content: {
paddingTop: 16
},
titleContainer: { titleContainer: {
flex: 1 flex: 1
}, },

View File

@ -31,6 +31,7 @@ interface THeaderFooter {
theme: TSupportedThemes; theme: TSupportedThemes;
} }
export const HEADER_HEIGHT = 54;
const ITEM_SIZE = 36; const ITEM_SIZE = 36;
const CONTAINER_MARGIN = 8; const CONTAINER_MARGIN = 8;
const ITEM_MARGIN = 8; const ITEM_MARGIN = 8;
@ -38,7 +39,8 @@ const ITEM_MARGIN = 8;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
alignItems: 'center', alignItems: 'center',
marginHorizontal: CONTAINER_MARGIN marginHorizontal: CONTAINER_MARGIN,
paddingBottom: 16
}, },
headerItem: { headerItem: {
height: ITEM_SIZE, height: ITEM_SIZE,

View File

@ -13,7 +13,7 @@ import { LISTENER } from '../Toast';
import EventEmitter from '../../lib/methods/helpers/events'; import EventEmitter from '../../lib/methods/helpers/events';
import { showConfirmationAlert } from '../../lib/methods/helpers/info'; import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet'; 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 events from '../../lib/methods/helpers/log/events';
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods'; import { getPermalinkMessage } from '../../lib/methods';
@ -511,6 +511,7 @@ const MessageActions = React.memo(
await getPermissions(); await getPermissions();
showActionSheet({ showActionSheet({
options: getOptions(message), options: getOptions(message),
headerHeight: HEADER_HEIGHT,
customHeader: customHeader:
!isReadOnly || room.reactWhenReadOnly ? ( !isReadOnly || room.reactWhenReadOnly ? (
<Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} /> <Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />

View File

@ -2,6 +2,7 @@ import { Camera, CameraType } from 'expo-camera';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAppSelector } from '..'; import { useAppSelector } from '..';
import { cancelCall, initVideoCall } from '../../../actions/videoConf'; 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 [mic, setMic] = useState(true);
const [cam, setCam] = useState(false); const [cam, setCam] = useState(false);
const [containerWidth, setContainerWidth] = useState(0); const [containerWidth, setContainerWidth] = useState(0);
const { bottom } = useSafeAreaInsets();
const username = useAppSelector(state => getUserSelector(state).username); const username = useAppSelector(state => getUserSelector(state).username);
const calling = useAppSelector(state => state.videoConf.calling); const calling = useAppSelector(state => state.videoConf.calling);
@ -36,7 +38,10 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R
); );
return ( 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} {calling ? <Ringer ringer={ERingerSounds.DIALTONE} /> : null}
<CallHeader <CallHeader
title={calling && user.direct ? i18n.t('Calling') : i18n.t('Start_a_call')} title={calling && user.direct ? i18n.t('Calling') : i18n.t('Start_a_call')}

View File

@ -478,7 +478,7 @@ describe('Room screen', () => {
.toExist() .toExist()
.withTimeout(2000); .withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); 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 sleep(300); // wait for animation
await waitFor(element(by[textMatcher]('Delete'))) await waitFor(element(by[textMatcher]('Delete')))
.toExist() .toExist()

View File

@ -237,7 +237,7 @@ describe('Threads', () => {
.withTimeout(5000); .withTimeout(5000);
await element(by.id(`message-thread-button-${thread}`)).tap(); await element(by.id(`message-thread-button-${thread}`)).tap();
await tryTapping(element(by[textMatcher]('replied')).atIndex(0), 2000, true); 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 sleep(300); // wait for animation
await element(by[textMatcher]('Delete')).atIndex(0).tap(); await element(by[textMatcher]('Delete')).atIndex(0).tap();
await element(by[textMatcher]('Delete').and(by.type(alertButtonType))).tap(); await element(by[textMatcher]('Delete').and(by.type(alertButtonType))).tap();