Back out "Send Modal onDismiss event on iOS (Fabric) and Android" @bypass-github-export-checks

Summary:
~~Original commit changeset: f419164032c3

Original Phabricator Diff: D52445670

bypass-github-export-checks

Changelog: [Internal]

Reviewed By: makovkastar

Differential Revision: D52932743

fbshipit-source-id: ea37270998213de0ae732477e0fb99b47aae7cd5
This commit is contained in:
Erica Klein
2024-01-20 05:57:48 -08:00
committed by Facebook GitHub Bot
parent 92b889b89e
commit a58ec074b6
17 changed files with 185 additions and 101 deletions
+5 -4
View File
@@ -43,10 +43,6 @@ export interface ModalBaseProps {
* The `onShow` prop allows passing a function that will be called once the modal has been shown.
*/
onShow?: ((event: NativeSyntheticEvent<any>) => 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 {
@@ -74,6 +70,11 @@ 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.
+53 -17
View File
@@ -12,7 +12,10 @@ 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';
@@ -22,14 +25,34 @@ 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<ModalEventDefinitions>(
// 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
// <Modal> 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',
|}>;
@@ -136,10 +159,6 @@ export type Props = $ReadOnly<{|
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,
|}>;
type State = {|
isRendering: boolean,
|};
function confirmProps(props: Props) {
if (__DEV__) {
if (
@@ -154,7 +173,7 @@ function confirmProps(props: Props) {
}
}
class Modal extends React.Component<Props, State> {
class Modal extends React.Component<Props> {
static defaultProps: {|hardwareAccelerated: boolean, visible: boolean|} = {
visible: true,
hardwareAccelerated: false,
@@ -162,27 +181,45 @@ class Modal extends React.Component<Props, State> {
static contextType: React.Context<RootTag> = RootTagContext;
_identifier: number;
_eventSubscription: ?EventSubscription;
constructor(props: Props) {
super(props);
this.state = {
isRendering: props.visible === true,
};
if (__DEV__) {
confirmProps(props);
}
this._identifier = uniqueModalIdentifier++;
}
componentDidUpdate(prevProps: Props) {
if (prevProps.visible !== true && this.props.visible === true) {
this.setState({isRendering: true});
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() {
if (__DEV__) {
confirmProps(this.props);
}
}
render(): React.Node {
if (this.props.visible !== true && !this.state.isRendering) {
if (this.props.visible !== true) {
return null;
}
@@ -216,14 +253,13 @@ class Modal extends React.Component<Props, State> {
onRequestClose={this.props.onRequestClose}
onShow={this.props.onShow}
onDismiss={() => {
this.setState({isRendering: false}, () => {
if (this.props.onDismiss) {
this.props.onDismiss();
}
});
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}
@@ -0,0 +1,21 @@
/**
* 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<Spec>('ModalManager'): ?Spec);
@@ -10,7 +10,11 @@
import type {ViewProps} from '../Components/View/ViewPropTypes';
import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
import type {DirectEventHandler, WithDefault} from '../Types/CodegenTypes';
import type {
DirectEventHandler,
Int32,
WithDefault,
} from '../Types/CodegenTypes';
import codegenNativeComponent from '../Utilities/codegenNativeComponent';
@@ -122,6 +126,11 @@ type NativeProps = $ReadOnly<{|
* See https://reactnative.dev/docs/modal#onorientationchange
*/
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,
/**
* The `identifier` is the unique number for identifying Modal components.
*/
identifier?: WithDefault<Int32, 0>,
|}>;
export default (codegenNativeComponent<NativeProps>('ModalHostView', {
@@ -13,6 +13,7 @@ exports[`<Modal /> should render as <RCTModalHostView> when not mocked 1`] = `
<RCTModalHostView
animationType="none"
hardwareAccelerated={false}
identifier={3}
onDismiss={[Function]}
onStartShouldSetResponder={[Function]}
presentationStyle="fullScreen"
@@ -6449,6 +6449,15 @@ exports[`public API should not change unintentionally Libraries/Modal/ModalInjec
"
`;
exports[`public API should not change unintentionally Libraries/Modal/NativeModalManager.js 1`] = `
"export interface Spec extends TurboModule {
+addListener: (eventName: string) => 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\\",
@@ -6479,6 +6488,7 @@ type NativeProps = $ReadOnly<{|
\\"portrait\\",
>,
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,
identifier?: WithDefault<Int32, 0>,
|}>;
declare export default HostComponent<NativeProps>;
"
@@ -8,6 +8,7 @@
#import "RCTModalHostViewComponentView.h"
#import <React/RCTBridge+Private.h>
#import <React/RCTModalManager.h>
#import <React/UIView+React.h>
#import <react/renderer/components/modal/ModalHostViewComponentDescriptor.h>
#import <react/renderer/components/modal/ModalHostViewState.h>
@@ -23,7 +23,6 @@
@property (nonatomic, assign, getter=isTransparent) BOOL transparent;
@property (nonatomic, copy) RCTDirectEventBlock onShow;
@property (nonatomic, copy) RCTDirectEventBlock onDismiss;
@property (nonatomic, assign) BOOL visible;
// Android only
@@ -31,11 +30,16 @@
@property (nonatomic, assign) BOOL hardwareAccelerated;
@property (nonatomic, assign) BOOL animated;
@property (nonatomic, copy) NSNumber *identifier;
@property (nonatomic, weak) id<RCTModalHostViewInteractor> delegate;
@property (nonatomic, copy) NSArray<NSString *> *supportedOrientations;
@property (nonatomic, copy) RCTDirectEventBlock onOrientationChange;
// Fabric only
@property (nonatomic, copy) RCTDirectEventBlock onDismiss;
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
@end
@@ -10,6 +10,7 @@
#import "RCTBridge.h"
#import "RCTModalHostView.h"
#import "RCTModalHostViewController.h"
#import "RCTModalManager.h"
#import "RCTShadowView.h"
#import "RCTUtils.h"
@@ -90,8 +91,8 @@ RCT_EXPORT_MODULE()
animated:(BOOL)animated
{
dispatch_block_t completionBlock = ^{
if (modalHostView.onDismiss) {
modalHostView.onDismiss(nil);
if (modalHostView.identifier) {
[[self.bridge moduleForClass:[RCTModalManager class]] modalDismissed:modalHostView.identifier];
}
};
dispatch_async(dispatch_get_main_queue(), ^{
@@ -123,10 +124,13 @@ RCT_EXPORT_VIEW_PROPERTY(statusBarTranslucent, BOOL)
RCT_EXPORT_VIEW_PROPERTY(hardwareAccelerated, BOOL)
RCT_EXPORT_VIEW_PROPERTY(animated, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onShow, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(identifier, NSNumber)
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
@@ -0,0 +1,17 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface RCTModalManager : RCTEventEmitter <RCTBridgeModule>
- (void)modalDismissed:(NSNumber *)modalID;
@end
@@ -0,0 +1,42 @@
/*
* 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<NSString *> *)supportedEvents
{
return @[ @"modalDismissed" ];
}
- (void)startObserving
{
_shouldEmit = YES;
}
- (void)stopObserving
{
_shouldEmit = NO;
}
- (void)modalDismissed:(NSNumber *)modalID
{
if (_shouldEmit) {
[self sendEventWithName:@"modalDismissed" body:@{@"modalID" : modalID}];
}
}
@end
@@ -5776,6 +5776,7 @@ public abstract interface class com/facebook/react/viewmanagers/ModalHostViewMan
public abstract fun setAnimated (Landroid/view/View;Z)V
public abstract fun setAnimationType (Landroid/view/View;Ljava/lang/String;)V
public abstract fun setHardwareAccelerated (Landroid/view/View;Z)V
public abstract fun setIdentifier (Landroid/view/View;I)V
public abstract fun setPresentationStyle (Landroid/view/View;Ljava/lang/String;)V
public abstract fun setStatusBarTranslucent (Landroid/view/View;Z)V
public abstract fun setSupportedOrientations (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V
@@ -6118,6 +6119,8 @@ public class com/facebook/react/views/modal/ReactModalHostManager : com/facebook
public fun setAnimationType (Lcom/facebook/react/views/modal/ReactModalHostView;Ljava/lang/String;)V
public synthetic fun setHardwareAccelerated (Landroid/view/View;Z)V
public fun setHardwareAccelerated (Lcom/facebook/react/views/modal/ReactModalHostView;Z)V
public synthetic fun setIdentifier (Landroid/view/View;I)V
public fun setIdentifier (Lcom/facebook/react/views/modal/ReactModalHostView;I)V
public synthetic fun setPresentationStyle (Landroid/view/View;Ljava/lang/String;)V
public fun setPresentationStyle (Lcom/facebook/react/views/modal/ReactModalHostView;Ljava/lang/String;)V
public synthetic fun setStatusBarTranslucent (Landroid/view/View;Z)V
@@ -6158,14 +6161,11 @@ public class com/facebook/react/views/modal/ReactModalHostView : android/view/Vi
public fun removeViewAt (I)V
protected fun setAnimationType (Ljava/lang/String;)V
protected fun setHardwareAccelerated (Z)V
protected fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V
protected fun setOnRequestCloseListener (Lcom/facebook/react/views/modal/ReactModalHostView$OnRequestCloseListener;)V
protected fun setOnShowListener (Landroid/content/DialogInterface$OnShowListener;)V
public fun setStateWrapper (Lcom/facebook/react/uimanager/StateWrapper;)V
protected fun setStatusBarTranslucent (Z)V
protected fun setTransparent (Z)V
protected fun setVisible (Z)V
protected fun showOrDismiss ()V
protected fun showOrUpdate ()V
public fun updateState (II)V
}
@@ -1,40 +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.
*/
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<DismissEvent> {
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();
}
}
@@ -95,7 +95,7 @@ public class ReactModalHostManager extends ViewGroupManager<ReactModalHostView>
@Override
@ReactProp(name = "visible")
public void setVisible(ReactModalHostView view, boolean visible) {
view.setVisible(visible);
// iOS only
}
@Override
@@ -110,6 +110,10 @@ public class ReactModalHostManager extends ViewGroupManager<ReactModalHostView>
@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) {
@@ -132,14 +136,6 @@ public class ReactModalHostManager extends ViewGroupManager<ReactModalHostView>
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);
}
}
@@ -154,6 +150,7 @@ public class ReactModalHostManager extends ViewGroupManager<ReactModalHostView>
MapBuilder.<String, Object>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"))
@@ -164,7 +161,7 @@ public class ReactModalHostManager extends ViewGroupManager<ReactModalHostView>
@Override
protected void onAfterUpdateTransaction(ReactModalHostView view) {
super.onAfterUpdateTransaction(view);
view.showOrDismiss();
view.showOrUpdate();
}
@Override
@@ -81,13 +81,11 @@ public class ReactModalHostView extends ViewGroup implements LifecycleEventListe
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) {
@@ -194,10 +192,6 @@ public class ReactModalHostView extends ViewGroup implements LifecycleEventListe
mOnShowListener = listener;
}
protected void setOnDismissListener(DialogInterface.OnDismissListener listener) {
mOnDismissListener = listener;
}
protected void setTransparent(boolean transparent) {
mTransparent = transparent;
}
@@ -217,10 +211,6 @@ public class ReactModalHostView extends ViewGroup implements LifecycleEventListe
mPropertyRequiresNewDialog = true;
}
protected void setVisible(boolean visible) {
mVisible = visible;
}
void setEventDispatcher(EventDispatcher eventDispatcher) {
mHostView.setEventDispatcher(eventDispatcher);
}
@@ -304,7 +294,6 @@ public class ReactModalHostView extends ViewGroup implements LifecycleEventListe
updateProperties();
mDialog.setOnShowListener(mOnShowListener);
mDialog.setOnDismissListener(mOnDismissListener);
mDialog.setOnKeyListener(
new DialogInterface.OnKeyListener() {
@Override
@@ -345,14 +334,6 @@ public class ReactModalHostView extends ViewGroup implements LifecycleEventListe
}
}
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.
@@ -133,6 +133,6 @@ export default ({
title: "Modal's onShow/onDismiss",
name: 'onShow',
description:
'onShow and onDismiss callbacks are called when a modal is shown/dismissed',
'onShow and onDismiss (iOS only) callbacks are called when a modal is shown/dismissed',
render: (): React.Node => <ModalOnShowOnDismiss />,
}: RNTesterModuleExample);
@@ -199,8 +199,8 @@ function ModalPresentation() {
<RNTOption
key="onDismiss"
style={styles.option}
label="onDismiss"
disabled={false}
label="onDismiss ⚫️"
disabled={Platform.OS !== 'ios'}
onPress={() =>
setProps(prev => ({
...prev,