diff --git a/packages/react-native/Libraries/Modal/Modal.d.ts b/packages/react-native/Libraries/Modal/Modal.d.ts index 4cc2df22367b95..1b035876efd994 100644 --- a/packages/react-native/Libraries/Modal/Modal.d.ts +++ b/packages/react-native/Libraries/Modal/Modal.d.ts @@ -43,6 +43,10 @@ export interface ModalBaseProps { * The `onShow` prop allows passing a function that will be called once the modal has been shown. */ onShow?: ((event: NativeSyntheticEvent) => void) | undefined; + /** + * The `onDismiss` prop allows passing a function that will be called once the modal has been dismissed. + */ + onDismiss?: (() => void) | undefined; } export interface ModalPropsIOS { @@ -70,11 +74,6 @@ export interface ModalPropsIOS { > | undefined; - /** - * The `onDismiss` prop allows passing a function that will be called once the modal has been dismissed. - */ - onDismiss?: (() => void) | undefined; - /** * The `onOrientationChange` callback is called when the orientation changes while the modal is being displayed. * The orientation provided is only 'portrait' or 'landscape'. This callback is also called on initial render, regardless of the current orientation. diff --git a/packages/react-native/Libraries/Modal/Modal.js b/packages/react-native/Libraries/Modal/Modal.js index 9750d2e5be31d3..44e70a73a61601 100644 --- a/packages/react-native/Libraries/Modal/Modal.js +++ b/packages/react-native/Libraries/Modal/Modal.js @@ -12,10 +12,7 @@ import type {ViewProps} from '../Components/View/ViewPropTypes'; import type {RootTag} from '../ReactNative/RootTag'; import type {DirectEventHandler} from '../Types/CodegenTypes'; -import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; -import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import ModalInjection from './ModalInjection'; -import NativeModalManager from './NativeModalManager'; import RCTModalHostView from './RCTModalHostViewNativeComponent'; import {VirtualizedListContextResetter} from '@react-native/virtualized-lists'; @@ -25,34 +22,14 @@ const AppContainer = require('../ReactNative/AppContainer'); const I18nManager = require('../ReactNative/I18nManager'); const {RootTagContext} = require('../ReactNative/RootTag'); const StyleSheet = require('../StyleSheet/StyleSheet'); -const Platform = require('../Utilities/Platform'); const React = require('react'); -type ModalEventDefinitions = { - modalDismissed: [{modalID: number}], -}; - -const ModalEventEmitter = - Platform.OS === 'ios' && NativeModalManager != null - ? new NativeEventEmitter( - // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior - // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeModalManager, - ) - : null; - /** * The Modal component is a simple way to present content above an enclosing view. * * See https://reactnative.dev/docs/modal */ -// In order to route onDismiss callbacks, we need to uniquely identifier each -// on screen. There can be different ones, either nested or as siblings. -// We cannot pass the onDismiss callback to native as the view will be -// destroyed before the callback is fired. -let uniqueModalIdentifier = 0; - type OrientationChangeEvent = $ReadOnly<{| orientation: 'portrait' | 'landscape', |}>; @@ -159,6 +136,10 @@ export type Props = $ReadOnly<{| onOrientationChange?: ?DirectEventHandler, |}>; +type State = {| + isRendering: boolean, +|}; + function confirmProps(props: Props) { if (__DEV__) { if ( @@ -173,7 +154,7 @@ function confirmProps(props: Props) { } } -class Modal extends React.Component { +class Modal extends React.Component { static defaultProps: {|hardwareAccelerated: boolean, visible: boolean|} = { visible: true, hardwareAccelerated: false, @@ -181,45 +162,27 @@ class Modal extends React.Component { static contextType: React.Context = RootTagContext; - _identifier: number; - _eventSubscription: ?EventSubscription; - constructor(props: Props) { super(props); + this.state = { + isRendering: props.visible === true, + }; if (__DEV__) { confirmProps(props); } - this._identifier = uniqueModalIdentifier++; } - componentDidMount() { - // 'modalDismissed' is for the old renderer in iOS only - if (ModalEventEmitter) { - this._eventSubscription = ModalEventEmitter.addListener( - 'modalDismissed', - event => { - if (event.modalID === this._identifier && this.props.onDismiss) { - this.props.onDismiss(); - } - }, - ); - } - } - - componentWillUnmount() { - if (this._eventSubscription) { - this._eventSubscription.remove(); + componentDidUpdate(prevProps: Props) { + if (prevProps.visible !== true && this.props.visible === true) { + this.setState({isRendering: true}); } - } - - componentDidUpdate() { if (__DEV__) { confirmProps(this.props); } } render(): React.Node { - if (this.props.visible !== true) { + if (this.props.visible !== true && !this.state.isRendering) { return null; } @@ -253,13 +216,14 @@ class Modal extends React.Component { onRequestClose={this.props.onRequestClose} onShow={this.props.onShow} onDismiss={() => { - if (this.props.onDismiss) { - this.props.onDismiss(); - } + this.setState({isRendering: false}, () => { + if (this.props.onDismiss) { + this.props.onDismiss(); + } + }); }} visible={this.props.visible} statusBarTranslucent={this.props.statusBarTranslucent} - identifier={this._identifier} style={styles.modal} // $FlowFixMe[method-unbinding] added when improving typing for this parameters onStartShouldSetResponder={this._shouldSetResponder} diff --git a/packages/react-native/Libraries/Modal/NativeModalManager.js b/packages/react-native/Libraries/Modal/NativeModalManager.js deleted file mode 100644 index f85a77ac4b5ce7..00000000000000 --- a/packages/react-native/Libraries/Modal/NativeModalManager.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict - * @format - */ - -import type {TurboModule} from '../TurboModule/RCTExport'; - -import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; - -export interface Spec extends TurboModule { - // RCTEventEmitter - +addListener: (eventName: string) => void; - +removeListeners: (count: number) => void; -} - -export default (TurboModuleRegistry.get('ModalManager'): ?Spec); diff --git a/packages/react-native/Libraries/Modal/RCTModalHostViewNativeComponent.js b/packages/react-native/Libraries/Modal/RCTModalHostViewNativeComponent.js index a21af54ae4fe72..ea80fa8ff5bb1d 100644 --- a/packages/react-native/Libraries/Modal/RCTModalHostViewNativeComponent.js +++ b/packages/react-native/Libraries/Modal/RCTModalHostViewNativeComponent.js @@ -10,11 +10,7 @@ import type {ViewProps} from '../Components/View/ViewPropTypes'; import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; -import type { - DirectEventHandler, - Int32, - WithDefault, -} from '../Types/CodegenTypes'; +import type {DirectEventHandler, WithDefault} from '../Types/CodegenTypes'; import codegenNativeComponent from '../Utilities/codegenNativeComponent'; @@ -126,11 +122,6 @@ type NativeProps = $ReadOnly<{| * See https://reactnative.dev/docs/modal#onorientationchange */ onOrientationChange?: ?DirectEventHandler, - - /** - * The `identifier` is the unique number for identifying Modal components. - */ - identifier?: WithDefault, |}>; export default (codegenNativeComponent('ModalHostView', { diff --git a/packages/react-native/Libraries/Modal/__tests__/__snapshots__/Modal-test.js.snap b/packages/react-native/Libraries/Modal/__tests__/__snapshots__/Modal-test.js.snap index 5c2f5c57f07640..9a98b1d43c8050 100644 --- a/packages/react-native/Libraries/Modal/__tests__/__snapshots__/Modal-test.js.snap +++ b/packages/react-native/Libraries/Modal/__tests__/__snapshots__/Modal-test.js.snap @@ -13,7 +13,6 @@ exports[` should render as when not mocked 1`] = ` void; - +removeListeners: (count: number) => void; -} -declare export default ?Spec; -" -`; - exports[`public API should not change unintentionally Libraries/Modal/RCTModalHostViewNativeComponent.js 1`] = ` "type OrientationChangeEvent = $ReadOnly<{| orientation: \\"portrait\\" | \\"landscape\\", @@ -6488,7 +6479,6 @@ type NativeProps = $ReadOnly<{| \\"portrait\\", >, onOrientationChange?: ?DirectEventHandler, - identifier?: WithDefault, |}>; declare export default HostComponent; " diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm index e20eee608a01c6..d814fa0b91d752 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm @@ -8,7 +8,6 @@ #import "RCTModalHostViewComponentView.h" #import -#import #import #import #import diff --git a/packages/react-native/React/Views/RCTModalHostView.h b/packages/react-native/React/Views/RCTModalHostView.h index 2fcdcaea83f5b2..67f6de08b3596d 100644 --- a/packages/react-native/React/Views/RCTModalHostView.h +++ b/packages/react-native/React/Views/RCTModalHostView.h @@ -23,6 +23,7 @@ @property (nonatomic, assign, getter=isTransparent) BOOL transparent; @property (nonatomic, copy) RCTDirectEventBlock onShow; +@property (nonatomic, copy) RCTDirectEventBlock onDismiss; @property (nonatomic, assign) BOOL visible; // Android only @@ -30,16 +31,11 @@ @property (nonatomic, assign) BOOL hardwareAccelerated; @property (nonatomic, assign) BOOL animated; -@property (nonatomic, copy) NSNumber *identifier; - @property (nonatomic, weak) id delegate; @property (nonatomic, copy) NSArray *supportedOrientations; @property (nonatomic, copy) RCTDirectEventBlock onOrientationChange; -// Fabric only -@property (nonatomic, copy) RCTDirectEventBlock onDismiss; - - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; @end diff --git a/packages/react-native/React/Views/RCTModalHostViewManager.m b/packages/react-native/React/Views/RCTModalHostViewManager.m index 4b9f9ad7267c8f..c3ebd189f1d073 100644 --- a/packages/react-native/React/Views/RCTModalHostViewManager.m +++ b/packages/react-native/React/Views/RCTModalHostViewManager.m @@ -10,7 +10,6 @@ #import "RCTBridge.h" #import "RCTModalHostView.h" #import "RCTModalHostViewController.h" -#import "RCTModalManager.h" #import "RCTShadowView.h" #import "RCTUtils.h" @@ -91,8 +90,8 @@ - (void)dismissModalHostView:(RCTModalHostView *)modalHostView animated:(BOOL)animated { dispatch_block_t completionBlock = ^{ - if (modalHostView.identifier) { - [[self.bridge moduleForClass:[RCTModalManager class]] modalDismissed:modalHostView.identifier]; + if (modalHostView.onDismiss) { + modalHostView.onDismiss(nil); } }; dispatch_async(dispatch_get_main_queue(), ^{ @@ -124,13 +123,10 @@ - (void)invalidate RCT_EXPORT_VIEW_PROPERTY(hardwareAccelerated, BOOL) RCT_EXPORT_VIEW_PROPERTY(animated, BOOL) RCT_EXPORT_VIEW_PROPERTY(onShow, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(identifier, NSNumber) +RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(supportedOrientations, NSArray) RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(visible, BOOL) RCT_EXPORT_VIEW_PROPERTY(onRequestClose, RCTDirectEventBlock) -// Fabric only -RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock) - @end diff --git a/packages/react-native/React/Views/RCTModalManager.h b/packages/react-native/React/Views/RCTModalManager.h deleted file mode 100644 index 237037fd8db6b2..00000000000000 --- a/packages/react-native/React/Views/RCTModalManager.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import -#import - -@interface RCTModalManager : RCTEventEmitter - -- (void)modalDismissed:(NSNumber *)modalID; - -@end diff --git a/packages/react-native/React/Views/RCTModalManager.m b/packages/react-native/React/Views/RCTModalManager.m deleted file mode 100644 index 85ddb29b149036..00000000000000 --- a/packages/react-native/React/Views/RCTModalManager.m +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import "RCTModalManager.h" - -@interface RCTModalManager () - -@property BOOL shouldEmit; - -@end - -@implementation RCTModalManager - -RCT_EXPORT_MODULE(); - -- (NSArray *)supportedEvents -{ - return @[ @"modalDismissed" ]; -} - -- (void)startObserving -{ - _shouldEmit = YES; -} - -- (void)stopObserving -{ - _shouldEmit = NO; -} - -- (void)modalDismissed:(NSNumber *)modalID -{ - if (_shouldEmit) { - [self sendEventWithName:@"modalDismissed" body:@{@"modalID" : modalID}]; - } -} - -@end diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/DismissEvent.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/DismissEvent.java new file mode 100644 index 00000000000000..2b2aeb712647be --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/DismissEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.modal; + +import androidx.annotation.Nullable; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.common.ViewUtil; +import com.facebook.react.uimanager.events.Event; + +/** {@link Event} for dismissing a Dialog. */ +/* package */ class DismissEvent extends Event { + + public static final String EVENT_NAME = "topDismiss"; + + @Deprecated + protected DismissEvent(int viewTag) { + this(ViewUtil.NO_SURFACE_ID, viewTag); + } + + protected DismissEvent(int surfaceId, int viewTag) { + super(surfaceId, viewTag); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Nullable + @Override + protected WritableMap getEventData() { + return Arguments.createMap(); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java index 74c6b8e707c123..241cc550c8a78c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java @@ -95,7 +95,7 @@ public void setHardwareAccelerated(ReactModalHostView view, boolean hardwareAcce @Override @ReactProp(name = "visible") public void setVisible(ReactModalHostView view, boolean visible) { - // iOS only + view.setVisible(visible); } @Override @@ -110,10 +110,6 @@ public void setAnimated(ReactModalHostView view, boolean value) {} @ReactProp(name = "supportedOrientations") public void setSupportedOrientations(ReactModalHostView view, @Nullable ReadableArray value) {} - @Override - @ReactProp(name = "identifier") - public void setIdentifier(ReactModalHostView view, int value) {} - @Override protected void addEventEmitters( final ThemedReactContext reactContext, final ReactModalHostView view) { @@ -136,6 +132,14 @@ public void onShow(DialogInterface dialog) { new ShowEvent(UIManagerHelper.getSurfaceId(reactContext), view.getId())); } }); + view.setOnDismissListener( + new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(@Nullable DialogInterface dialog) { + dispatcher.dispatchEvent( + new DismissEvent(UIManagerHelper.getSurfaceId(reactContext), view.getId())); + } + }); view.setEventDispatcher(dispatcher); } } @@ -150,7 +154,6 @@ public Map getExportedCustomDirectEventTypeConstants() { MapBuilder.builder() .put(RequestCloseEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRequestClose")) .put(ShowEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShow")) - // iOS only .put("topDismiss", MapBuilder.of("registrationName", "onDismiss")) // iOS only .put("topOrientationChange", MapBuilder.of("registrationName", "onOrientationChange")) @@ -161,7 +164,7 @@ public Map getExportedCustomDirectEventTypeConstants() { @Override protected void onAfterUpdateTransaction(ReactModalHostView view) { super.onAfterUpdateTransaction(view); - view.showOrUpdate(); + view.showOrDismiss(); } @Override diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java index 2ae3bb9bda3c1b..f9fb24b6a03395 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java @@ -81,11 +81,13 @@ public interface OnRequestCloseListener { private boolean mStatusBarTranslucent; private String mAnimationType; private boolean mHardwareAccelerated; + private boolean mVisible; // Set this flag to true if changing a particular property on the view requires a new Dialog to // be created. For instance, animation does since it affects Dialog creation through the theme // but transparency does not since we can access the window to update the property. private boolean mPropertyRequiresNewDialog; private @Nullable DialogInterface.OnShowListener mOnShowListener; + private @Nullable DialogInterface.OnDismissListener mOnDismissListener; private @Nullable OnRequestCloseListener mOnRequestCloseListener; public ReactModalHostView(ThemedReactContext context) { @@ -192,6 +194,10 @@ protected void setOnShowListener(DialogInterface.OnShowListener listener) { mOnShowListener = listener; } + protected void setOnDismissListener(DialogInterface.OnDismissListener listener) { + mOnDismissListener = listener; + } + protected void setTransparent(boolean transparent) { mTransparent = transparent; } @@ -211,6 +217,10 @@ protected void setHardwareAccelerated(boolean hardwareAccelerated) { mPropertyRequiresNewDialog = true; } + protected void setVisible(boolean visible) { + mVisible = visible; + } + void setEventDispatcher(EventDispatcher eventDispatcher) { mHostView.setEventDispatcher(eventDispatcher); } @@ -294,6 +304,7 @@ protected void showOrUpdate() { updateProperties(); mDialog.setOnShowListener(mOnShowListener); + mDialog.setOnDismissListener(mOnDismissListener); mDialog.setOnKeyListener( new DialogInterface.OnKeyListener() { @Override @@ -334,6 +345,14 @@ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { } } + protected void showOrDismiss() { + if (mVisible) { + showOrUpdate(); + } else { + dismiss(); + } + } + /** * Returns the view that will be the root view of the dialog. We are wrapping this in a * FrameLayout because this is the system's way of notifying us that the dialog size has changed. diff --git a/packages/rn-tester/js/examples/Modal/ModalOnShow.js b/packages/rn-tester/js/examples/Modal/ModalOnShow.js index 8ef9764794a570..ce9bc50c6a9a72 100644 --- a/packages/rn-tester/js/examples/Modal/ModalOnShow.js +++ b/packages/rn-tester/js/examples/Modal/ModalOnShow.js @@ -133,6 +133,6 @@ export default ({ title: "Modal's onShow/onDismiss", name: 'onShow', description: - 'onShow and onDismiss (iOS only) callbacks are called when a modal is shown/dismissed', + 'onShow and onDismiss callbacks are called when a modal is shown/dismissed', render: (): React.Node => , }: RNTesterModuleExample); diff --git a/packages/rn-tester/js/examples/Modal/ModalPresentation.js b/packages/rn-tester/js/examples/Modal/ModalPresentation.js index e5f2cad774ebf1..921cd7251d240a 100644 --- a/packages/rn-tester/js/examples/Modal/ModalPresentation.js +++ b/packages/rn-tester/js/examples/Modal/ModalPresentation.js @@ -199,8 +199,8 @@ function ModalPresentation() { setProps(prev => ({ ...prev,