Skip to content
Merged
Changes from all commits
Commits
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
156 changes: 93 additions & 63 deletions src/components/Modal/ReanimatedModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import noop from 'lodash/noop';
import React, {useEffect, useEffectEvent, useRef, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {NativeEventSubscription, ViewStyle} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import {BackHandler, InteractionManager, Modal, StyleSheet, View} from 'react-native';
Expand Down Expand Up @@ -55,64 +55,37 @@ function ReanimatedModal({
shouldReturnFocus,
...props
}: ReanimatedModalProps) {
const [isVisibleState, setIsVisibleState] = useState(isVisible);
const [isContainerOpen, setIsContainerOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const {windowWidth, windowHeight} = useWindowDimensions();
const styles = useThemeStyles();

const backHandlerListener = useRef<NativeEventSubscription | null>(null);
const handleRef = useRef<number | undefined>(undefined);
const transitionHandleRef = useRef<TransitionHandle | null>(null);

const isTransitioning = isVisible !== isContainerOpen;
const backdropStyle: ViewStyle = {width: windowWidth, height: windowHeight, backgroundColor: backdropColor};
const modalStyle = {zIndex: StyleSheet.flatten(style)?.zIndex};
const styles = useThemeStyles();

const onBackButtonPressHandler = () => {
const onBackButtonPressHandler = useCallback(() => {
if (shouldIgnoreBackHandlerDuringTransition && isTransitioning) {
return false;
}
if (isVisible) {
if (isVisibleState) {
onBackButtonPress();
return true;
}
return false;
};

const handleEscape = (e: KeyboardEvent) => {
if (e.key !== 'Escape' || onBackButtonPressHandler() !== true) {
return;
}
e.stopImmediatePropagation();
};

const clearTransitionHandles = () => {
if (handleRef.current) {
InteractionManager.clearInteractionHandle(handleRef.current);
handleRef.current = undefined;
}
if (transitionHandleRef.current) {
TransitionTracker.endTransition(transitionHandleRef.current);
transitionHandleRef.current = null;
}
};

const onOpenCallBack = () => {
setIsContainerOpen(true);
clearTransitionHandles();
onModalShow();
};

const onCloseCallBack = () => {
setIsContainerOpen(false);
clearTransitionHandles();
}, [isVisibleState, onBackButtonPress, isTransitioning, shouldIgnoreBackHandlerDuringTransition]);

// Because on Android, the Modal's onDismiss callback does not work reliably. There's a reported issue at:
// https://stackoverflow.com/questions/58937956/react-native-modal-ondismiss-not-invoked
// Therefore, we manually call onModalHide() here for Android.
if (getPlatform() === CONST.PLATFORM.ANDROID) {
onModalHide();
}
};
const handleEscape = useCallback(
(e: KeyboardEvent) => {
if (e.key !== 'Escape' || onBackButtonPressHandler() !== true) {
return;
}
e.stopImmediatePropagation();
},
[onBackButtonPressHandler],
);

useEffect(() => {
if (getPlatform() === CONST.PLATFORM.WEB) {
Expand All @@ -130,29 +103,86 @@ function ReanimatedModal({
};
}, [handleEscape, onBackButtonPressHandler]);

useEffect(
() => () => {
if (handleRef.current) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.clearInteractionHandle(handleRef.current);
}
if (transitionHandleRef.current) {
TransitionTracker.endTransition(transitionHandleRef.current);
transitionHandleRef.current = null;
}

setIsVisibleState(false);
setIsContainerOpen(false);
},

[],
);

useEffect(() => {
if (isTransitioning) {
if (isVisible && !isContainerOpen && !isTransitioning) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
handleRef.current = InteractionManager.createInteractionHandle();
transitionHandleRef.current = TransitionTracker.startTransition();
}

return () => {
clearTransitionHandles();
};
}, [isTransitioning]);

const fireTransitionCallbacks = useEffectEvent(() => {
if (isVisible && !isContainerOpen) {
onModalWillShow();
} else if (!isVisible && isContainerOpen) {

// eslint-disable-next-line react-hooks/set-state-in-effect
setIsVisibleState(true);
setIsTransitioning(true);
} else if (!isVisible && isContainerOpen && !isTransitioning) {
handleRef.current = InteractionManager.createInteractionHandle();
transitionHandleRef.current = TransitionTracker.startTransition();
onModalWillHide();

blurActiveElement();
setIsVisibleState(false);
setIsTransitioning(true);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible, isContainerOpen, isTransitioning]);

useEffect(() => {
fireTransitionCallbacks();
}, [isVisible, isContainerOpen]);
const backdropStyle: ViewStyle = useMemo(() => {
return {width: windowWidth, height: windowHeight, backgroundColor: backdropColor};
}, [windowWidth, windowHeight, backdropColor]);

const onOpenCallBack = useCallback(() => {
setIsTransitioning(false);
setIsContainerOpen(true);
if (handleRef.current) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.clearInteractionHandle(handleRef.current);
}
if (transitionHandleRef.current) {
TransitionTracker.endTransition(transitionHandleRef.current);
transitionHandleRef.current = null;
}
onModalShow();
}, [onModalShow]);

const onCloseCallBack = useCallback(() => {
setIsTransitioning(false);
setIsContainerOpen(false);
if (handleRef.current) {
InteractionManager.clearInteractionHandle(handleRef.current);
}
if (transitionHandleRef.current) {
TransitionTracker.endTransition(transitionHandleRef.current);
transitionHandleRef.current = null;
}

// Because on Android, the Modal's onDismiss callback does not work reliably. There's a reported issue at:
// https://stackoverflow.com/questions/58937956/react-native-modal-ondismiss-not-invoked
// Therefore, we manually call onModalHide() here for Android.
if (getPlatform() === CONST.PLATFORM.ANDROID) {
onModalHide();
}
}, [onModalHide]);

const modalStyle = useMemo(() => {
return {zIndex: StyleSheet.flatten(style)?.zIndex};
}, [style]);

const containerView = (
<Container
Expand Down Expand Up @@ -186,7 +216,7 @@ function ReanimatedModal({
/>
);

if (!coverScreen && isVisible) {
if (!coverScreen && isVisibleState) {
return (
<View
pointerEvents="box-none"
Expand All @@ -197,8 +227,8 @@ function ReanimatedModal({
</View>
);
}
const isBackdropMounted = isVisible || (isTransitioning && getPlatform() === CONST.PLATFORM.WEB);
const modalVisibility = isVisible || isTransitioning;
const isBackdropMounted = isVisibleState || ((isTransitioning || isContainerOpen !== isVisibleState) && getPlatform() === CONST.PLATFORM.WEB);
const modalVisibility = isVisibleState || isTransitioning || isContainerOpen !== isVisibleState;
return (
<LayoutAnimationConfig skipExiting={getPlatform() !== CONST.PLATFORM.WEB}>
<Modal
Expand All @@ -225,7 +255,7 @@ function ReanimatedModal({
pointerEvents="box-none"
style={[style, {margin: 0}]}
>
{isVisible && containerView}
{isVisibleState && containerView}
</KeyboardAvoidingView>
) : (
<FocusTrapForModal
Expand All @@ -234,7 +264,7 @@ function ReanimatedModal({
shouldReturnFocus={shouldReturnFocus ?? !shouldEnableNewFocusManagement}
shouldPreventScroll={shouldPreventScrollOnFocus}
>
{isVisible && containerView}
{isVisibleState && containerView}
</FocusTrapForModal>
)}
</Modal>
Expand Down
Loading