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 ;
@@ -183,30 +182,34 @@ export type ModalProps = {
183182 ...ViewProps ,
184183} ;
185184
186- function confirmProps ( props : ModalProps ) {
185+ function confirmProps ( {
186+ transparent,
187+ presentationStyle,
188+ navigationBarTranslucent,
189+ statusBarTranslucent,
190+ allowSwipeDismissal,
191+ onRequestClose,
192+ } : ModalProps ) {
187193 if ( __DEV__ ) {
188194 if (
189- props . presentationStyle &&
190- props . presentationStyle !== 'overFullScreen' &&
191- props . transparent === true
195+ presentationStyle &&
196+ presentationStyle !== 'overFullScreen' &&
197+ transparent === true
192198 ) {
193199 console . warn (
194- `Modal with '${ props . presentationStyle } ' presentation style and 'transparent' value is not supported.` ,
200+ `Modal with '${ presentationStyle } ' presentation style and 'transparent' value is not supported.` ,
195201 ) ;
196202 }
197- if (
198- props . navigationBarTranslucent === true &&
199- props . statusBarTranslucent !== true
200- ) {
203+ if ( navigationBarTranslucent === true && statusBarTranslucent !== true ) {
201204 console . warn (
202205 'Modal with translucent navigation bar and without translucent status bar is not supported.' ,
203206 ) ;
204207 }
205208
206209 if (
207210 Platform . OS === 'ios' &&
208- props . allowSwipeDismissal === true &&
209- ! props . onRequestClose
211+ allowSwipeDismissal === true &&
212+ ! onRequestClose
210213 ) {
211214 console . warn (
212215 'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.' ,
@@ -215,163 +218,161 @@ function confirmProps(props: ModalProps) {
215218 }
216219}
217220
218- // Create a state to track whether the Modal is rendering or not.
219- // This is the only prop that controls whether the modal is rendered or not.
220- type ModalState = {
221- isRendered : boolean ,
222- } ;
223-
224- class Modal extends React . Component < ModalProps , ModalState > {
225- static defaultProps : { hardwareAccelerated : boolean , visible : boolean } = {
226- visible : true ,
227- hardwareAccelerated : false ,
228- } ;
221+ const onStartShouldSetResponder = ( ) => true ; // We don't want any responder events bubbling out of the modal.
229222
230- static contextType : React . Context < RootTag > = RootTagContext ;
223+ const Modal : component (
224+ ref ?: React . RefSetter < ModalInstance > ,
225+ ...props : ModalProps
226+ ) = ( {
227+ backdropColor,
228+ transparent,
229+ children,
230+ presentationStyle,
231+ animationType,
232+ onRequestClose,
233+ onShow,
234+ visible = true ,
235+ hardwareAccelerated = false ,
236+ ref : modalRef ,
237+ statusBarTranslucent,
238+ navigationBarTranslucent,
239+ supportedOrientations,
240+ onOrientationChange,
241+ allowSwipeDismissal,
242+ testID,
243+ onDismiss,
244+ } : {
245+ ref ?: React . RefSetter < PublicModalInstance > ,
246+ ...ModalProps ,
247+ } ) : React . Node => {
248+ const isVisible = visible === true ;
231249
232- _identifier : number ;
233- _eventSubscription : ?EventSubscription ;
250+ // Create a state to track whether the Modal is rendering or not.
251+ // This is the only prop that controls whether the modal is rendered or not.
252+ const [ isRendered , setIsRendered ] = useState ( visible === true ) ;
253+ const [ identifier ] = useState ( ( ) => uniqueModalIdentifier ++ ) ;
234254
235- constructor ( props : ModalProps ) {
236- super ( props ) ;
237- if ( __DEV__ ) {
238- confirmProps ( props ) ;
255+ useEffect ( ( ) => {
256+ if ( ! ModalEventEmitter ) {
257+ return ;
239258 }
240- this . _identifier = uniqueModalIdentifier ++ ;
241- this . state = {
242- isRendered : props . visible === true ,
243- } ;
244- }
245259
246- componentDidMount ( ) {
247- // 'modalDismissed' is for the old renderer in iOS only
248- if ( ModalEventEmitter ) {
249- this . _eventSubscription = ModalEventEmitter . addListener (
250- 'modalDismissed' ,
251- event => {
252- this . setState ( { isRendered : false } , ( ) => {
253- if ( event . modalID === this . _identifier && this . props . onDismiss ) {
254- this . props . onDismiss ( ) ;
255- }
256- } ) ;
257- } ,
258- ) ;
259- }
260- }
260+ const eventSubscription = ModalEventEmitter . addListener (
261+ 'modalDismissed' ,
262+ event => {
263+ setIsRendered ( false ) ;
264+ if ( event . modalID === identifier ) {
265+ onDismiss ?. ( ) ;
266+ }
267+ } ,
268+ ) ;
269+ return ( ) => eventSubscription . remove ( ) ;
270+ } , [ onDismiss , identifier ] ) ;
261271
262- componentWillUnmount ( ) {
263- if ( Platform . OS === 'ios' ) {
264- this . setState ( { isRendered : false } ) ;
272+ useEffect ( ( ) => {
273+ if ( isVisible ) {
274+ setIsRendered ( true ) ;
265275 }
266- if ( this . _eventSubscription ) {
267- this . _eventSubscription . remove ( ) ;
268- }
269- }
276+ } , [ isVisible ] ) ;
270277
271- componentDidUpdate ( prevProps : ModalProps ) {
272- if ( prevProps . visible === false && this . props . visible === true ) {
273- this . setState ( { isRendered : true } ) ;
274- }
278+ useEffect ( ( ) => {
279+ return ( ) => {
280+ setIsRendered ( false ) ;
281+ } ;
282+ } , [ ] ) ;
275283
284+ useEffect ( ( ) => {
276285 if ( __DEV__ ) {
277- confirmProps ( this . props ) ;
286+ confirmProps ( {
287+ transparent,
288+ presentationStyle,
289+ navigationBarTranslucent,
290+ statusBarTranslucent,
291+ allowSwipeDismissal,
292+ onRequestClose,
293+ } ) ;
278294 }
279- }
280-
281- // Helper function to encapsulate platform specific logic to show or not the Modal.
282- _shouldShowModal ( ) : boolean {
295+ } , [
296+ transparent ,
297+ presentationStyle ,
298+ navigationBarTranslucent ,
299+ statusBarTranslucent ,
300+ allowSwipeDismissal ,
301+ onRequestClose ,
302+ ] ) ;
303+
304+ const rootTag = useContext ( RootTagContext ) ;
305+
306+ const onDismissIos = useCallback ( ( ) => {
307+ // OnDismiss is implemented on iOS only.
283308 if ( Platform . OS === 'ios' ) {
284- return this . props . visible === true || this . state . isRendered === true ;
285- }
286-
287- return this . props . visible === true ;
288- }
289-
290- render ( ) : React . Node {
291- if ( ! this . _shouldShowModal ( ) ) {
292- return null ;
293- }
294-
295- // Only override backgroundColor when transparent or backdropColor are
296- // explicitly set, so that these Modal-specific props take precedence
297- // over the generic style prop. The default backgroundColor ('white')
298- // is defined in styles.container below.
299- const containerStyles : { backgroundColor ?: ColorValue } = { } ;
300- if ( this . props . transparent === true ) {
301- containerStyles . backgroundColor = 'transparent' ;
302- } else if ( this . props . backdropColor != null ) {
303- containerStyles . backgroundColor = this . props . backdropColor ;
304- }
305-
306- let animationType = this . props . animationType || 'none' ;
307-
308- let presentationStyle = this . props . presentationStyle ;
309- if ( ! presentationStyle ) {
310- presentationStyle = 'fullScreen' ;
311- if ( this . props . transparent === true ) {
312- presentationStyle = 'overFullScreen' ;
313- }
309+ setIsRendered ( false ) ;
310+ onDismiss ?. ( ) ;
314311 }
312+ } , [ onDismiss ] ) ;
315313
316- const innerChildren = __DEV__ ? (
317- < AppContainer rootTag = { this . context } > { this . props . children } </ AppContainer >
318- ) : (
319- this . props . children
320- ) ;
321-
322- const onDismiss = ( ) => {
323- // OnDismiss is implemented on iOS only.
324- if ( Platform . OS === 'ios' ) {
325- this . setState ( { isRendered : false } , ( ) => {
326- if ( this . props . onDismiss ) {
327- this . props . onDismiss ( ) ;
328- }
329- } ) ;
330- }
331- } ;
314+ const shouldShowModal =
315+ Platform . OS === 'ios' ? isRendered && isVisible : isVisible ;
332316
333- return (
334- < RCTModalHostView
335- /* $FlowFixMe[incompatible-type] Natural Inference rollout. See
336- * https://fburl.com/workplace/6291gfvu */
337- animationType = { animationType }
338- presentationStyle = { presentationStyle }
339- transparent = { this . props . transparent }
340- hardwareAccelerated = { this . props . hardwareAccelerated }
341- onRequestClose = { this . props . onRequestClose }
342- onShow = { this . props . onShow }
343- onDismiss = { onDismiss }
344- ref = { this . props . modalRef }
345- visible = { this . props . visible }
346- statusBarTranslucent = { this . props . statusBarTranslucent }
347- navigationBarTranslucent = { this . props . navigationBarTranslucent }
348- identifier = { this . _identifier }
349- style = { styles . modal }
350- // $FlowFixMe[method-unbinding] added when improving typing for this parameters
351- onStartShouldSetResponder = { this . _shouldSetResponder }
352- supportedOrientations = { this . props . supportedOrientations }
353- onOrientationChange = { this . props . onOrientationChange }
354- allowSwipeDismissal = { this . props . allowSwipeDismissal }
355- testID = { this . props . testID } >
356- < VirtualizedListContextResetter >
357- < ScrollView . Context . Provider value = { null } >
358- < View
359- // $FlowFixMe[incompatible-type]
360- style = { [ styles . container , this . props . style , containerStyles ] }
361- collapsable = { false } >
362- { innerChildren }
363- </ View >
364- </ ScrollView . Context . Provider >
365- </ VirtualizedListContextResetter >
366- </ RCTModalHostView >
367- ) ;
317+ if ( ! shouldShowModal ) {
318+ return null ;
368319 }
369320
370- // We don't want any responder events bubbling out of the modal.
371- _shouldSetResponder ( ) : boolean {
372- return true ;
321+ const isTransparent = transparent === true ;
322+
323+ // Only override backgroundColor when transparent or backdropColor are
324+ // explicitly set, so that these Modal-specific props take precedence
325+ // over the generic style prop. The default backgroundColor ('white')
326+ // is defined in styles.container below.
327+ const containerStyles = { } ;
328+ if ( this . props . transparent === true ) {
329+ containerStyles . backgroundColor = 'transparent' ;
330+ } else if ( this . props . backdropColor != null ) {
331+ containerStyles . backgroundColor = this . props . backdropColor ;
373332 }
374- }
333+
334+ return (
335+ < RCTModalHostView
336+ /* $FlowFixMe[incompatible-type] Natural Inference rollout. See
337+ * https://fburl.com/workplace/6291gfvu */
338+ animationType = { animationType || 'none' }
339+ presentationStyle = {
340+ presentationStyle || ( isTransparent ? 'overFullScreen' : 'fullScreen' )
341+ }
342+ transparent = { transparent }
343+ hardwareAccelerated = { hardwareAccelerated }
344+ onRequestClose = { onRequestClose }
345+ onShow = { onShow }
346+ onDismiss = { onDismissIos }
347+ ref = { modalRef }
348+ visible = { visible }
349+ statusBarTranslucent = { statusBarTranslucent }
350+ navigationBarTranslucent = { navigationBarTranslucent }
351+ identifier = { identifier }
352+ style = { styles . modal }
353+ // $FlowFixMe[method-unbinding] added when improving typing for this parameters
354+ onStartShouldSetResponder = { onStartShouldSetResponder }
355+ supportedOrientations = { supportedOrientations }
356+ onOrientationChange = { onOrientationChange }
357+ allowSwipeDismissal = { allowSwipeDismissal }
358+ testID = { testID } >
359+ < VirtualizedListContextResetter >
360+ < ScrollView . Context . Provider value = { null } >
361+ < View
362+ // $FlowFixMe[incompatible-type]
363+ style = { [ styles . container , this . props . style , containerStyles ] }
364+ collapsable = { false } >
365+ { __DEV__ ? (
366+ < AppContainer rootTag = { rootTag } > { children } </ AppContainer >
367+ ) : (
368+ children
369+ ) }
370+ </ View >
371+ </ ScrollView . Context . Provider >
372+ </ VirtualizedListContextResetter >
373+ </ RCTModalHostView >
374+ ) ;
375+ } ;
375376
376377const side = I18nManager . getConstants ( ) . isRTL ? 'right' : 'left' ;
377378const styles = StyleSheet . create ( {
@@ -392,25 +393,9 @@ const styles = StyleSheet.create({
392393 } ,
393394} ) ;
394395
395- type ModalRefProps = Readonly < {
396- ref ?: React . RefSetter < ModalInstance > ,
397- } > ;
398-
399- // NOTE: This wrapper component is necessary because `Modal` is a class
400- // component and we need to map `ref` to a differently named prop. This can be
401- // removed when `Modal` is a functional component.
402- function Wrapper ( {
403- ref,
404- ...props
405- } : {
406- ...ModalRefProps ,
407- ...ModalProps ,
408- } ) : React . Node {
409- return < Modal { ...props } modalRef = { ref } /> ;
410- }
396+ Modal . displayName = 'Modal' ;
411397
412- Wrapper . displayName = 'Modal' ;
413398// $FlowExpectedError[prop-missing]
414- Wrapper . Context = VirtualizedListContextResetter ;
399+ Modal . Context = VirtualizedListContextResetter ;
415400
416- export default Wrapper ;
401+ export default Modal ;
0 commit comments