Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UX Reliability] Use new modal in BaseReportActionContextMenu #56606

Draft
wants to merge 51 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
fb2f774
feat: change modal animation library
BartoszGrajdek Nov 6, 2024
1d83349
Merge remote-tracking branch 'origin/main' into @BartoszGrajdek/react…
BartoszGrajdek Nov 7, 2024
442b29d
adding some types, and do some refactor
sumo-slonik Nov 7, 2024
0770e20
working on null handling and linter problems
sumo-slonik Nov 7, 2024
51e30ab
Merge branch '@BartoszGrajdek/react-native-modal-refactor' into @Bart…
sumo-slonik Nov 7, 2024
fed11de
fix types and build error
sumo-slonik Nov 8, 2024
e51ff8a
fix linters problems
sumo-slonik Nov 8, 2024
61d4bf3
fix typo in TransactionType
sumo-slonik Nov 8, 2024
e7b1bb5
fix permissions issue
sumo-slonik Nov 8, 2024
2d43ab0
fix issue with to little height of container
sumo-slonik Nov 12, 2024
ed4e2e9
Merge branch 'main' into @BartoszGrajdek/react-native-modal-refactor-…
sumo-slonik Nov 14, 2024
3ad4eab
fix problem with no handling gestures
sumo-slonik Nov 14, 2024
1e78131
Merge pull request #133 from software-mansion-labs/@BartoszGrajdek/re…
sumo-slonik Nov 15, 2024
fc04fa1
refactor: ts & eslint cleanup
BartoszGrajdek Nov 15, 2024
ad079da
add callbacks to animations instead of timeout
sumo-slonik Nov 18, 2024
dd1ff93
fix hideModalContentWhileAnimating
sumo-slonik Nov 18, 2024
96de617
work in progress on any types
sumo-slonik Nov 18, 2024
b2ab25c
fix emoji picker tab
sumo-slonik Nov 18, 2024
5c2b0f6
add swipe animations using react reanimate
sumo-slonik Nov 19, 2024
a95e496
refactor and clean code
sumo-slonik Nov 20, 2024
d9d2df3
Update SearchRouterModal.tsx
sumo-slonik Nov 20, 2024
6ac6696
changes from Bartek CR
sumo-slonik Nov 25, 2024
525f9ca
Merge remote-tracking branch 'origin/main' into @BartoszGrajdek/react…
BartoszGrajdek Dec 12, 2024
50ea848
Merge branch '@BartoszGrajdek/react-native-modal-refactor' into @Bart…
sumo-slonik Dec 18, 2024
61f207a
feat: add handling for different modal types
BartoszGrajdek Dec 18, 2024
dee6299
Merge branch '@BartoszGrajdek/react-native-modal-refactor' into @Bart…
sumo-slonik Dec 18, 2024
0312e11
fix merge problems
sumo-slonik Dec 18, 2024
afcb7db
Modal code refactor
BartoszGrajdek Dec 18, 2024
17fbefe
[49354] Migrate react-native-modal to reanimated
BartoszGrajdek Jan 13, 2025
757aaa6
chore: resolve merge conflicts
BartoszGrajdek Jan 23, 2025
6735c18
refactor: address review comments & remove gesture handling
BartoszGrajdek Jan 23, 2025
ed6f506
Merge remote-tracking branch 'origin/main' into @BartoszGrajdek/react…
BartoszGrajdek Jan 23, 2025
5daefab
fix: enable new modal in PopoverWithMeasuredContent-related components
BartoszGrajdek Jan 27, 2025
27271a8
chore: resolve merge conflicts
BartoszGrajdek Jan 28, 2025
d400ad4
fix: floating action button performance issues
BartoszGrajdek Jan 28, 2025
bd4d1cd
refactor: remove duplicated opacity on modal backdrop
BartoszGrajdek Jan 28, 2025
02d93f8
fix: bottom-docked modal regressions
BartoszGrajdek Jan 29, 2025
10ddb0b
fix: TS error
BartoszGrajdek Jan 29, 2025
0fd231d
Merge remote-tracking branch 'origin/main' into @BartoszGrajdek/react…
BartoszGrajdek Jan 29, 2025
14b1d84
fix: keyframe mock
BartoszGrajdek Jan 29, 2025
495d21b
chore: migrate only FAB
BartoszGrajdek Jan 31, 2025
9f5ae9b
chore: remove unnecessary changes
BartoszGrajdek Jan 31, 2025
6b7ef68
fix: reanimated keyframe easing
BartoszGrajdek Jan 31, 2025
24c77d0
fix: reanimated keyframe easing
BartoszGrajdek Feb 3, 2025
70bf577
chore: resolve merge conflicts
BartoszGrajdek Feb 3, 2025
38a61f3
chore: resolve merge conflicts
BartoszGrajdek Feb 6, 2025
e57e097
Merge remote-tracking branch 'origin/main' into @BartoszGrajdek/react…
BartoszGrajdek Feb 7, 2025
9d21c42
feat: enable new modal in report action context menu
BartoszGrajdek Feb 10, 2025
4cb6e96
fix: slide out animation not appearing
BartoszGrajdek Feb 10, 2025
59900cf
fix: eslint issues
BartoszGrajdek Feb 11, 2025
4e65f7d
fix: type mismatch
BartoszGrajdek Feb 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 4 additions & 42 deletions src/components/FloatingActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import type {ForwardedRef} from 'react';
import React, {forwardRef, useEffect, useRef} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Role, Text, View} from 'react-native';
import {Platform} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Animated, {Easing, interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Svg, {Path} from 'react-native-svg';
import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused';
import useIsCurrentRouteHome from '@hooks/useIsCurrentRouteHome';
Expand All @@ -22,30 +21,6 @@ import EducationalTooltip from './Tooltip/EducationalTooltip';
const AnimatedPath = Animated.createAnimatedComponent(Path);
AnimatedPath.displayName = 'AnimatedPath';

type AdapterPropsRecord = {
type: number;
payload?: number | null;
};

type AdapterProps = {
fill?: string | AdapterPropsRecord;
stroke?: string | AdapterPropsRecord;
};

const adapter = createAnimatedPropAdapter(
(props: AdapterProps) => {
if (Object.keys(props).includes('fill')) {
// eslint-disable-next-line no-param-reassign
props.fill = {type: 0, payload: processColor(props.fill)};
}
if (Object.keys(props).includes('stroke')) {
// eslint-disable-next-line no-param-reassign
props.stroke = {type: 0, payload: processColor(props.stroke)};
}
},
['fill', 'stroke'],
);

type FloatingActionButtonProps = {
/* Callback to fire on request to toggle the FloatingActionButton */
onPress: (event: GestureResponderEvent | KeyboardEvent | undefined) => void;
Expand All @@ -61,7 +36,7 @@ type FloatingActionButtonProps = {
};

function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef<HTMLDivElement | View | Text>) {
const {success, buttonDefaultBG, textLight, textDark} = useTheme();
const {success, buttonDefaultBG, textLight} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
const fabPressable = useRef<HTMLDivElement | View | Text | null>(null);
Expand Down Expand Up @@ -94,22 +69,9 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
return {
transform: [{rotate: `${sharedValue.get() * 135}deg`}],
backgroundColor,
borderRadius,
};
});

