Skip to content

Commit

Permalink
Merge pull request #25672 from JKobrynski/migrateBaseModal
Browse files Browse the repository at this point in the history
rewrite BaseModal to a functional component
  • Loading branch information
marcaaron authored Aug 25, 2023
2 parents 22c1687 + 7423e68 commit d17d0c7
Showing 1 changed file with 174 additions and 157 deletions.
331 changes: 174 additions & 157 deletions src/components/Modal/BaseModal.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React, {PureComponent} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import ReactNativeModal from 'react-native-modal';
import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import styles from '../../styles/styles';
import * as Modal from '../../libs/actions/Modal';
import * as StyleUtils from '../../styles/StyleUtils';
import themeColors from '../../styles/themes/default';
import {propTypes as modalPropTypes, defaultProps as modalDefaultProps} from './modalPropTypes';
import * as Modal from '../../libs/actions/Modal';
import getModalStyles from '../../styles/getModalStyles';
import useWindowDimensions from '../../hooks/useWindowDimensions';
import variables from '../../styles/variables';
import CONST from '../../CONST';
import ComposerFocusManager from '../../libs/ComposerFocusManager';

const propTypes = {
Expand All @@ -24,173 +26,188 @@ const defaultProps = {
forwardedRef: () => {},
};

class BaseModal extends PureComponent {
constructor(props) {
super(props);

this.hideModal = this.hideModal.bind(this);
}

componentDidMount() {
if (!this.props.isVisible) {
return;
}

Modal.willAlertModalBecomeVisible(true);

// To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu
Modal.setCloseModal(this.props.onClose);
}

componentDidUpdate(prevProps) {
if (prevProps.isVisible === this.props.isVisible) {
return;
}

Modal.willAlertModalBecomeVisible(this.props.isVisible);
Modal.setCloseModal(this.props.isVisible ? this.props.onClose : null);
}

componentWillUnmount() {
// Only trigger onClose and setModalVisibility if the modal is unmounting while visible.
if (this.props.isVisible) {
this.hideModal(true);
Modal.willAlertModalBecomeVisible(false);
}

// To prevent closing any modal already unmounted when this modal still remains as visible state
Modal.setCloseModal(null);
}
function BaseModal({
isVisible,
onClose,
shouldSetModalVisibility,
onModalHide,
type,
popoverAnchorPosition,
innerContainerStyle,
outerStyle,
onModalShow,
propagateSwipe,
fullscreen,
animationIn,
animationOut,
useNativeDriver,
hideModalContentWhileAnimating,
animationInTiming,
animationOutTiming,
statusBarTranslucent,
onLayout,
avoidKeyboard,
forwardedRef,
children,
}) {
const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions();

const safeAreaInsets = useSafeAreaInsets();

/**
* Hides modal
* @param {Boolean} [callHideCallback=true] Should we call the onModalHide callback
*/
hideModal(callHideCallback = true) {
if (this.props.shouldSetModalVisibility) {
Modal.setModalVisibility(false);
}
if (callHideCallback) {
this.props.onModalHide();
const hideModal = useCallback(
(callHideCallback = true) => {
if (shouldSetModalVisibility) {
Modal.setModalVisibility(false);
}
if (callHideCallback) {
onModalHide();
}
Modal.onModalDidClose();
if (!fullscreen) {
ComposerFocusManager.setReadyToFocus();
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- adding onModalHide to the dependency array causes too many unnecessary rerenders
[shouldSetModalVisibility],
);

useEffect(() => {
Modal.willAlertModalBecomeVisible(isVisible);

// To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu
Modal.setCloseModal(isVisible ? onClose : null);

return () => {
// Only trigger onClose and setModalVisibility if the modal is unmounting while visible.
if (isVisible) {
hideModal(true);
Modal.willAlertModalBecomeVisible(false);
}

// To prevent closing any modal already unmounted when this modal still remains as visible state
Modal.setCloseModal(null);
};
}, [hideModal, isVisible, onClose]);

const handleShowModal = () => {
if (shouldSetModalVisibility) {
Modal.setModalVisibility(true);
}
Modal.onModalDidClose();
if (!this.props.fullscreen) {
ComposerFocusManager.setReadyToFocus();
onModalShow();
};

const handleBackdropPress = (e) => {
if (e && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
return;
}
}

render() {
const {
modalStyle,
modalContainerStyle,
swipeDirection,
animationIn,
animationOut,
shouldAddTopSafeAreaMargin,
shouldAddBottomSafeAreaMargin,
shouldAddTopSafeAreaPadding,
shouldAddBottomSafeAreaPadding,
hideBackdrop,
} = getModalStyles(
this.props.type,
{
windowWidth: this.props.windowWidth,
windowHeight: this.props.windowHeight,
isSmallScreenWidth: this.props.isSmallScreenWidth,
},
this.props.popoverAnchorPosition,
this.props.innerContainerStyle,
this.props.outerStyle,
);
return (
<ReactNativeModal
onBackdropPress={(e) => {
if (e && e.key === 'Enter') {
return;
}
this.props.onClose();
}}
// Note: Escape key on web/desktop will trigger onBackButtonPress callback
// eslint-disable-next-line react/jsx-props-no-multi-spaces
onBackButtonPress={this.props.onClose}
onModalWillShow={() => {
ComposerFocusManager.resetReadyToFocus();
}}
onModalShow={() => {
if (this.props.shouldSetModalVisibility) {
Modal.setModalVisibility(true);
}
this.props.onModalShow();
}}
propagateSwipe={this.props.propagateSwipe}
onModalHide={this.hideModal}
onDismiss={() => ComposerFocusManager.setReadyToFocus()}
onSwipeComplete={this.props.onClose}
swipeDirection={swipeDirection}
isVisible={this.props.isVisible}
backdropColor={themeColors.overlay}
backdropOpacity={hideBackdrop ? 0 : variables.overlayOpacity}
backdropTransitionOutTiming={0}
hasBackdrop={this.props.fullscreen}
coverScreen={this.props.fullscreen}
style={modalStyle}
deviceHeight={this.props.windowHeight}
deviceWidth={this.props.windowWidth}
animationIn={this.props.animationIn || animationIn}
animationOut={this.props.animationOut || animationOut}
useNativeDriver={this.props.useNativeDriver}
hideModalContentWhileAnimating={this.props.hideModalContentWhileAnimating}
animationInTiming={this.props.animationInTiming}
animationOutTiming={this.props.animationOutTiming}
statusBarTranslucent={this.props.statusBarTranslucent}
onLayout={this.props.onLayout}
avoidKeyboard={this.props.avoidKeyboard}
onClose();
};

const handleDismissModal = () => {
ComposerFocusManager.setReadyToFocus();
};

const {
modalStyle,
modalContainerStyle,
swipeDirection,
animationIn: modalStyleAnimationIn,
animationOut: modalStyleAnimationOut,
shouldAddTopSafeAreaMargin,
shouldAddBottomSafeAreaMargin,
shouldAddTopSafeAreaPadding,
shouldAddBottomSafeAreaPadding,
hideBackdrop,
} = useMemo(
() =>
getModalStyles(
type,
{
windowWidth,
windowHeight,
isSmallScreenWidth,
},
popoverAnchorPosition,
innerContainerStyle,
outerStyle,
),
[innerContainerStyle, isSmallScreenWidth, outerStyle, popoverAnchorPosition, type, windowHeight, windowWidth],
);

const {
paddingTop: safeAreaPaddingTop,
paddingBottom: safeAreaPaddingBottom,
paddingLeft: safeAreaPaddingLeft,
paddingRight: safeAreaPaddingRight,
} = StyleUtils.getSafeAreaPadding(safeAreaInsets);

const modalPaddingStyles = StyleUtils.getModalPaddingStyles({
safeAreaPaddingTop,
safeAreaPaddingBottom,
safeAreaPaddingLeft,
safeAreaPaddingRight,
shouldAddBottomSafeAreaMargin,
shouldAddTopSafeAreaMargin,
shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaPadding,
modalContainerStyleMarginTop: modalContainerStyle.marginTop,
modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
insets: safeAreaInsets,
});

return (
<ReactNativeModal
onBackdropPress={handleBackdropPress}
// Note: Escape key on web/desktop will trigger onBackButtonPress callback
// eslint-disable-next-line react/jsx-props-no-multi-spaces
onBackButtonPress={onClose}
onModalShow={handleShowModal}
propagateSwipe={propagateSwipe}
onModalHide={hideModal}
onDismiss={handleDismissModal}
onSwipeComplete={onClose}
swipeDirection={swipeDirection}
isVisible={isVisible}
backdropColor={themeColors.overlay}
backdropOpacity={hideBackdrop ? 0 : variables.overlayOpacity}
backdropTransitionOutTiming={0}
hasBackdrop={fullscreen}
coverScreen={fullscreen}
style={modalStyle}
deviceHeight={windowHeight}
deviceWidth={windowWidth}
animationIn={animationIn || modalStyleAnimationIn}
animationOut={animationOut || modalStyleAnimationOut}
useNativeDriver={useNativeDriver}
hideModalContentWhileAnimating={hideModalContentWhileAnimating}
animationInTiming={animationInTiming}
animationOutTiming={animationOutTiming}
statusBarTranslucent={statusBarTranslucent}
onLayout={onLayout}
avoidKeyboard={avoidKeyboard}
>
<View
style={[styles.defaultModalContainer, modalContainerStyle, modalPaddingStyles, !isVisible && styles.pointerEventsNone]}
ref={forwardedRef}
nativeID="no-drag-area"
>
<SafeAreaInsetsContext.Consumer>
{(insets) => {
const {
paddingTop: safeAreaPaddingTop,
paddingBottom: safeAreaPaddingBottom,
paddingLeft: safeAreaPaddingLeft,
paddingRight: safeAreaPaddingRight,
} = StyleUtils.getSafeAreaPadding(insets);

const modalPaddingStyles = StyleUtils.getModalPaddingStyles({
safeAreaPaddingTop,
safeAreaPaddingBottom,
safeAreaPaddingLeft,
safeAreaPaddingRight,
shouldAddBottomSafeAreaMargin,
shouldAddTopSafeAreaMargin,
shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaPadding,
modalContainerStyleMarginTop: modalContainerStyle.marginTop,
modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
insets,
});

return (
<View
style={[styles.defaultModalContainer, modalContainerStyle, modalPaddingStyles, !this.props.isVisible ? styles.pointerEventsNone : {}]}
ref={this.props.forwardedRef}
nativeID="no-drag-area"
>
{this.props.children}
</View>
);
}}
</SafeAreaInsetsContext.Consumer>
</ReactNativeModal>
);
}
{children}
</View>
</ReactNativeModal>
);
}

BaseModal.propTypes = propTypes;
BaseModal.defaultProps = defaultProps;
BaseModal.displayName = 'BaseModal';

export default React.forwardRef((props, ref) => (
export default forwardRef((props, ref) => (
<BaseModal
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
Expand Down

0 comments on commit d17d0c7

Please sign in to comment.