1010
1111import type { HostInstance } from '../../src/private/types/HostInstance' ;
1212import type { ViewProps } from '../Components/View/ViewPropTypes' ;
13- import type { RootTag } from '../ReactNative/RootTag' ;
1413import type { DirectEventHandler } from '../Types/CodegenTypes' ;
1514
1615import NativeEventEmitter from '../EventEmitter/NativeEventEmitter' ;
1716import { type ColorValue } from '../StyleSheet/StyleSheet' ;
18- import { type EventSubscription } from '../vendor/emitter/EventEmitter' ;
1917import NativeModalManager from './NativeModalManager' ;
2018import RCTModalHostView from './RCTModalHostViewNativeComponent' ;
2119import VirtualizedLists from '@react-native/virtualized-lists' ;
2220import * as React from 'react' ;
21+ import { useCallback , useContext , useEffect , useState } from 'react' ;
2322
2423const ScrollView = require ( '../Components/ScrollView/ScrollView' ) . default ;
2524const View = require ( '../Components/View/View' ) . default ;
@@ -179,30 +178,34 @@ export type ModalProps = {
179178 ...ViewProps ,
180179} ;
181180
182- function confirmProps ( props : ModalProps ) {
181+ function confirmProps ( {
182+ transparent,
183+ presentationStyle,
184+ navigationBarTranslucent,
185+ statusBarTranslucent,
186+ allowSwipeDismissal,
187+ onRequestClose,
188+ } : ModalProps ) {
183189 if ( __DEV__ ) {
184190 if (
185- props . presentationStyle &&
186- props . presentationStyle !== 'overFullScreen' &&
187- props . transparent === true
191+ presentationStyle &&
192+ presentationStyle !== 'overFullScreen' &&
193+ transparent === true
188194 ) {
189195 console . warn (
190- `Modal with '${ props . presentationStyle } ' presentation style and 'transparent' value is not supported.` ,
196+ `Modal with '${ presentationStyle } ' presentation style and 'transparent' value is not supported.` ,
191197 ) ;
192198 }
193- if (
194- props . navigationBarTranslucent === true &&
195- props . statusBarTranslucent !== true
196- ) {
199+ if ( navigationBarTranslucent === true && statusBarTranslucent !== true ) {
197200 console . warn (
198201 'Modal with translucent navigation bar and without translucent status bar is not supported.' ,
199202 ) ;
200203 }
201204
202205 if (
203206 Platform . OS === 'ios' &&
204- props . allowSwipeDismissal === true &&
205- ! props . onRequestClose
207+ allowSwipeDismissal === true &&
208+ ! onRequestClose
206209 ) {
207210 console . warn (
208211 'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.' ,
@@ -211,163 +214,161 @@ function confirmProps(props: ModalProps) {
211214 }
212215}
213216
214- // Create a state to track whether the Modal is rendering or not.
215- // This is the only prop that controls whether the modal is rendered or not.
216- type ModalState = {
217- isRendered : boolean ,
218- } ;
219-
220- class Modal extends React . Component < ModalProps , ModalState > {
221- static defaultProps : { hardwareAccelerated : boolean , visible : boolean } = {
222- visible : true ,
223- hardwareAccelerated : false ,
224- } ;
217+ const onStartShouldSetResponder = ( ) => true ; // We don't want any responder events bubbling out of the modal.
225218
226- static contextType : React . Context < RootTag > = RootTagContext ;
219+ const Modal : component (
220+ ref ?: React . RefSetter < PublicModalInstance > ,
221+ ...props : ModalProps
222+ ) = ( {
223+ backdropColor,
224+ transparent,
225+ children,
226+ presentationStyle,
227+ animationType,
228+ onRequestClose,
229+ onShow,
230+ visible = true ,
231+ hardwareAccelerated = false ,
232+ ref : modalRef ,
233+ statusBarTranslucent,
234+ navigationBarTranslucent,
235+ supportedOrientations,
236+ onOrientationChange,
237+ allowSwipeDismissal,
238+ testID,
239+ onDismiss,
240+ } : {
241+ ref ?: React . RefSetter < PublicModalInstance > ,
242+ ...ModalProps ,
243+ } ) : React . Node => {
244+ const isVisible = visible === true ;
227245
228- _identifier : number ;
229- _eventSubscription : ?EventSubscription ;
246+ // Create a state to track whether the Modal is rendering or not.
247+ // This is the only prop that controls whether the modal is rendered or not.
248+ const [ isRendered , setIsRendered ] = useState ( visible === true ) ;
249+ const [ identifier ] = useState ( ( ) => uniqueModalIdentifier ++ ) ;
230250
231- constructor ( props : ModalProps ) {
232- super ( props ) ;
233- if ( __DEV__ ) {
234- confirmProps ( props ) ;
251+ useEffect ( ( ) => {
252+ if ( ! ModalEventEmitter ) {
253+ return ;
235254 }
236- this . _identifier = uniqueModalIdentifier ++ ;
237- this . state = {
238- isRendered : props . visible === true ,
239- } ;
240- }
241255
242- componentDidMount ( ) {
243- // 'modalDismissed' is for the old renderer in iOS only
244- if ( ModalEventEmitter ) {
245- this . _eventSubscription = ModalEventEmitter . addListener (
246- 'modalDismissed' ,
247- event => {
248- this . setState ( { isRendered : false } , ( ) => {
249- if ( event . modalID === this . _identifier && this . props . onDismiss ) {
250- this . props . onDismiss ( ) ;
251- }
252- } ) ;
253- } ,
254- ) ;
255- }
256- }
256+ const eventSubscription = ModalEventEmitter . addListener (
257+ 'modalDismissed' ,
258+ event => {
259+ setIsRendered ( false ) ;
260+ if ( event . modalID === identifier ) {
261+ onDismiss ?. ( ) ;
262+ }
263+ } ,
264+ ) ;
265+ return ( ) => eventSubscription . remove ( ) ;
266+ } , [ onDismiss , identifier ] ) ;
257267
258- componentWillUnmount ( ) {
259- if ( Platform . OS === 'ios' ) {
260- this . setState ( { isRendered : false } ) ;
261- }
262- if ( this . _eventSubscription ) {
263- this . _eventSubscription . remove ( ) ;
268+ useEffect ( ( ) => {
269+ if ( isVisible ) {
270+ setIsRendered ( true ) ;
264271 }
265- }
272+ } , [ isVisible ] ) ;
266273
267- componentDidUpdate ( prevProps : ModalProps ) {
268- if ( prevProps . visible === false && this . props . visible === true ) {
269- this . setState ( { isRendered : true } ) ;
270- }
274+ useEffect ( ( ) => {
275+ return ( ) => {
276+ setIsRendered ( false ) ;
277+ } ;
278+ } , [ ] ) ;
271279
280+ useEffect ( ( ) => {
272281 if ( __DEV__ ) {
273- confirmProps ( this . props ) ;
282+ confirmProps ( {
283+ transparent,
284+ presentationStyle,
285+ navigationBarTranslucent,
286+ statusBarTranslucent,
287+ allowSwipeDismissal,
288+ onRequestClose,
289+ } ) ;
274290 }
275- }
276-
277- // Helper function to encapsulate platform specific logic to show or not the Modal.
278- _shouldShowModal ( ) : boolean {
291+ } , [
292+ transparent ,
293+ presentationStyle ,
294+ navigationBarTranslucent ,
295+ statusBarTranslucent ,
296+ allowSwipeDismissal ,
297+ onRequestClose ,
298+ ] ) ;
299+
300+ const rootTag = useContext ( RootTagContext ) ;
301+
302+ const onDismissIos = useCallback ( ( ) => {
303+ // OnDismiss is implemented on iOS only.
279304 if ( Platform . OS === 'ios' ) {
280- return this . props . visible === true || this . state . isRendered === true ;
305+ setIsRendered ( false ) ;
306+ onDismiss ?. ( ) ;
281307 }
308+ } , [ onDismiss ] ) ;
282309
283- return this . props . visible === true ;
284- }
310+ const shouldShowModal =
311+ Platform . OS === 'ios' ? isRendered && isVisible : isVisible ;
285312
286- render ( ) : React . Node {
287- if ( ! this . _shouldShowModal ( ) ) {
288- return null ;
289- }
290-
291- // Only override backgroundColor when transparent or backdropColor are
292- // explicitly set, so that these Modal-specific props take precedence
293- // over the generic style prop. The default backgroundColor ('white')
294- // is defined in styles.container below.
295- const containerStyles : { backgroundColor ?: ColorValue } = { } ;
296- if ( this . props . transparent === true ) {
297- containerStyles . backgroundColor = 'transparent' ;
298- } else if ( this . props . backdropColor != null ) {
299- containerStyles . backgroundColor = this . props . backdropColor ;
300- }
301-
302- let animationType = this . props . animationType || 'none' ;
303-
304- let presentationStyle = this . props . presentationStyle ;
305- if ( ! presentationStyle ) {
306- presentationStyle = 'fullScreen' ;
307- if ( this . props . transparent === true ) {
308- presentationStyle = 'overFullScreen' ;
309- }
310- }
311-
312- const innerChildren = __DEV__ ? (
313- < AppContainer rootTag = { this . context } > { this . props . children } </ AppContainer >
314- ) : (
315- this . props . children
316- ) ;
317-
318- const onDismiss = ( ) => {
319- // OnDismiss is implemented on iOS only.
320- if ( Platform . OS === 'ios' ) {
321- this . setState ( { isRendered : false } , ( ) => {
322- if ( this . props . onDismiss ) {
323- this . props . onDismiss ( ) ;
324- }
325- } ) ;
326- }
327- } ;
328-
329- return (
330- < RCTModalHostView
331- /* $FlowFixMe[incompatible-type] Natural Inference rollout. See
332- * https://fburl.com/workplace/6291gfvu */
333- animationType = { animationType }
334- presentationStyle = { presentationStyle }
335- transparent = { this . props . transparent }
336- hardwareAccelerated = { this . props . hardwareAccelerated }
337- onRequestClose = { this . props . onRequestClose }
338- onShow = { this . props . onShow }
339- onDismiss = { onDismiss }
340- ref = { this . props . modalRef }
341- visible = { this . props . visible }
342- statusBarTranslucent = { this . props . statusBarTranslucent }
343- navigationBarTranslucent = { this . props . navigationBarTranslucent }
344- identifier = { this . _identifier }
345- style = { styles . modal }
346- // $FlowFixMe[method-unbinding] added when improving typing for this parameters
347- onStartShouldSetResponder = { this . _shouldSetResponder }
348- supportedOrientations = { this . props . supportedOrientations }
349- onOrientationChange = { this . props . onOrientationChange }
350- allowSwipeDismissal = { this . props . allowSwipeDismissal }
351- testID = { this . props . testID } >
352- < VirtualizedListContextResetter >
353- < ScrollView . Context . Provider value = { null } >
354- < View
355- // $FlowFixMe[incompatible-type]
356- style = { [ styles . container , this . props . style , containerStyles ] }
357- collapsable = { false } >
358- { innerChildren }
359- </ View >
360- </ ScrollView . Context . Provider >
361- </ VirtualizedListContextResetter >
362- </ RCTModalHostView >
363- ) ;
313+ if ( ! shouldShowModal ) {
314+ return null ;
364315 }
365316
366- // We don't want any responder events bubbling out of the modal.
367- _shouldSetResponder ( ) : boolean {
368- return true ;
317+ const isTransparent = transparent === true ;
318+
319+ // Only override backgroundColor when transparent or backdropColor are
320+ // explicitly set, so that these Modal-specific props take precedence
321+ // over the generic style prop. The default backgroundColor ('white')
322+ // is defined in styles.container below.
323+ const containerStyles = { } ;
324+ if ( this . props . transparent === true ) {
325+ containerStyles . backgroundColor = 'transparent' ;
326+ } else if ( this . props . backdropColor != null ) {
327+ containerStyles . backgroundColor = this . props . backdropColor ;
369328 }
370- }
329+
330+ return (
331+ < RCTModalHostView
332+ /* $FlowFixMe[incompatible-type] Natural Inference rollout. See
333+ * https://fburl.com/workplace/6291gfvu */
334+ animationType = { animationType || 'none' }
335+ presentationStyle = {
336+ presentationStyle || ( isTransparent ? 'overFullScreen' : 'fullScreen' )
337+ }
338+ transparent = { transparent }
339+ hardwareAccelerated = { hardwareAccelerated }
340+ onRequestClose = { onRequestClose }
341+ onShow = { onShow }
342+ onDismiss = { onDismissIos }
343+ ref = { modalRef }
344+ visible = { visible }
345+ statusBarTranslucent = { statusBarTranslucent }
346+ navigationBarTranslucent = { navigationBarTranslucent }
347+ identifier = { identifier }
348+ style = { styles . modal }
349+ // $FlowFixMe[method-unbinding] added when improving typing for this parameters
350+ onStartShouldSetResponder = { onStartShouldSetResponder }
351+ supportedOrientations = { supportedOrientations }
352+ onOrientationChange = { onOrientationChange }
353+ allowSwipeDismissal = { allowSwipeDismissal }
354+ testID = { testID } >
355+ < VirtualizedListContextResetter >
356+ < ScrollView . Context . Provider value = { null } >
357+ < View
358+ // $FlowFixMe[incompatible-type]
359+ style = { [ styles . container , this . props . style , containerStyles ] }
360+ collapsable = { false } >
361+ { __DEV__ ? (
362+ < AppContainer rootTag = { rootTag } > { children } </ AppContainer >
363+ ) : (
364+ children
365+ ) }
366+ </ View >
367+ </ ScrollView . Context . Provider >
368+ </ VirtualizedListContextResetter >
369+ </ RCTModalHostView >
370+ ) ;
371+ } ;
371372
372373const side = I18nManager . getConstants ( ) . isRTL ? 'right' : 'left' ;
373374const styles = StyleSheet . create ( {
@@ -388,25 +389,9 @@ const styles = StyleSheet.create({
388389 } ,
389390} ) ;
390391
391- type ModalRefProps = Readonly < {
392- ref ?: React . RefSetter < PublicModalInstance > ,
393- } > ;
394-
395- // NOTE: This wrapper component is necessary because `Modal` is a class
396- // component and we need to map `ref` to a differently named prop. This can be
397- // removed when `Modal` is a functional component.
398- function Wrapper ( {
399- ref,
400- ...props
401- } : {
402- ...ModalRefProps ,
403- ...ModalProps ,
404- } ) : React . Node {
405- return < Modal { ...props } modalRef = { ref } /> ;
406- }
392+ Modal . displayName = 'Modal' ;
407393
408- Wrapper . displayName = 'Modal' ;
409394// $FlowExpectedError[prop-missing]
410- Wrapper . Context = VirtualizedListContextResetter ;
395+ Modal . Context = VirtualizedListContextResetter ;
411396
412- export default Wrapper ;
397+ export default Modal ;
0 commit comments