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..9d2be4b1868 --- /dev/null +++ b/change/react-native-windows-31db428a-029b-4735-94f6-2c4368b9bda6.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "implement onDismiss and remove titlebar from Modal", + "packageName": "react-native-windows", + "email": "tatianakapos@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/react-native-windows-4fec92b2-f049-43b1-8e13-d9c351c5511b.json b/change/react-native-windows-4fec92b2-f049-43b1-8e13-d9c351c5511b.json new file mode 100644 index 00000000000..44e2d5e1164 --- /dev/null +++ b/change/react-native-windows-4fec92b2-f049-43b1-8e13-d9c351c5511b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Implement cursor property", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/react-native-windows-e6508963-e86e-45b4-86e5-3f82b548e2f7.json b/change/react-native-windows-e6508963-e86e-45b4-86e5-3f82b548e2f7.json new file mode 100644 index 00000000000..a42e6f696ef --- /dev/null +++ b/change/react-native-windows-e6508963-e86e-45b4-86e5-3f82b548e2f7.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix Modal position, disable input to parent hwnd, and add onShow", + "packageName": "react-native-windows", + "email": "tatianakapos@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file 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 4ed2de275a8..fbae830d053 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 @@ -200,7 +200,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/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap index d881a738d9a..5439d25e9f7 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap @@ -23436,14 +23436,7 @@ exports[`snapshotAllPages Modal 1`] = ` }, null, null, - [ - { - "borderWidth": 0, - }, - { - "backgroundColor": "#7676801e", - }, - ], + null, { "marginRight": 8, "marginTop": 6, diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index ebaa245d1cf..004c260b203 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -12,15 +12,18 @@ #include #include #include +#include #include #include "Composition.Input.h" #include "CompositionViewComponentView.h" #include "ReactNativeIsland.h" #include "RootComponentView.h" -#ifdef USE_WINUI3 -#include -#endif +namespace ABI::Microsoft::UI::Input { +struct IInputCursor; +} + +#include namespace Microsoft::ReactNative { @@ -328,6 +331,11 @@ CompositionEventHandler::~CompositionEventHandler() { } } #endif + + if (m_hcursorOwned) { + ::DestroyCursor(m_hcursor); + m_hcursor = nullptr; + } } facebook::react::SurfaceId CompositionEventHandler::SurfaceId() const noexcept { @@ -507,6 +515,10 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w } break; } + case WM_SETCURSOR: { + UpdateCursor(); + return 1; + } } return 0; @@ -753,6 +765,151 @@ void CompositionEventHandler::HandleIncomingPointerEvent( hoveredViews.emplace_back(ReactTaggedView(componentViewDescriptor.view)); } m_currentlyHoveredViewsPerPointer[pointerId] = std::move(hoveredViews); + + if (IsMousePointerEvent(event)) { + UpdateCursor(); + } +} + +void CompositionEventHandler::UpdateCursor() noexcept { + for (auto &taggedView : m_currentlyHoveredViewsPerPointer[MOUSE_POINTER_ID]) { + if (auto view = taggedView.view()) { + if (auto viewcomponent = + view.try_as()) { + auto cursorInfo = viewcomponent->cursor(); + if (cursorInfo.first != facebook::react::Cursor::Auto || cursorInfo.second != nullptr) { + SetCursor(cursorInfo.first, cursorInfo.second); + return; + } + } + } + } + + SetCursor(facebook::react::Cursor::Auto, nullptr); +} + +void CompositionEventHandler::SetCursor(facebook::react::Cursor cursor, HCURSOR hcur) noexcept { + if (m_currentCursor == cursor && m_hcursor == hcur) + return; + + if (auto strongRootView = m_wkRootView.get()) { + if (auto island = strongRootView.Island()) { + auto pointerSource = winrt::Microsoft::UI::Input::InputPointerSource::GetForIsland(island); + + if (!hcur) { + winrt::Windows::UI::Core::CoreCursorType type = winrt::Windows::UI::Core::CoreCursorType::Arrow; + switch (cursor) { + case facebook::react::Cursor::Pointer: + type = winrt::Windows::UI::Core::CoreCursorType::Hand; + break; + case facebook::react::Cursor::Help: + type = winrt::Windows::UI::Core::CoreCursorType::Help; + break; + case facebook::react::Cursor::NotAllowed: + type = winrt::Windows::UI::Core::CoreCursorType::UniversalNo; + break; + case facebook::react::Cursor::Wait: + type = winrt::Windows::UI::Core::CoreCursorType::Wait; + break; + case facebook::react::Cursor::Move: + type = winrt::Windows::UI::Core::CoreCursorType::SizeAll; + break; + case facebook::react::Cursor::NESWResize: + type = winrt::Windows::UI::Core::CoreCursorType::SizeNortheastSouthwest; + break; + case facebook::react::Cursor::NSResize: + type = winrt::Windows::UI::Core::CoreCursorType::SizeNorthSouth; + break; + case facebook::react::Cursor::NWSEResize: + type = winrt::Windows::UI::Core::CoreCursorType::SizeNorthwestSoutheast; + break; + case facebook::react::Cursor::EWResize: + type = winrt::Windows::UI::Core::CoreCursorType::SizeWestEast; + break; + case facebook::react::Cursor::Text: + type = winrt::Windows::UI::Core::CoreCursorType::IBeam; + break; + case facebook::react::Cursor::Progress: + type = winrt::Windows::UI::Core::CoreCursorType::Wait; // IDC_APPSTARTING not mapped to CoreCursor? + break; + case facebook::react::Cursor::Crosshair: + type = winrt::Windows::UI::Core::CoreCursorType::Cross; + break; + default: + break; + } + + m_inputCursor = winrt::Microsoft::UI::Input::InputCursor::CreateFromCoreCursor( + winrt::Windows::UI::Core::CoreCursor(type, 0)); + m_hcursor = hcur; + } else { + auto cursorInterop = winrt::get_activation_factory< + winrt::Microsoft::UI::Input::InputCursor, + ABI::Microsoft::UI::Input::IInputCursorStaticsInterop>(); + winrt::com_ptr spunk; + winrt::check_hresult(cursorInterop->CreateFromHCursor( + hcur, reinterpret_cast(spunk.put_void()))); + m_hcursor = hcur; + m_inputCursor = spunk.as(); + } + + pointerSource.Cursor(m_inputCursor); + } else { + if (m_hcursorOwned) { + ::DestroyCursor(m_hcursor); + m_hcursorOwned = false; + } + if (hcur == nullptr) { + const WCHAR *idc = IDC_ARROW; + switch (cursor) { + case facebook::react::Cursor::Pointer: + idc = IDC_HAND; + break; + case facebook::react::Cursor::Help: + idc = IDC_HELP; + break; + case facebook::react::Cursor::NotAllowed: + idc = IDC_NO; + break; + case facebook::react::Cursor::Wait: + idc = IDC_WAIT; + break; + case facebook::react::Cursor::Move: + idc = IDC_SIZEALL; + break; + case facebook::react::Cursor::NESWResize: + idc = IDC_SIZENESW; + break; + case facebook::react::Cursor::NSResize: + idc = IDC_SIZENS; + break; + case facebook::react::Cursor::NWSEResize: + idc = IDC_SIZENWSE; + break; + case facebook::react::Cursor::EWResize: + idc = IDC_SIZEWE; + break; + case facebook::react::Cursor::Text: + idc = IDC_IBEAM; + break; + case facebook::react::Cursor::Progress: + idc = IDC_APPSTARTING; + break; + case facebook::react::Cursor::Crosshair: + idc = IDC_CROSS; + break; + default: + break; + } + m_hcursor = ::LoadCursor(nullptr, idc); + m_hcursorOwned = true; + } else { + m_hcursor = hcur; + } + ::SetCursor(m_hcursor); + } + m_currentCursor = cursor; + } } void CompositionEventHandler::UpdateActiveTouch( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h index a114ac08773..f35bd44115d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h @@ -147,6 +147,9 @@ class CompositionEventHandler : public std::enable_shared_from_this m_activeTouches; // iOS is map of touch event args to ActiveTouch..? PointerId m_touchId = 0; int m_fragmentTag = -1; @@ -157,6 +160,10 @@ class CompositionEventHandler : public std::enable_shared_from_this m_capturedPointers; + HCURSOR m_hcursor{nullptr}; + bool m_hcursorOwned{false}; // If we create the cursor, so we need to destroy it + facebook::react::Cursor m_currentCursor{facebook::react::Cursor::Auto}; + winrt::Microsoft::UI::Input::InputCursor m_inputCursor{nullptr}; #ifdef USE_WINUI3 winrt::event_token m_pointerPressedToken; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 18a7966c627..bb0e2c1693d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -791,6 +791,10 @@ void ComponentView::updateClippingPath( } } +std::pair ComponentView::cursor() const noexcept { + return {viewProps()->cursor, nullptr}; +} + void ComponentView::indexOffsetForBorder(uint32_t &index) const noexcept { if (m_borderPrimitive) { index += m_borderPrimitive->numberOfVisuals(); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index 25ee06c7d9f..29f1412878d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -106,6 +106,8 @@ struct ComponentView : public ComponentViewT< void Toggle() noexcept override; virtual winrt::Microsoft::ReactNative::implementation::ClipState getClipState() noexcept; + virtual std::pair cursor() const noexcept; + const facebook::react::LayoutMetrics &layoutMetrics() const noexcept; virtual std::string DefaultControlType() const noexcept; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp index f583149aa3d..08b584f2162 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp @@ -24,6 +24,7 @@ #include "IReactContext.h" #include "ReactHost/ReactInstanceWin.h" #include "ReactNativeHost.h" +#include "WindowsModalHostViewShadowNode.h" namespace winrt::Microsoft::ReactNative::Composition::implementation { WindowsModalHostComponentView::WindowsModalHostComponentView( @@ -33,6 +34,23 @@ 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 = + winrt::Microsoft::ReactNative::implementation::ReactNativeHost::GetReactNativeHost(m_reactContext.Properties()); + winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId( + host.InstanceSettings().Properties(), m_prevWindowID); + m_prevWindowID = 0; + } + + // enable input to parent + EnableWindow(m_parentHwnd, true); + // Check if the window handle (m_hwnd) exists and destroy it if necessary if (m_hwnd) { // Close/Destroy the modal window @@ -77,17 +95,19 @@ void WindowsModalHostComponentView::EnsureModalCreated() { m_prevWindowID = winrt::Microsoft::ReactNative::ReactCoreInjection::GetTopLevelWindowId(m_reactContext.Properties().Handle()); - auto roothwnd = GetHwndForParenting(); + m_parentHwnd = GetHwndForParenting(); + + auto windowsStyle = m_showTitleBar ? WS_OVERLAPPEDWINDOW : WS_POPUP; m_hwnd = CreateWindow( c_modalWindowClassName, L"React-Native Modal", - WS_OVERLAPPEDWINDOW, + windowsStyle, CW_USEDEFAULT, CW_USEDEFAULT, MODAL_MIN_WIDTH, MODAL_MIN_HEIGHT, - roothwnd, // parent + m_parentHwnd, // parent nullptr, hInstance, spunk.get()); @@ -126,7 +146,7 @@ void WindowsModalHostComponentView::EnsureModalCreated() { constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; RECT rc; - GetClientRect(roothwnd, &rc); + GetClientRect(m_parentHwnd, &rc); // Maximum size is set to size of parent hwnd constraints.MaximumSize = {(rc.right - rc.left) * ScaleFactor(m_hwnd), (rc.bottom - rc.top) / ScaleFactor(m_hwnd)}; constraints.MinimumSize = {MODAL_MIN_WIDTH * ScaleFactor(m_hwnd), MODAL_MIN_HEIGHT * ScaleFactor(m_hwnd)}; @@ -141,6 +161,14 @@ void WindowsModalHostComponentView::ShowOnUIThread() { ShowWindow(m_hwnd, SW_NORMAL); BringWindowToTop(m_hwnd); SetFocus(m_hwnd); + + // disable input to parent + EnableWindow(m_parentHwnd, false); + + // dispatch onShow event + auto emitter = std::static_pointer_cast(m_eventEmitter); + facebook::react::ModalHostViewEventEmitter::OnShow onShowArgs; + emitter->onShow(onShowArgs); } } @@ -149,12 +177,21 @@ 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); + // reset the topWindowID if (m_prevWindowID) { auto host = winrt::Microsoft::ReactNative::implementation::ReactNativeHost::GetReactNativeHost(m_reactContext.Properties()); winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId( host.InstanceSettings().Properties(), m_prevWindowID); + m_prevWindowID = 0; } } @@ -266,22 +303,32 @@ void WindowsModalHostComponentView::AdjustWindowSize() noexcept { if (m_layoutMetrics.overflowInset.right == 0 && m_layoutMetrics.overflowInset.bottom == 0) { return; } + // Modal's size is based on it's children, use the overflow to calculate the width/height float xPos = (-m_layoutMetrics.overflowInset.right * (m_layoutMetrics.pointScaleFactor)); float yPos = (-m_layoutMetrics.overflowInset.bottom * (m_layoutMetrics.pointScaleFactor)); 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 - MoveWindow(m_hwnd, 0, 0, (int)(rect.right - rect.left), (int)(rect.bottom - rect.top), true); + + if (m_showTitleBar) { + AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); // Adjust for title bar and borders + } // set the layoutMetrics - m_layoutMetrics.frame.size = { - (float)rect.right - rect.left + m_layoutMetrics.frame.origin.x, - (float)rect.bottom - rect.top + m_layoutMetrics.frame.origin.y}; + m_layoutMetrics.frame.size = {(float)rect.right - rect.left, (float)rect.bottom - rect.top}; m_layoutMetrics.overflowInset.right = 0; m_layoutMetrics.overflowInset.bottom = 0; + // get Modal's position based on parent + RECT parentRC; + GetWindowRect(m_parentHwnd, &parentRC); + float xCor = (parentRC.left + parentRC.right - m_layoutMetrics.frame.size.width) / 2; // midpointx - width / 2 + float yCor = (parentRC.top + parentRC.bottom - m_layoutMetrics.frame.size.height) / 2; // midpointy - height / 2 + + // Adjust window position and size + MoveWindow(m_hwnd, (int)xCor, (int)yCor, (int)(rect.right - rect.left), (int)(rect.bottom - rect.top), true); + // Let RNWIsland know that Modal's size has changed winrt::get_self(m_reactNativeIsland) ->NotifySizeChanged(); @@ -294,6 +341,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/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h index a53fb56da16..fbb3f10ec21 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h @@ -60,9 +60,11 @@ struct WindowsModalHostComponentView static void RegisterWndClass() noexcept; private: + HWND m_parentHwnd{nullptr}; HWND m_hwnd{nullptr}; uint64_t m_prevWindowID; bool m_isVisible{false}; + bool m_showTitleBar{false}; winrt::Microsoft::ReactNative::ReactNativeIsland m_reactNativeIsland; }; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index da018e2697f..af203ad8f78 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -236,7 +236,7 @@ struct CompTextHost : public winrt::implements { //@cmember Establish a new cursor shape void TxSetCursor(HCURSOR hcur, BOOL fText) override { - assert(false); + m_outer->m_hcursor = hcur; } //@cmember Converts screen coordinates of a specified point to the client coordinates @@ -732,6 +732,9 @@ void WindowsTextInputComponentView::OnPointerMoved( auto hr = m_textServices->TxSendMessage(msg, static_cast(wParam), static_cast(lParam), &lresult); args.Handled(hr != S_FALSE); } + + m_textServices->OnTxSetCursor( + DVASPECT_CONTENT, -1, nullptr, nullptr, nullptr, nullptr, nullptr, ptContainer.x, ptContainer.y); } void WindowsTextInputComponentView::OnKeyDown( @@ -1479,6 +1482,10 @@ WindowsTextInputComponentView::createVisual() noexcept { return visual; } +std::pair WindowsTextInputComponentView::cursor() const noexcept { + return {viewProps()->cursor, m_hcursor}; +} + void WindowsTextInputComponentView::onThemeChanged() noexcept { const auto &props = windowsTextInputProps(); updateCursorColor(props.cursorColor, props.textAttributes.foregroundColor); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h index a943c1f0581..155fe5e6492 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h @@ -78,6 +78,8 @@ struct WindowsTextInputComponentView winrt::Microsoft::ReactNative::Composition::Experimental::IVisual createVisual() noexcept; + std::pair cursor() const noexcept override; + private: struct DrawBlock { DrawBlock(WindowsTextInputComponentView &view); @@ -133,6 +135,7 @@ struct WindowsTextInputComponentView bool m_multiline{false}; DWORD m_propBitsMask{0}; DWORD m_propBits{0}; + HCURSOR m_hcursor{nullptr}; std::vector m_submitKeyEvents; }; diff --git a/vnext/overrides.json b/vnext/overrides.json index 5d8bbfd7c43..d6a22051bb8 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -516,6 +516,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": "82b045bad618345055acd3b44abff4622ac0d6a5" + }, { "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..bee411a14df --- /dev/null +++ b/vnext/src-win/Libraries/Modal/Modal.windows.js @@ -0,0 +1,352 @@ +/** + * 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, + // $FlowFixMe[incompatible-type-arg] +> = ModalInjection.unstable_Modal ?? Modal; + +module.exports = ExportedModal;