-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
rewrite BaseModal to a functional component #25672
Changes from 6 commits
3ae6ffa
485b4df
d2f2331
379107f
5fd5ee1
ee17a26
a40077b
7423e68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
|
||
const propTypes = { | ||
...modalPropTypes, | ||
|
@@ -23,166 +25,180 @@ 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); | ||
const hideModal = useCallback( | ||
(callHideCallback = true) => { | ||
if (shouldSetModalVisibility) { | ||
Modal.setModalVisibility(false); | ||
} | ||
if (callHideCallback) { | ||
onModalHide(); | ||
} | ||
Modal.onModalDidClose(); | ||
}, | ||
// 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); | ||
} | ||
Comment on lines
+85
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will cause a regression. We should not use the isVisible state here as this will be evaluated at the time of render and not when the component unmounts i.e. if you initially render the modal as visible (true) then on unmount this condition will always be true even if the state value change by then. What we should do instead is to use a ref const isVisibleRef = useRef(isVisible);
useEffect(() => {
isVisibleRef.current = isVisible;
}, [isVisible]); |
||
|
||
// To prevent closing any modal already unmounted when this modal still remains as visible state | ||
Modal.setCloseModal(null); | ||
}; | ||
}, [hideModal, isVisible, onClose]); | ||
Comment on lines
+78
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like we have a regression from this change, when onClose callback changes, it will also hide the modal due to the cleanup logic of this effect. #25998 |
||
|
||
const handleShowModal = () => { | ||
if (shouldSetModalVisibility) { | ||
Modal.setModalVisibility(true); | ||
} | ||
if (callHideCallback) { | ||
this.props.onModalHide(); | ||
onModalShow(); | ||
}; | ||
|
||
const handleBackdropPress = (e) => { | ||
if (e && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) { | ||
return; | ||
} | ||
Modal.onModalDidClose(); | ||
} | ||
|
||
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} | ||
onModalShow={() => { | ||
if (this.props.shouldSetModalVisibility) { | ||
Modal.setModalVisibility(true); | ||
} | ||
this.props.onModalShow(); | ||
}} | ||
propagateSwipe={this.props.propagateSwipe} | ||
onModalHide={this.hideModal} | ||
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 { | ||
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} | ||
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} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This caused a regression #27359. Previously on component mount Modal.setCloseModal was only called if the modal was rendered visible.