const animatedProps = useAnimatedProps(
() => {
const fill = interpolateColor(sharedValue.get(), [0, 1], [textLight, textDark]);

return {
fill,
};
},
undefined,
Platform.OS === 'web' ? undefined : adapter,
);

const toggleFabAction = (event: GestureResponderEvent | KeyboardEvent | undefined) => {
hideProductTrainingTooltip();
// Drop focus to avoid blue focus ring.
Expand Down Expand Up @@ -144,14 +106,14 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
role={role}
shouldUseHapticsOnLongPress={false}
>
<Animated.View style={[styles.floatingActionButton, animatedStyle]}>
<Animated.View style={[styles.floatingActionButton, {borderRadius}, animatedStyle]}>
<Svg
width={variables.iconSizeNormal}
height={variables.iconSizeNormal}
>
<AnimatedPath
d="M12,3c0-1.1-0.9-2-2-2C8.9,1,8,1.9,8,3v5H3c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h5v5c0,1.1,0.9,2,2,2c1.1,0,2-0.9,2-2v-5h5c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-5V3z"
animatedProps={animatedProps}
fill={textLight}
/>
</Svg>
</Animated.View>
Expand Down
38 changes: 30 additions & 8 deletions src/components/Modal/BaseModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {PortalHost} from '@gorhom/portal';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
import type {ModalProps as ReactNativeModalProps} from 'react-native-modal';
import ReactNativeModal from 'react-native-modal';
import type {ValueOf} from 'type-fest';
import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal';
import useKeyboardState from '@hooks/useKeyboardState';
Expand All @@ -17,10 +18,26 @@ import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay';
import variables from '@styles/variables';
import {areAllModalsHidden, closeTop, onModalDidClose, setCloseModal, setModalVisibility, willAlertModalBecomeVisible} from '@userActions/Modal';
import CONST from '@src/CONST';
import BottomDockedModal from './BottomDockedModal';
import type ModalProps from './BottomDockedModal/types';
import ModalContent from './ModalContent';
import ModalContext from './ModalContext';
import type BaseModalProps from './types';

type ModalComponentProps = (ReactNativeModalProps | ModalProps) & {
type?: ValueOf<typeof CONST.MODAL.MODAL_TYPE>;
shouldUseNewModal: boolean;
};

function ModalComponent({type, shouldUseNewModal, ...props}: ModalComponentProps) {
if (type === CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED && shouldUseNewModal) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <BottomDockedModal {...(props as ModalProps)} />;
}
// eslint-disable-next-line react/jsx-props-no-spreading
return <ReactNativeModal {...(props as ReactNativeModalProps)} />;
}

