diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js index 79dd98d0e876..8035eca3d96a 100644 --- a/src/components/Modal/BaseModal.js +++ b/src/components/Modal/BaseModal.js @@ -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 = { @@ -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 ( - { - 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 ( + + - - {(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 ( - - {this.props.children} - - ); - }} - - - ); - } + {children} + + + ); } BaseModal.propTypes = propTypes; BaseModal.defaultProps = defaultProps; +BaseModal.displayName = 'BaseModal'; -export default React.forwardRef((props, ref) => ( +export default forwardRef((props, ref) => (