Skip to content

Commit

Permalink
Send Modal onDismiss event on iOS (Fabric) and Android (#42014)
Browse files Browse the repository at this point in the history
Summary:
1. Modal onDismiss is not working on iOS (Fabric).
2. Modal onDismiss is currently only available on iOS. On Android, we don't have a way to know when exactly a modal is dismissed.

Currently, the onDismiss is emitted using a device event as a workaround to the RCTModalHostView unable to receive the component event as it's already unmounted when visible is false.

This PR removes the workaround and keeps RCTModalHostView mounted until the onDismiss event is emitted from the host and sends the onDismiss event on Android.

bypass-github-export-checks

## Changelog:
[ANDROID] [ADDED] - Added support for Modal onDismiss prop
[IOS] [FIXED] - Fix onDismiss is not working on Fabric
[General][Breaking] - The public API of Modal has changed. We don't have anymore a NativeModalManger turbomodule; RCTModalHostViewNtiveComponent's Prop does not require to pass an identifier anymore.

Pull Request resolved: #42014

Test Plan:
1. Run rn-tester
2. Open the Modal example
3. The second example shows the counter for the show and dismiss count
4. Show and dismiss the modal and verify the count is incremented correctly

https://github.com/facebook/react-native/assets/50919443/108bfb26-c8f6-43b2-ac40-f0b46e48771b

Reviewed By: javache, sammy-SC

Differential Revision: D52445670

Pulled By: cipolleschi

fbshipit-source-id: f419164032c3bef67387200778b274299bf0659f
  • Loading branch information
bernhardoj authored and facebook-github-bot committed Jan 16, 2024
1 parent b311c3e commit 314cfa8
Show file tree
Hide file tree
Showing 16 changed files with 98 additions and 182 deletions.
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) {
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 @@ -6449,15 +6449,6 @@ 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\\",
Expand Down Expand Up @@ -6488,7 +6479,6 @@ type NativeProps = $ReadOnly<{|
\\"portrait\\",
>,
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,
identifier?: WithDefault<Int32, 0>,
|}>;
declare export default HostComponent<NativeProps>;
"
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.

Loading

0 comments on commit 314cfa8

Please sign in to comment.