diff --git a/packages/react-native/Libraries/Modal/Modal.js b/packages/react-native/Libraries/Modal/Modal.js index d525d98ddd49..0ff46f3386ee 100644 --- a/packages/react-native/Libraries/Modal/Modal.js +++ b/packages/react-native/Libraries/Modal/Modal.js @@ -10,7 +10,6 @@ import type {HostInstance} from '../../src/private/types/HostInstance'; import type {ViewProps} from '../Components/View/ViewPropTypes'; -import type {RootTag} from '../ReactNative/RootTag'; import type {DirectEventHandler} from '../Types/CodegenTypes'; import {type ColorValue} from '../StyleSheet/StyleSheet'; @@ -18,6 +17,7 @@ import Platform from '../Utilities/Platform'; import RCTModalHostView from './RCTModalHostViewNativeComponent'; import VirtualizedLists from '@react-native/virtualized-lists'; import * as React from 'react'; +import {useCallback, useContext, useEffect, useState} from 'react'; const ScrollView = require('../Components/ScrollView/ScrollView').default; const View = require('../Components/View/View').default; @@ -191,21 +191,25 @@ export type ModalProps = { ...ViewProps, }; -function confirmProps(props: ModalProps) { +function confirmProps({ + transparent, + presentationStyle, + navigationBarTranslucent, + statusBarTranslucent, + allowSwipeDismissal, + onRequestClose, +}: ModalProps) { if (__DEV__) { if ( - props.presentationStyle && - props.presentationStyle !== 'overFullScreen' && - props.transparent === true + presentationStyle && + presentationStyle !== 'overFullScreen' && + transparent === true ) { console.warn( - `Modal with '${props.presentationStyle}' presentation style and 'transparent' value is not supported.`, + `Modal with '${presentationStyle}' presentation style and 'transparent' value is not supported.`, ); } - if ( - props.navigationBarTranslucent === true && - props.statusBarTranslucent !== true - ) { + if (navigationBarTranslucent === true && statusBarTranslucent !== true) { console.warn( 'Modal with translucent navigation bar and without translucent status bar is not supported.', ); @@ -213,8 +217,8 @@ function confirmProps(props: ModalProps) { if ( Platform.OS === 'ios' && - props.allowSwipeDismissal === true && - !props.onRequestClose + allowSwipeDismissal === true && + !onRequestClose ) { console.warn( 'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.', @@ -223,150 +227,145 @@ function confirmProps(props: ModalProps) { } } -// Create a state to track whether the Modal is rendering or not. -// This is the only prop that controls whether the modal is rendered or not. -type ModalState = { - isRendered: boolean, -}; +const onStartShouldSetResponder = () => true; // We don't want any responder events bubbling out of the modal. -/** - * A basic way to present content above an enclosing view. - * - * @see https://reactnative.dev/docs/modal - */ -class Modal extends React.Component { - static defaultProps: {hardwareAccelerated: boolean, visible: boolean} = { - visible: true, - hardwareAccelerated: false, - }; - - static contextType: React.Context = RootTagContext; - - _identifier: number; +const Modal: component( + ref?: React.RefSetter, + ...props: ModalProps +) = ({ + backdropColor, + transparent, + children, + presentationStyle, + animationType, + onRequestClose, + onShow, + visible = true, + hardwareAccelerated = false, + ref: modalRef, + statusBarTranslucent, + navigationBarTranslucent, + supportedOrientations, + onOrientationChange, + allowSwipeDismissal, + testID, + onDismiss, + style, +}: { + ref?: React.RefSetter, + ...ModalProps, +}): React.Node => { + const isVisible = visible === true; - constructor(props: ModalProps) { - super(props); - if (__DEV__) { - confirmProps(props); - } - this._identifier = uniqueModalIdentifier++; - this.state = { - isRendered: props.visible === true, - }; - } + // Create a state to track whether the Modal is rendering or not. + // This is the only prop that controls whether the modal is rendered or not. + const [isRendered, setIsRendered] = useState(visible === true); + const [identifier] = useState(() => uniqueModalIdentifier++); - componentWillUnmount() { - if (Platform.OS === 'ios') { - this.setState({isRendered: false}); + useEffect(() => { + if (isVisible) { + setIsRendered(true); } - } + }, [isVisible]); - componentDidUpdate(prevProps: ModalProps) { - if (prevProps.visible === false && this.props.visible === true) { - this.setState({isRendered: true}); - } + useEffect(() => { + return () => { + setIsRendered(false); + }; + }, []); + useEffect(() => { if (__DEV__) { - confirmProps(this.props); + confirmProps({ + transparent, + presentationStyle, + navigationBarTranslucent, + statusBarTranslucent, + allowSwipeDismissal, + onRequestClose, + }); } - } - - // Helper function to encapsulate platform specific logic to show or not the Modal. - _shouldShowModal(): boolean { + }, [ + transparent, + presentationStyle, + navigationBarTranslucent, + statusBarTranslucent, + allowSwipeDismissal, + onRequestClose, + ]); + + const rootTag = useContext(RootTagContext); + + const onDismissIos = useCallback(() => { + // OnDismiss is implemented on iOS only. if (Platform.OS === 'ios') { - return this.props.visible === true || this.state.isRendered === true; + setIsRendered(false); + onDismiss?.(); } + }, [onDismiss]); - return this.props.visible === true; - } - - render(): React.Node { - if (!this._shouldShowModal()) { - return null; - } - - // Only override backgroundColor when transparent or backdropColor are - // explicitly set, so that these Modal-specific props take precedence - // over the generic style prop. The default backgroundColor ('white') - // is defined in styles.container below. - const containerStyles: {backgroundColor?: ColorValue} = {}; - if (this.props.transparent === true) { - containerStyles.backgroundColor = 'transparent'; - } else if (this.props.backdropColor != null) { - containerStyles.backgroundColor = this.props.backdropColor; - } - - let animationType = this.props.animationType || 'none'; - - let presentationStyle = this.props.presentationStyle; - if (!presentationStyle) { - presentationStyle = 'fullScreen'; - if (this.props.transparent === true) { - presentationStyle = 'overFullScreen'; - } - } - - const innerChildren = __DEV__ ? ( - {this.props.children} - ) : ( - this.props.children - ); - - const onDismiss = () => { - // OnDismiss is implemented on iOS only. - if (Platform.OS === 'ios') { - this.setState({isRendered: false}, () => { - if (this.props.onDismiss) { - this.props.onDismiss(); - } - }); - } - }; + const shouldShowModal = + Platform.OS === 'ios' ? isRendered && isVisible : isVisible; - return ( - - - - - {innerChildren} - - - - - ); + if (!shouldShowModal) { + return null; } - // We don't want any responder events bubbling out of the modal. - _shouldSetResponder(): boolean { - return true; + const isTransparent = transparent === true; + + // Only override backgroundColor when transparent or backdropColor are + // explicitly set, so that these Modal-specific props take precedence + // over the generic style prop. The default backgroundColor ('white') + // is defined in styles.container below. + const containerStyles: {backgroundColor?: ColorValue} = {}; + if (isTransparent) { + containerStyles.backgroundColor = 'transparent'; + } else if (backdropColor != null) { + containerStyles.backgroundColor = backdropColor; } -} + + return ( + + + + + {__DEV__ ? ( + {children} + ) : ( + children + )} + + + + + ); +}; const side = I18nManager.getConstants().isRTL ? 'right' : 'left'; const styles = StyleSheet.create({ @@ -387,25 +386,9 @@ const styles = StyleSheet.create({ }, }); -type ModalRefProps = Readonly<{ - ref?: React.RefSetter, -}>; - -// NOTE: This wrapper component is necessary because `Modal` is a class -// component and we need to map `ref` to a differently named prop. This can be -// removed when `Modal` is a functional component. -function Wrapper({ - ref, - ...props -}: { - ...ModalRefProps, - ...ModalProps, -}): React.Node { - return ; -} +Modal.displayName = 'Modal'; -Wrapper.displayName = 'Modal'; // $FlowExpectedError[prop-missing] -Wrapper.Context = VirtualizedListContextResetter; +Modal.Context = VirtualizedListContextResetter; -export default Wrapper; +export default Modal; diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 74c407136ff1..f2a8fb6d50ef 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<3043bde0b577699e10a24eb52b462a92>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -273,7 +273,12 @@ declare const MemoedPressable_default: ( ref?: React.Ref }, ) => React.ReactNode -declare const Modal: typeof Wrapper_default +declare const Modal: typeof Modal_default +declare const Modal_default: ( + props: ModalProps & { + ref?: React.Ref + }, +) => React.ReactNode declare const modulo: typeof $$AnimatedImplementation.modulo declare const moduloImpl: ( a: AnimatedNode_default, @@ -3046,9 +3051,6 @@ declare type ModalPropsIOS = { | "portrait" > } -declare type ModalRefProps = { - readonly ref?: React.Ref -} declare type ModeChangeEvent = Readonly< Omit & { mode: VirtualViewMode @@ -5760,9 +5762,6 @@ declare type WithDefault< Type extends DefaultTypes, Value extends (null | Type | undefined) | string, > = null | Type | undefined -declare function Wrapper_default( - $$PARAM_0$$: ModalRefProps & ModalProps, -): React.ReactNode declare type WrapperComponentProvider = ( appParameters?: AppParameters, appKey?: string, @@ -5906,7 +5905,7 @@ export { MeasureInWindowOnSuccessCallback, // a285f598 MeasureLayoutOnSuccessCallback, // 3592502a MeasureOnSuccessCallback, // 82824e59 - Modal, // f4b88be5 + Modal, // bfb4a780 ModalBaseProps, // a6e4334f ModalInstance, // 0f04c1bf ModalProps, // c07a247a