Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send Modal onDismiss event on iOS (Fabric) and Android #42014

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions packages/react-native/Libraries/Modal/Modal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 {
Expand Down Expand Up @@ -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.
Expand Down
70 changes: 17 additions & 53 deletions packages/react-native/Libraries/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<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',
|}>;
Expand Down Expand Up @@ -159,6 +136,10 @@ export type Props = $ReadOnly<{|
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,
|}>;

type State = {|
isRendering: boolean,
|};

function confirmProps(props: Props) {
if (__DEV__) {
if (
Expand All @@ -173,53 +154,35 @@ function confirmProps(props: Props) {
}
}

class Modal extends React.Component<Props> {
class Modal extends React.Component<Props, State> {
static defaultProps: {|hardwareAccelerated: boolean, visible: boolean|} = {
visible: true,
hardwareAccelerated: false,
};

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++;
}

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of `isRendering?

isRendering is mapped to value of visible prop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isRendering is being used to keep the Modal rendered until the onDismiss callback is called.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the parent component unmounts Modal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, onDismiss won't be called.

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;
}

Expand Down Expand Up @@ -253,13 +216,14 @@ class Modal extends React.Component<Props> {
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}
Expand Down
21 changes: 0 additions & 21 deletions packages/react-native/Libraries/Modal/NativeModalManager.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -126,11 +122,6 @@ 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', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ exports[`<Modal /> should render as <RCTModalHostView> when not mocked 1`] = `
<RCTModalHostView
animationType="none"
hardwareAccelerated={false}
identifier={3}
onDismiss={[Function]}
onStartShouldSetResponder={[Function]}
presentationStyle="fullScreen"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
#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>
Expand Down
6 changes: 1 addition & 5 deletions packages/react-native/React/Views/RCTModalHostView.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,19 @@
@property (nonatomic, assign, getter=isTransparent) BOOL transparent;

@property (nonatomic, copy) RCTDirectEventBlock onShow;
@property (nonatomic, copy) RCTDirectEventBlock onDismiss;
@property (nonatomic, assign) BOOL visible;

// Android only
@property (nonatomic, assign) BOOL statusBarTranslucent;
@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
Expand Down
10 changes: 3 additions & 7 deletions packages/react-native/React/Views/RCTModalHostViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#import "RCTBridge.h"
#import "RCTModalHostView.h"
#import "RCTModalHostViewController.h"
#import "RCTModalManager.h"
#import "RCTShadowView.h"
#import "RCTUtils.h"

Expand Down Expand Up @@ -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(), ^{
Expand Down Expand Up @@ -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
17 changes: 0 additions & 17 deletions packages/react-native/React/Views/RCTModalManager.h

This file was deleted.

42 changes: 0 additions & 42 deletions packages/react-native/React/Views/RCTModalManager.m

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<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();
}
}
Loading
Loading