function BaseModal(
{
isVisible,
Expand All @@ -41,18 +58,22 @@ function BaseModal(
hideModalContentWhileAnimating = false,
animationInTiming,
animationOutTiming,
animationInDelay,
statusBarTranslucent = true,
navigationBarTranslucent = true,
onLayout,
avoidKeyboard = false,
children,
shouldUseCustomBackdrop = false,
shouldUseNewModal = false,
onBackdropPress,
modalId,
shouldEnableNewFocusManagement = false,
restoreFocusType,
shouldUseModalPaddingStyle = true,
initialFocus = false,
swipeThreshold = 150,
swipeDirection,
shouldPreventScrollOnFocus = false,
}: BaseModalProps,
ref: React.ForwardedRef<View>,
Expand All @@ -78,7 +99,6 @@ function BaseModal(
}
ComposerFocusManager.resetReadyToFocus(uniqueModalId);
}, [shouldEnableNewFocusManagement, uniqueModalId]);

/**
* Hides modal
* @param callHideCallback - Should we call the onModalHide callback
Expand Down Expand Up @@ -129,12 +149,12 @@ function BaseModal(
[],
);

const handleShowModal = () => {
const handleShowModal = useCallback(() => {
if (shouldSetModalVisibility) {
setModalVisibility(true);
}
onModalShow();
};
}, [onModalShow, shouldSetModalVisibility]);

const handleBackdropPress = (e?: KeyboardEvent) => {
if (e?.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
Expand All @@ -155,7 +175,6 @@ function BaseModal(
const {
modalStyle,
modalContainerStyle,
swipeDirection,
animationIn: modalStyleAnimationIn,
animationOut: modalStyleAnimationOut,
shouldAddTopSafeAreaMargin,
Expand Down Expand Up @@ -225,7 +244,7 @@ function BaseModal(
collapsable={false}
style={[styles.pAbsolute, {zIndex: 1}]}
>
<ReactNativeModal
<ModalComponent
// Prevent the parent element to capture a click. This is useful when the modal component is put inside a pressable.
onClick={(e) => e.stopPropagation()}
onBackdropPress={handleBackdropPress}
Expand All @@ -239,6 +258,7 @@ function BaseModal(
onDismiss={handleDismissModal}
onSwipeComplete={() => onClose?.()}
swipeDirection={swipeDirection}
swipeThreshold={swipeThreshold}
isVisible={isVisible}
backdropColor={theme.overlay}
backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity}
Expand All @@ -249,6 +269,7 @@ function BaseModal(
deviceHeight={windowHeight}
deviceWidth={windowWidth}
animationIn={animationIn ?? modalStyleAnimationIn}
animationInDelay={animationInDelay}
animationOut={animationOut ?? modalStyleAnimationOut}
useNativeDriver={useNativeDriver}
useNativeDriverForBackdrop={useNativeDriverForBackdrop}
Expand All @@ -260,12 +281,13 @@ function BaseModal(
onLayout={onLayout}
avoidKeyboard={avoidKeyboard}
customBackdrop={shouldUseCustomBackdrop ? <Overlay onPress={handleBackdropPress} /> : undefined}
type={type}
shouldUseNewModal={shouldUseNewModal}
>
<ModalContent
onModalWillShow={saveFocusState}
onDismiss={handleDismissModal}
>
<PortalHost name="modal" />
<FocusTrapForModal
active={isVisible}
initialFocus={initialFocus}
Expand All @@ -279,7 +301,7 @@ function BaseModal(
</View>
</FocusTrapForModal>
</ModalContent>
</ReactNativeModal>
</ModalComponent>
</View>
</ModalContext.Provider>
);
Expand Down
83 changes: 83 additions & 0 deletions src/components/Modal/BottomDockedModal/Backdrop/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, {useMemo} from 'react';
import Animated, {Easing, Keyframe} from 'react-native-reanimated';
import type {ReanimatedKeyframe} from 'react-native-reanimated/lib/typescript/layoutReanimation/animationBuilder/Keyframe';
import type {BackdropProps} from '@components/Modal/BottomDockedModal/types';
import {PressableWithoutFeedback} from '@components/Pressable';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';

const easing = Easing.bezier(0.76, 0.0, 0.24, 1.0);

/**
* Due to issues with react-native-reanimated Keyframes the easing type doesn't account for bezier functions
* and we also need to use internal .build() function to make the easing apply on each mount.
*
* This causes problems with both eslint & Typescript and is going to be fixed in react-native-reanimated 3.17 with these PRs merged:
* https://github.com/software-mansion/react-native-reanimated/pull/6960
* https://github.com/software-mansion/react-native-reanimated/pull/6958
*
* Once that's added we can apply our changes to files in BottomDockedModal/Backdrop/*.tsx and BottomDockedModal/Container/*.tsx
*/

