From edfe35829166da67ee4c3e3655db2283b703b058 Mon Sep 17 00:00:00 2001 From: Tatiana Kapos Date: Mon, 18 Nov 2024 16:38:24 -0800 Subject: [PATCH 1/3] add onDismiss event --- .../Modal/ModalPresentation.windows.js | 2 +- .../WindowsModalHostViewComponentView.cpp | 13 + vnext/overrides.json | 6 + .../src-win/Libraries/Modal/Modal.windows.js | 351 ++++++++++++++++++ 4 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 vnext/src-win/Libraries/Modal/Modal.windows.js diff --git a/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js index aa78521cffd..779d42b8d76 100644 --- a/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js +++ b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js @@ -212,7 +212,7 @@ function ModalPresentation() { key="onDismiss" style={styles.option} label="onDismiss ⚫️" - disabled={Platform.OS !== 'ios'} + disabled={Platform.OS !== 'ios' && Platform.OS !== 'windows'} onPress={() => setProps(prev => ({ ...prev, diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp index c4aa52a9a16..01854bdacc7 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp @@ -34,6 +34,11 @@ WindowsModalHostComponentView::WindowsModalHostComponentView( : Super(compContext, tag, reactContext) {} WindowsModalHostComponentView::~WindowsModalHostComponentView() { + // dispatch onDismiss event + auto emitter = std::static_pointer_cast(m_eventEmitter); + facebook::react::ModalHostViewEventEmitter::OnDismiss onDismissArgs; + emitter->onDismiss(onDismissArgs); + // reset the topWindowID if (m_prevWindowID) { auto host = @@ -170,6 +175,11 @@ void WindowsModalHostComponentView::HideOnUIThread() noexcept { SendMessage(m_hwnd, WM_CLOSE, 0, 0); } + // dispatch onDismiss event + auto emitter = std::static_pointer_cast(m_eventEmitter); + facebook::react::ModalHostViewEventEmitter::OnDismiss onDismissArgs; + emitter->onDismiss(onDismissArgs); + // enable input to parent EnableWindow(m_parentHwnd, true); @@ -326,6 +336,9 @@ void WindowsModalHostComponentView::updateProps( *std::static_pointer_cast(oldProps ? oldProps : viewProps()); const auto &newModalProps = *std::static_pointer_cast(props); newModalProps.visible ? m_isVisible = true : m_isVisible = false; + if (!m_isVisible) { + HideOnUIThread(); + } base_type::updateProps(props, oldProps); } diff --git a/vnext/overrides.json b/vnext/overrides.json index 9c37dd9dd3b..4cd049b1b8c 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -510,6 +510,12 @@ "baseFile": "packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js", "baseHash": "663d3325298404d7c012a6aa53e833eb5fc2ec76" }, + { + "type": "derived", + "file": "src-win/Libraries/Modal/Modal.windows.js", + "baseFile": "packages/react-native/Libraries/Modal/Modal.js", + "baseHash": "6aa04ce4c8fb0c43298fa64f35be8b8b8e21e93f" + }, { "type": "derived", "file": "src-win/Libraries/NativeComponent/BaseViewConfig.windows.js", diff --git a/vnext/src-win/Libraries/Modal/Modal.windows.js b/vnext/src-win/Libraries/Modal/Modal.windows.js new file mode 100644 index 00000000000..2507ff0cc1b --- /dev/null +++ b/vnext/src-win/Libraries/Modal/Modal.windows.js @@ -0,0 +1,351 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +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'; + +const ScrollView = require('../Components/ScrollView/ScrollView'); +const View = require('../Components/View/View'); +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' || Platform.OS === 'windows') && // [Windows] + 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' && Platform.OS !== 'windows' // [Windows] + ? 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', +|}>; + +export type Props = $ReadOnly<{| + ...ViewProps, + + /** + * The `animationType` prop controls how the modal animates. + * + * See https://reactnative.dev/docs/modal#animationtype + */ + animationType?: ?('none' | 'slide' | 'fade'), + + /** + * The `presentationStyle` prop controls how the modal appears. + * + * See https://reactnative.dev/docs/modal#presentationstyle + */ + presentationStyle?: ?( + | 'fullScreen' + | 'pageSheet' + | 'formSheet' + | 'overFullScreen' + ), + + /** + * The `transparent` prop determines whether your modal will fill the + * entire view. + * + * See https://reactnative.dev/docs/modal#transparent + */ + transparent?: ?boolean, + + /** + * The `statusBarTranslucent` prop determines whether your modal should go under + * the system statusbar. + * + * See https://reactnative.dev/docs/modal.html#statusbartranslucent-android + */ + statusBarTranslucent?: ?boolean, + + /** + * The `hardwareAccelerated` prop controls whether to force hardware + * acceleration for the underlying window. + * + * This prop works only on Android. + * + * See https://reactnative.dev/docs/modal#hardwareaccelerated + */ + hardwareAccelerated?: ?boolean, + + /** + * The `visible` prop determines whether your modal is visible. + * + * See https://reactnative.dev/docs/modal#visible + */ + visible?: ?boolean, + + /** + * The `onRequestClose` callback is called when the user taps the hardware + * back button on Android or the menu button on Apple TV. + * + * This is required on Apple TV and Android. + * + * See https://reactnative.dev/docs/modal#onrequestclose + */ + onRequestClose?: ?DirectEventHandler, + + /** + * The `onShow` prop allows passing a function that will be called once the + * modal has been shown. + * + * See https://reactnative.dev/docs/modal#onshow + */ + onShow?: ?DirectEventHandler, + + /** + * The `onDismiss` prop allows passing a function that will be called once + * the modal has been dismissed. + * + * See https://reactnative.dev/docs/modal#ondismiss + */ + onDismiss?: ?() => mixed, + + /** + * The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations. + * + * See https://reactnative.dev/docs/modal#supportedorientations + */ + supportedOrientations?: ?$ReadOnlyArray< + | 'portrait' + | 'portrait-upside-down' + | 'landscape' + | 'landscape-left' + | 'landscape-right', + >, + + /** + * The `onOrientationChange` callback is called when the orientation changes while the modal is being displayed. + * + * See https://reactnative.dev/docs/modal#onorientationchange + */ + onOrientationChange?: ?DirectEventHandler, + + /** + * The `backdropColor` props sets the background color of the modal's container. + * Defaults to `white` if not provided and transparent is `false`. Ignored if `transparent` is `true`. + */ + backdropColor?: ?string, +|}>; + +function confirmProps(props: Props) { + if (__DEV__) { + if ( + props.presentationStyle && + props.presentationStyle !== 'overFullScreen' && + props.transparent === true + ) { + console.warn( + `Modal with '${props.presentationStyle}' presentation style and 'transparent' value is not supported.`, + ); + } + } +} + +// Create a state to track whether the Modal is rendering or not. +// This is the only prop that controls whether the modal is rendered or not. +type State = { + isRendered: boolean, +}; + +class Modal extends React.Component { + static defaultProps: {|hardwareAccelerated: boolean, visible: boolean|} = { + visible: true, + hardwareAccelerated: false, + }; + + static contextType: React.Context = RootTagContext; + + _identifier: number; + _eventSubscription: ?EventSubscription; + + constructor(props: Props) { + super(props); + if (__DEV__) { + confirmProps(props); + } + this._identifier = uniqueModalIdentifier++; + this.state = { + isRendered: props.visible === true, + }; + } + + componentDidMount() { + // 'modalDismissed' is for the old renderer in iOS only + if (ModalEventEmitter) { + this._eventSubscription = ModalEventEmitter.addListener( + 'modalDismissed', + event => { + this.setState({isRendered: false}, () => { + if (event.modalID === this._identifier && this.props.onDismiss) { + this.props.onDismiss(); + } + }); + }, + ); + } + } + + componentWillUnmount() { + if (this._eventSubscription) { + this._eventSubscription.remove(); + } + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.visible === false && this.props.visible === true) { + this.setState({isRendered: true}); + } + + if (__DEV__) { + confirmProps(this.props); + } + } + + // Helper function to encapsulate platform specific logic to show or not the Modal. + _shouldShowModal(): boolean { + if (Platform.OS === 'ios' || Platform.OS === 'windows') { + // [Windows] + return this.props.visible === true || this.state.isRendered === true; + } + + return this.props.visible === true; + } + + render(): React.Node { + if (!this._shouldShowModal()) { + return null; + } + + const containerStyles = { + backgroundColor: + this.props.transparent === true + ? 'transparent' + : this.props.backdropColor ?? 'white', + }; + + let animationType = this.props.animationType || 'none'; + + let presentationStyle = this.props.presentationStyle; + if (!presentationStyle) { + presentationStyle = 'fullScreen'; + if (this.props.transparent === true) { + presentationStyle = 'overFullScreen'; + } + } + + const innerChildren = __DEV__ ? ( + {this.props.children} + ) : ( + this.props.children + ); + + const onDismiss = () => { + // OnDismiss is implemented on iOS/Windows only. // [Windows] + if (Platform.OS === 'ios' || Platform.OS === 'windows') { + // [Windows] + this.setState({isRendered: false}, () => { + if (this.props.onDismiss) { + this.props.onDismiss(); + } + }); + } + }; + + return ( + + + + + {innerChildren} + + + + + ); + } + + // We don't want any responder events bubbling out of the modal. + _shouldSetResponder(): boolean { + return true; + } +} + +const side = I18nManager.getConstants().isRTL ? 'right' : 'left'; +const styles = StyleSheet.create({ + modal: { + position: 'absolute', + }, + container: { + /* $FlowFixMe[invalid-computed-prop] (>=0.111.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.111 was deployed. To see + * the error, delete this comment and run Flow. */ + [side]: 0, + top: 0, + flex: 1, + }, +}); + +const ExportedModal: React.AbstractComponent< + React.ElementConfig, +> = ModalInjection.unstable_Modal ?? Modal; + +module.exports = ExportedModal; From 6f4cefedc455b90464171556cb0a49a529ae4a55 Mon Sep 17 00:00:00 2001 From: Tatiana Kapos Date: Tue, 19 Nov 2024 13:53:03 -0800 Subject: [PATCH 2/3] remove title bar --- .../Composition/Modal/WindowsModalHostViewComponentView.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp index 01854bdacc7..f644824dae4 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp @@ -100,7 +100,7 @@ void WindowsModalHostComponentView::EnsureModalCreated() { m_hwnd = CreateWindow( c_modalWindowClassName, L"React-Native Modal", - WS_OVERLAPPEDWINDOW, + (WS_POPUP), CW_USEDEFAULT, CW_USEDEFAULT, MODAL_MIN_WIDTH, @@ -308,7 +308,6 @@ void WindowsModalHostComponentView::AdjustWindowSize() noexcept { RECT rc; GetClientRect(m_hwnd, &rc); RECT rect = {0, 0, (int)xPos, (int)yPos}; - AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); // Adjust for title bar and borders // set the layoutMetrics m_layoutMetrics.frame.size = {(float)rect.right - rect.left, (float)rect.bottom - rect.top}; From 0546fa0f3317b5aafce61730c1b9a16925e283ee Mon Sep 17 00:00:00 2001 From: Tatiana Kapos Date: Tue, 19 Nov 2024 13:53:40 -0800 Subject: [PATCH 3/3] Change files --- ...ative-windows-31db428a-029b-4735-94f6-2c4368b9bda6.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-31db428a-029b-4735-94f6-2c4368b9bda6.json diff --git a/change/react-native-windows-31db428a-029b-4735-94f6-2c4368b9bda6.json b/change/react-native-windows-31db428a-029b-4735-94f6-2c4368b9bda6.json new file mode 100644 index 00000000000..7b616fc952d --- /dev/null +++ b/change/react-native-windows-31db428a-029b-4735-94f6-2c4368b9bda6.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "implement onDismiss and remove titlebar from Modal", + "packageName": "react-native-windows", + "email": "tatianakapos@microsoft.com", + "dependentChangeType": "patch" +}