/* eslint-disable @typescript-eslint/no-unsafe-call */
function Backdrop({style, customBackdrop, onBackdropPress, animationInTiming = 300, animationOutTiming = 300}: BackdropProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const Entering = useMemo(() => {
const FadeIn = new Keyframe({
from: {opacity: 0},
to: {
opacity: 0.72,
// @ts-expect-error Types mismatch in reanimated, should to be fixed in 3.17
easing,
},
});

// @ts-expect-error Internal function used to fix easing issue, should to be fixed in 3.17
return FadeIn.duration(animationInTiming).build() as ReanimatedKeyframe;
}, [animationInTiming]);

const Exiting = useMemo(() => {
const FadeOut = new Keyframe({
from: {opacity: 0.72},
to: {
opacity: 0,
// @ts-expect-error Types mismatch in reanimated, should to be fixed in 3.17
easing,
},
});

// @ts-expect-error Internal function used to fix easing issue, should to be fixed in 3.17
return FadeOut.duration(animationOutTiming).build() as ReanimatedKeyframe;
}, [animationOutTiming]);

const BackdropOverlay = useMemo(
() => (
<Animated.View
entering={Entering}
exiting={Exiting}
style={[styles.modalBackdrop, style]}
>
{!!customBackdrop && customBackdrop}
</Animated.View>
),
[Entering, Exiting, customBackdrop, style, styles.modalBackdrop],
);

if (!customBackdrop) {
return (
<PressableWithoutFeedback
accessible
accessibilityLabel={translate('modal.backdropLabel')}
onPressIn={onBackdropPress}
>
{BackdropOverlay}
</PressableWithoutFeedback>
);
}

return BackdropOverlay;
}

export default Backdrop;
79 changes: 79 additions & 0 deletions src/components/Modal/BottomDockedModal/Backdrop/index.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
import Animated, {Easing, Keyframe} from 'react-native-reanimated';
import type {BackdropProps} from '@components/Modal/BottomDockedModal/types';
import {PressableWithoutFeedback} from '@components/Pressable';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';

const easing = Easing.bezier(0.76, 0.0, 0.24, 1.0);

/**
* Due to issues with react-native-reanimated Keyframes the easing type doesn't account for bezier functions
* and we also need to use internal .build() function to make the easing apply on each mount.
*
* This causes problems with both eslint & Typescript and is going to be fixed in react-native-reanimated 3.17 with these PRs merged:
* https://github.com/software-mansion/react-native-reanimated/pull/6960
* https://github.com/software-mansion/react-native-reanimated/pull/6958
*
* Once that's added we can apply our changes to files in BottomDockedModal/Backdrop/*.tsx and BottomDockedModal/Container/*.tsx
*/

/* eslint-disable @typescript-eslint/no-unsafe-call */
function Backdrop({style, customBackdrop, onBackdropPress, animationInTiming = 300, animationOutTiming = 300}: BackdropProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const Entering = useMemo(() => {
const FadeIn = new Keyframe({
from: {opacity: 0},
to: {
opacity: 0.72,
// @ts-expect-error Types mismatch in reanimated, should to be fixed in 3.17
easing,
},
});

return FadeIn.duration(animationInTiming);
}, [animationInTiming]);

const Exiting = useMemo(() => {
const FadeOut = new Keyframe({
from: {opacity: 0.72},
to: {
opacity: 0,
// @ts-expect-error Types mismatch in reanimated, should to be fixed in 3.17
easing,
},
});

return FadeOut.duration(animationOutTiming);
}, [animationOutTiming]);

if (!customBackdrop) {
return (
<PressableWithoutFeedback
accessible
accessibilityLabel={translate('modal.backdropLabel')}
onPress={onBackdropPress}
>
<Animated.View
style={[styles.modalBackdrop, style]}
entering={Entering}
exiting={Exiting}
/>
</PressableWithoutFeedback>
);
}

return (
<Animated.View
entering={Entering}
exiting={Exiting}
>
<View style={[styles.modalBackdrop, style]}>{!!customBackdrop && customBackdrop}</View>
</Animated.View>
);
}

export default Backdrop;
Loading
Loading