diff --git a/change/@office-iss-react-native-win32-f99cfd3f-e8be-4146-8f14-07aca4f2c5b1.json b/change/@office-iss-react-native-win32-f99cfd3f-e8be-4146-8f14-07aca4f2c5b1.json new file mode 100644 index 00000000000..039e5671eb0 --- /dev/null +++ b/change/@office-iss-react-native-win32-f99cfd3f-e8be-4146-8f14-07aca4f2c5b1.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "adds default Modal", + "packageName": "@office-iss/react-native-win32", + "email": "tatianakapos@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-283ad80a-9f48-40a3-b7a3-227795b6f110.json b/change/react-native-windows-283ad80a-9f48-40a3-b7a3-227795b6f110.json new file mode 100644 index 00000000000..bd0c3b87371 --- /dev/null +++ b/change/react-native-windows-283ad80a-9f48-40a3-b7a3-227795b6f110.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "adds default modal that hosts fabric components", + "packageName": "react-native-windows", + "email": "tatianakapos@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/@office-iss/react-native-win32-tester/src/js/utils/RNTesterList.win32.js b/packages/@office-iss/react-native-win32-tester/src/js/utils/RNTesterList.win32.js index 83090c43f16..e93da886775 100644 --- a/packages/@office-iss/react-native-win32-tester/src/js/utils/RNTesterList.win32.js +++ b/packages/@office-iss/react-native-win32-tester/src/js/utils/RNTesterList.win32.js @@ -59,6 +59,11 @@ const Components: Array = [ key: 'ImageWin32Test', module: require('@office-iss/react-native-win32/Libraries/Image/Tests/ImageWin32Test'), }, + { + key: 'ModalExample', + category: 'UI', + module: require('../examples/Modal/ModalExample'), + }, /* { key: 'JSResponderHandlerExample', @@ -68,11 +73,6 @@ const Components: Array = [ key: 'KeyboardAvoidingViewExample', module: require('../examples/KeyboardAvoidingView/KeyboardAvoidingViewExample'), }, - { - key: 'ModalExample', - category: 'UI', - module: require('../examples/Modal/ModalExample'), - }, { key: 'NewAppScreenExample', module: require('../examples/NewAppScreen/NewAppScreenExample'), diff --git a/packages/@office-iss/react-native-win32/overrides.json b/packages/@office-iss/react-native-win32/overrides.json index 71ae1200e19..1bfbb16c27b 100644 --- a/packages/@office-iss/react-native-win32/overrides.json +++ b/packages/@office-iss/react-native-win32/overrides.json @@ -506,6 +506,12 @@ "baseHash": "bc63f57b28f1e98755101f650a2c15b6aa263abf", "issue": 11041 }, + { + "type": "copy", + "file": "src-win/src/private/reactdevtools/ReactDevToolsSettingsManager.win32.js", + "baseFile": "packages/react-native/src/private/reactdevtools/ReactDevToolsSettingsManager.android.js", + "baseHash": "df41b76dc3d2df9455fae588748261d7b0a22d01" + }, { "type": "derived", "file": "src-win/src/private/specs/modules/NativeAccessibilityInfoWin32.js", @@ -523,12 +529,6 @@ "file": "src-win/src/private/specs/modules/NativePlatformConstantsWin.js", "baseFile": "packages/react-native/src/private/specs/modules/NativePlatformConstantsAndroid.js", "baseHash": "fa0f34a2de33b641bd63863629087644796d8b59" - }, - { - "type": "copy", - "file": "src-win/src/private/reactdevtools/ReactDevToolsSettingsManager.win32.js", - "baseFile": "packages/react-native/src/private/reactdevtools/ReactDevToolsSettingsManager.android.js", - "baseHash": "df41b76dc3d2df9455fae588748261d7b0a22d01" } ] } \ No newline at end of file diff --git a/packages/@react-native-windows/tester/overrides.json b/packages/@react-native-windows/tester/overrides.json index 3158780f461..e7ef1bb066a 100644 --- a/packages/@react-native-windows/tester/overrides.json +++ b/packages/@react-native-windows/tester/overrides.json @@ -41,6 +41,18 @@ "baseHash": "eb640e1c484a8cdaa04d653505d5aab6429eb4b0", "issue": 12869 }, + { + "type": "patch", + "file": "src/js/examples/Modal/ModalOnShow.windows.js", + "baseFile": "packages/rn-tester/js/examples/Modal/ModalOnShow.js", + "baseHash": "d442625a5c03de1ea78a43385c8f60dd9b4da68c" + }, + { + "type": "patch", + "file": "src/js/examples/Modal/ModalPresentation.windows.js", + "baseFile": "packages/rn-tester/js/examples/Modal/ModalPresentation.js", + "baseHash": "1e3d81c2a1f6e5a05747244a4efd18548d88051d" + }, { "type": "patch", "file": "src/js/examples/Pressable/PressableExample.windows.js", diff --git a/packages/@react-native-windows/tester/src/js/examples/Modal/ModalOnShow.windows.js b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalOnShow.windows.js new file mode 100644 index 00000000000..e4e2468c967 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalOnShow.windows.js @@ -0,0 +1,149 @@ +/** + * 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-local + * @format + */ + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +import * as React from 'react'; +import {Modal, Pressable, StyleSheet, Text, View} from 'react-native'; + +function ModalOnShowOnDismiss(): React.Node { + const [modalShowComponent, setModalShowComponent] = React.useState(true); + const [modalVisible, setModalVisible] = React.useState(false); + const [onShowCount, setOnShowCount] = React.useState(0); + const [onDismissCount, setOnDismissCount] = React.useState(0); + + return ( + + {modalShowComponent && ( + { + setOnShowCount(onShowCount + 1); + }} + onDismiss={() => { + setOnDismissCount(onDismissCount + 1); + }} + onRequestClose={() => { + setModalVisible(false); + }}> + + + + onShow is called {onShowCount} times + + + onDismiss is called {onDismissCount} times + + setModalVisible(false)}> + + Hide modal by setting visible to false + + + setModalShowComponent(false)}> + + Hide modal by removing component + + + + + + )} + onShow is called {onShowCount} times + + onDismiss is called {onDismissCount} times + + { + setModalShowComponent(true); + setModalVisible(true); + }}> + + Show Modal + + + + ); +} + +const styles = StyleSheet.create({ + container: { + display: 'flex', + alignItems: 'center', + paddingVertical: 30, + }, + centeredView: { + // flex: 1, [Windows] + justifyContent: 'center', + alignItems: 'center', + }, + modalBackdrop: { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + modalView: { + margin: 20, + backgroundColor: 'white', + borderRadius: 20, + padding: 35, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + button: { + borderRadius: 20, + padding: 10, + marginVertical: 20, + elevation: 2, + }, + buttonOpen: { + backgroundColor: '#F194FF', + }, + buttonClose: { + backgroundColor: '#2196F3', + }, + textStyle: { + color: 'white', + fontWeight: 'bold', + textAlign: 'center', + }, + // [Windows + widthHeight: { + width: 300, + height: 400, + }, + // Windows] +}); + +export default ({ + title: "Modal's onShow/onDismiss", + name: 'onShow', + description: + 'onShow and onDismiss (iOS only) callbacks are called when a modal is shown/dismissed', + render: (): React.Node => , +}: RNTesterModuleExample); 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 new file mode 100644 index 00000000000..4ed2de275a8 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js @@ -0,0 +1,321 @@ +/** + * 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-local + * @format + */ + +/* eslint-disable no-alert */ + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; +import type {Props as ModalProps} from 'react-native/Libraries/Modal/Modal'; + +import RNTOption from '../../components/RNTOption'; +import * as React from 'react'; +import {Modal, Platform, StyleSheet, Switch, Text, View} from 'react-native'; + +const RNTesterButton = require('../../components/RNTesterButton'); + +const animationTypes = ['slide', 'none', 'fade']; +const presentationStyles = [ + 'fullScreen', + 'pageSheet', + 'formSheet', + 'overFullScreen', +]; +const supportedOrientations = [ + 'portrait', + 'portrait-upside-down', + 'landscape', + 'landscape-left', + 'landscape-right', +]; + +function ModalPresentation() { + const onDismiss = React.useCallback(() => { + alert('onDismiss'); + }, []); + + const onShow = React.useCallback(() => { + alert('onShow'); + }, []); + + const onRequestClose = React.useCallback(() => { + console.log('onRequestClose'); + }, []); + + const [props, setProps] = React.useState({ + animationType: 'none', + transparent: false, + hardwareAccelerated: false, + statusBarTranslucent: false, + presentationStyle: Platform.select({ + ios: 'fullScreen', + default: undefined, + }), + supportedOrientations: Platform.select({ + ios: ['portrait'], + default: undefined, + }), + onDismiss: undefined, + onShow: undefined, + visible: false, + }); + const presentationStyle = props.presentationStyle; + const hardwareAccelerated = props.hardwareAccelerated; + const statusBarTranslucent = props.statusBarTranslucent; + + const [currentOrientation, setCurrentOrientation] = React.useState('unknown'); + + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + const onOrientationChange = event => + setCurrentOrientation(event.nativeEvent.orientation); + + const controls = ( + <> + + Status Bar Translucent 🟢 + + setProps(prev => ({...prev, statusBarTranslucent: enabled})) + } + /> + + + Hardware Acceleration 🟢 + + setProps(prev => ({ + ...prev, + hardwareAccelerated: enabled, + })) + } + /> + + + Presentation Style ⚫️ + + {presentationStyles.map(type => ( + + setProps(prev => { + if (type === 'overFullScreen' && prev.transparent === true) { + return { + ...prev, + presentationStyle: type, + transparent: false, + }; + } + return { + ...prev, + presentationStyle: + type === prev.presentationStyle ? undefined : type, + }; + }) + } + selected={type === presentationStyle} + /> + ))} + + + + + Transparent + + setProps(prev => ({...prev, transparent: enabled})) + } + /> + + {Platform.OS === 'ios' && presentationStyle !== 'overFullScreen' ? ( + + iOS Modal can only be transparent with 'overFullScreen' Presentation + Style + + ) : null} + + + Supported Orientation ⚫️ + + {supportedOrientations.map(orientation => ( + + setProps(prev => { + if (prev.supportedOrientations?.includes(orientation)) { + return { + ...prev, + supportedOrientations: prev.supportedOrientations?.filter( + o => o !== orientation, + ), + }; + } + return { + ...prev, + supportedOrientations: [ + ...(prev.supportedOrientations ?? []), + orientation, + ], + }; + }) + } + selected={props.supportedOrientations?.includes(orientation)} + /> + ))} + + + + Actions + + + setProps(prev => ({ + ...prev, + onShow: prev.onShow ? undefined : onShow, + })) + } + selected={!!props.onShow} + /> + + setProps(prev => ({ + ...prev, + onDismiss: prev.onDismiss ? undefined : onDismiss, + })) + } + selected={!!props.onDismiss} + /> + + + + ); + + return ( + + setProps(prev => ({...prev, visible: true}))}> + Show Modal + + + + + + This modal was presented with animationType: ' + {props.animationType}' + + {Platform.OS === 'ios' ? ( + + It is currently displayed in {currentOrientation} mode. + + ) : null} + setProps(prev => ({...prev, visible: false}))}> + Close + + {controls} + + + + + Animation Type + + {animationTypes.map(type => ( + setProps(prev => ({...prev, animationType: type}))} + selected={type === props.animationType} + /> + ))} + + + {controls} + + ); +} + +const styles = StyleSheet.create({ + row: { + flexWrap: 'wrap', + flexDirection: 'row', + }, + rowWithSpaceBetween: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + block: { + borderColor: 'rgba(0,0,0, 0.1)', + borderBottomWidth: 1, + padding: 6, + }, + inlineBlock: { + padding: 6, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + borderColor: 'rgba(0,0,0, 0.1)', + borderBottomWidth: 1, + }, + title: { + margin: 3, + fontWeight: 'bold', + }, + option: { + marginRight: 8, + marginTop: 6, + }, + modalContainer: { + // [Windows + width: 500, + height: 500, + // flex: 1, + // justifyContent: 'center', + // padding: 20, + // Windows ] + }, + modalInnerContainer: { + borderRadius: 10, + backgroundColor: '#fff', + padding: 10, + }, + warning: { + margin: 3, + fontSize: 12, + color: 'red', + }, +}); + +export default ({ + title: 'Modal Presentation', + name: 'basic', + description: 'Modals can be presented with or without animation', + render: (): React.Node => , +}: RNTesterModuleExample); diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js index 19ce898a2ae..d6c1e7a705e 100644 --- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js +++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js @@ -63,11 +63,11 @@ const Components: Array = [ category: 'UI', module: require('../examples-win/Glyph/GlyphExample'), }, - // { - // key: 'ModalExample', - // category: 'UI', - // module: require('../examples/Modal/ModalExample'), - // }, + { + key: 'ModalExample', + category: 'UI', + module: require('../examples/Modal/ModalExample'), + }, { key: 'Drawing Island', module: require('../examples-win/NativeComponents/DrawingIsland'), diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap index 596b368e838..e126d7e183e 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap @@ -3828,6 +3828,95 @@ exports[`Home UIA Tree Dump Linking 1`] = ` } `; +exports[`Home UIA Tree Dump Modal 1`] = ` +{ + "Automation Tree": { + "AutomationId": "Modal", + "ControlType": 50026, + "IsKeyboardFocusable": true, + "LocalizedControlType": "group", + "Name": "Modal Component for presenting modal views.", + "__Children": [ + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Modal", + }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Component for presenting modal views.", + }, + ], + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "Modal Component for presenting modal views.", + "TestId": "Modal", + }, + "__Children": [ + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + ], + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 255, 255, 255)", + }, + "Comment": "Modal", + "Offset": "0, 0, 0", + "Size": "966, 78", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "16, 16, 0", + "Size": "51, 25", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "51, 25", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 0, 0", + "Size": "0, 0", + "Visual Type": "SpriteVisual", + }, + ], + }, + { + "Offset": "16, 45, 0", + "Size": "934, 17", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "934, 17", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 0, 0", + "Size": "0, 0", + "Visual Type": "SpriteVisual", + }, + ], + }, + ], + }, +} +`; + exports[`Home UIA Tree Dump Mouse Click Events 1`] = ` { "Automation Tree": { @@ -4498,7 +4587,7 @@ exports[`Home UIA Tree Dump Performance Comparison Examples 1`] = ` }, "Comment": "Performance Comparison Examples", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -4854,7 +4943,7 @@ exports[`Home UIA Tree Dump Popup 1`] = ` }, "Comment": "Popup", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -5321,12 +5410,12 @@ exports[`Home UIA Tree Dump ScrollViewSimpleExample 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", }, { @@ -5520,12 +5609,12 @@ exports[`Home UIA Tree Dump SectionList 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", }, { @@ -6032,7 +6121,7 @@ exports[`Home UIA Tree Dump TextInputs with key prop 1`] = ` }, "Comment": "TextInputs with key prop", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -6210,7 +6299,7 @@ exports[`Home UIA Tree Dump Touchable* and onPress 1`] = ` }, "Comment": "Touchable* and onPress", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -6838,12 +6927,12 @@ exports[`Home UIA Tree Dump XAML 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "47, 25", + "Size": "47, 24", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "47, 25", + "Size": "47, 24", "Visual Type": "SpriteVisual", }, { @@ -6927,12 +7016,12 @@ exports[`Home UIA Tree Dump XMLHttpRequest 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "135, 24", + "Size": "135, 25", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "135, 24", + "Size": "135, 25", "Visual Type": "SpriteVisual", }, { 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 84d5c419c5d..f7fd04eb43b 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 @@ -27678,6 +27678,1442 @@ exports[`snapshotAllPages Linking 2`] = ` `; +exports[`snapshotAllPages Modal 1`] = ` + + + + Show Modal + + + + + Animation Type + + + + + + slide + + + + + + + none + + + + + + + fade + + + + + + + + Status Bar Translucent 🟢 + + + + + + Hardware Acceleration 🟢 + + + + + + Presentation Style ⚫️ + + + + + + fullScreen + + + + + + + pageSheet + + + + + + + formSheet + + + + + + + overFullScreen + + + + + + + + + Transparent + + + + + + + Supported Orientation ⚫️ + + + + + + portrait + + + + + + + portrait-upside-down + + + + + + + landscape + + + + + + + landscape-left + + + + + + + landscape-right + + + + + + + + Actions + + + + + + onShow + + + + + + + onDismiss ⚫️ + + + + + + +`; + +exports[`snapshotAllPages Modal 2`] = ` + + + onShow is called + 0 + times + + + onDismiss is called + 0 + times + + + + Show Modal + + + +`; + exports[`snapshotAllPages Mouse Click Events 1`] = ` const &handler) noexcept { return m_mountedEvent.add(handler); diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h index b5382ccad28..4e46a1e1d0f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h @@ -113,6 +113,7 @@ struct ComponentView : public ComponentViewT { virtual void onGotFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept; void MarkAsCustomComponent() noexcept; virtual void onMounted() noexcept; + bool isMounted() noexcept; virtual void onUnmounted() noexcept; void onDestroying() noexcept; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index 3acf0840507..ebaa245d1cf 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -139,8 +139,9 @@ struct CompositionInputKeyboardSource : winrt::implements< CompositionEventHandler::CompositionEventHandler( const winrt::Microsoft::ReactNative::ReactContext &context, - const winrt::Microsoft::ReactNative::ReactNativeIsland &reactNativeIsland) - : m_context(context), m_wkRootView(reactNativeIsland) {} + const winrt::Microsoft::ReactNative::ReactNativeIsland &reactNativeIsland, + const int fragmentTag) + : m_fragmentTag(fragmentTag), m_context(context), m_wkRootView(reactNativeIsland) {} void CompositionEventHandler::Initialize() noexcept { #ifdef USE_WINUI3 @@ -831,7 +832,30 @@ void CompositionEventHandler::getTargetPointerArgs( ptLocal.y = ptScaled.y - (clientRect.top / strongRootView.ScaleFactor()); } } else { - tag = RootComponentView().hitTest(ptScaled, ptLocal); + if (m_fragmentTag == -1) { + tag = RootComponentView().hitTest(ptScaled, ptLocal); + return; + } + + // check if the fragment tag exists + if (!fabricuiManager->GetViewRegistry().findComponentViewWithTag(m_fragmentTag)) { + return; + } + + auto fagmentView = fabricuiManager->GetViewRegistry().componentViewDescriptorWithTag(m_fragmentTag).view; + auto fagmentchildren = fagmentView.Children(); + + // call the hitTest with the fargment as the RootComponent + for (auto index = fagmentchildren.Size(); index > 0; index--) { + auto childView = fagmentchildren.GetAt(index - 1); + auto targetTag = + winrt::get_self(childView)->hitTest( + ptScaled, ptLocal); + if (targetTag != -1) { + tag = targetTag; + break; + } + } } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h index 44a1c12b129..a114ac08773 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h @@ -31,7 +31,8 @@ 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; std::map> m_currentlyHoveredViewsPerPointer; winrt::weak_ref m_wkRootView; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index b314a73e057..e073fdb172f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -870,6 +870,11 @@ winrt::Microsoft::ReactNative::ComponentView ViewComponentView::Create( ViewComponentView::defaultProps(), compContext, tag, reactContext, ComponentViewFeatures::Default); } +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual +ViewComponentView::VisualToMountChildrenInto() noexcept { + return Visual(); +} + void ViewComponentView::MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { @@ -889,7 +894,7 @@ void ViewComponentView::MountChildComponentView( } } } - Visual().InsertAt(compositionChild->OuterVisual(), visualIndex); + VisualToMountChildrenInto().InsertAt(compositionChild->OuterVisual(), visualIndex); } else { m_hasNonVisualChildren = true; } @@ -902,7 +907,7 @@ void ViewComponentView::UnmountChildComponentView( indexOffsetForBorder(index); if (auto compositionChild = childComponentView.try_as()) { - Visual().Remove(compositionChild->OuterVisual()); + VisualToMountChildrenInto().Remove(compositionChild->OuterVisual()); } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index 0eaedc369d5..2c79f512e16 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -162,6 +162,7 @@ struct ViewComponentView : public ViewComponentViewT< facebook::react::Tag tag, winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept; + virtual winrt::Microsoft::ReactNative::Composition::Experimental::IVisual VisualToMountChildrenInto() noexcept; void MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept override; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp index 90ea200458c..d4be4afbd20 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp @@ -10,8 +10,16 @@ #include "../CompositionDynamicAutomationProvider.h" #include "Unicode.h" +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include #include "IReactContext.h" #include "ReactHost/ReactInstanceWin.h" @@ -22,13 +30,15 @@ WindowsModalHostComponentView::WindowsModalHostComponentView( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, winrt::Microsoft::ReactNative::ReactContext const &reactContext) - : Super( - WindowsModalHostComponentView::defaultProps(), - compContext, - tag, - reactContext, - ComponentViewFeatures::Default & ~ComponentViewFeatures::Background) { - m_context = reactContext; // save context + : Super(compContext, tag, reactContext) {} + +WindowsModalHostComponentView::~WindowsModalHostComponentView() { + // Check if the window handle (m_hwnd) exists and destroy it if necessary + if (m_hwnd) { + // Close/Destroy the modal window + SendMessage(m_hwnd, WM_DESTROY, 0, 0); + m_hwnd = nullptr; + } } winrt::Microsoft::ReactNative::ComponentView WindowsModalHostComponentView::Create( @@ -38,34 +48,35 @@ winrt::Microsoft::ReactNative::ComponentView WindowsModalHostComponentView::Crea return winrt::make(compContext, tag, reactContext); } -// constants for creating a new windows (code mostly taken from LogBox) +// constants for creating a new windows constexpr PCWSTR c_modalWindowClassName = L"MS_REACTNATIVE_MODAL"; constexpr auto CompHostProperty = L"CompHost"; -const int MODAL_DEFAULT_WIDTH = 500; -const int MODAL_DEFAULT_HEIGHT = 500; +const int MODAL_MIN_WIDTH = 50; +const int MODAL_MIN_HEIGHT = 50; + +float ScaleFactor(HWND hwnd) noexcept { + return GetDpiForWindow(hwnd) / static_cast(USER_DEFAULT_SCREEN_DPI); +} // creates a new modal window void WindowsModalHostComponentView::EnsureModalCreated() { auto host = - winrt::Microsoft::ReactNative::implementation::ReactNativeHost::GetReactNativeHost(m_context.Properties()); + winrt::Microsoft::ReactNative::implementation::ReactNativeHost::GetReactNativeHost(m_reactContext.Properties()); + // return if hwnd already exists if (!host || m_hwnd) { return; } - RegisterWndClass(); // creates and register a windows class - auto CompositionHwndHost = winrt::Microsoft::ReactNative::CompositionHwndHost(); - winrt::Microsoft::ReactNative::ReactViewOptions viewOptions; - viewOptions.ComponentName(L"Modal"); - CompositionHwndHost.ReactViewHost(winrt::Microsoft::ReactNative::ReactCoreInjection::MakeViewHost(host, viewOptions)); + RegisterWndClass(); + HINSTANCE hInstance = GetModuleHandle(NULL); - winrt::impl::abi::type *pHost{nullptr}; winrt::com_ptr<::IUnknown> spunk; - CompositionHwndHost.as(spunk); // get the root hwnd - auto roothwnd = reinterpret_cast( - winrt::Microsoft::ReactNative::ReactCoreInjection::GetTopLevelWindowId(m_context.Properties().Handle())); + m_prevWindowID = + winrt::Microsoft::ReactNative::ReactCoreInjection::GetTopLevelWindowId(m_reactContext.Properties().Handle()); + auto roothwnd = reinterpret_cast(m_prevWindowID); m_hwnd = CreateWindow( c_modalWindowClassName, @@ -73,8 +84,8 @@ void WindowsModalHostComponentView::EnsureModalCreated() { WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, - MODAL_DEFAULT_WIDTH, - MODAL_DEFAULT_HEIGHT, + MODAL_MIN_WIDTH, + MODAL_MIN_HEIGHT, roothwnd, // parent nullptr, hInstance, @@ -85,11 +96,47 @@ void WindowsModalHostComponentView::EnsureModalCreated() { throw std::exception("Failed to create new hwnd for Modal: " + GetLastError()); } + // Disable user sizing of the hwnd + ::SetWindowLong(m_hwnd, GWL_STYLE, GetWindowLong(m_hwnd, GWL_STYLE) & ~WS_SIZEBOX); + + // set the top-level windows as the new hwnd + winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId( + host.InstanceSettings().Properties(), reinterpret_cast(m_hwnd)); + + // get current compositor - handles the creation/manipulation of visual objects + auto compositionContext = CompositionContext(); + auto compositor = + winrt::Microsoft::ReactNative::Composition::Experimental::MicrosoftCompositionContextHelper::InnerCompositor( + compositionContext); + + // create a react native island - code taken from CompositionHwndHost + auto bridge = winrt::Microsoft::UI::Content::DesktopChildSiteBridge::Create( + compositor, winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd)); + m_reactNativeIsland = winrt::Microsoft::ReactNative::ReactNativeIsland(compositor, m_reactContext.Handle(), *this); + auto contentIsland = m_reactNativeIsland.Island(); + bridge.Connect(contentIsland); + bridge.Show(); + + // set ScaleFactor + ScaleFactor(m_hwnd); + + // set layout contraints + winrt::Microsoft::ReactNative::LayoutConstraints constraints; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; + + RECT rc; + GetClientRect(roothwnd, &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)}; + m_reactNativeIsland.Arrange(constraints, {0, 0}); + bridge.ResizePolicy(winrt::Microsoft::UI::Content::ContentSizePolicy::ResizeContentToParentWindow); + spunk.detach(); } void WindowsModalHostComponentView::ShowOnUIThread() { - if (m_hwnd) { + if (m_hwnd && !IsWindowVisible(m_hwnd)) { ShowWindow(m_hwnd, SW_NORMAL); BringWindowToTop(m_hwnd); SetFocus(m_hwnd); @@ -98,7 +145,15 @@ void WindowsModalHostComponentView::ShowOnUIThread() { void WindowsModalHostComponentView::HideOnUIThread() noexcept { if (m_hwnd) { - ::ShowWindow(m_hwnd, SW_HIDE); + SendMessage(m_hwnd, WM_CLOSE, 0, 0); + } + + // 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); } } @@ -121,23 +176,22 @@ LRESULT CALLBACK ModalBoxWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM } switch (message) { - case WM_NCCREATE: { // sent before WM_CREATE, lparam should be identical to members of CreateWindowEx + case WM_NCCREATE: { // called before WM_CREATE, lparam should be identical to members of CreateWindowEx auto createStruct = reinterpret_cast(lparam); // CreateStruct data = static_cast<::IUnknown *>(createStruct->lpCreateParams); SetProp(hwnd, CompHostProperty, data); // adds new properties to window break; } - case WM_CREATE: { // recieves after window is created but before visible - // host.Initialize((uint64_t)hwnd); cause Modal to throw a not registered error - break; - } case WM_CLOSE: { // Just hide the window instead of destroying it ::ShowWindow(hwnd, SW_HIDE); return 0; } case WM_DESTROY: { // called when we want to destroy the window - data->Release(); + ::ShowWindow(hwnd, SW_HIDE); + if (data) { + data->Release(); + } SetProp(hwnd, CompHostProperty, nullptr); break; } @@ -174,121 +228,74 @@ void WindowsModalHostComponentView::RegisterWndClass() noexcept { registered = true; } +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual +WindowsModalHostComponentView::VisualToMountChildrenInto() noexcept { + return m_reactNativeIsland + .as() + .InternalRootVisual(); +} + +// childComponentView - reference to the child component view +// index - the position in which the childComponentView should be mounted void WindowsModalHostComponentView::MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { - // Disabled due to partial Modal implementation. Tracking re-enablement with task list here: - // https://github.com/microsoft/react-native-windows/issues/11157 assert(false); + EnsureModalCreated(); base_type::MountChildComponentView(childComponentView, index); } void WindowsModalHostComponentView::UnmountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { - // Disabled due to partial Modal implementation.Tracking re-enablement with task list here : https : // - // github.com/microsoft/react-native-windows/issues/11157 assert(false); base_type::UnmountChildComponentView(childComponentView, index); } -void WindowsModalHostComponentView::HandleCommand( - const winrt::Microsoft::ReactNative::HandleCommandArgs &args) noexcept { - Super::HandleCommand(args); +void WindowsModalHostComponentView::updateLayoutMetrics( + facebook::react::LayoutMetrics const &layoutMetrics, + facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept { + base_type::updateLayoutMetrics(layoutMetrics, oldLayoutMetrics); + if (m_hwnd) { + EnsureModalCreated(); + AdjustWindowSize(); + ShowOnUIThread(); + } } +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); + + // 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.overflowInset.right = 0; + m_layoutMetrics.overflowInset.bottom = 0; + + // Let RNWIsland know that Modal's size has changed + winrt::get_self(m_reactNativeIsland) + ->NotifySizeChanged(); +}; + void WindowsModalHostComponentView::updateProps( facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept { const auto &oldModalProps = *std::static_pointer_cast(oldProps ? oldProps : viewProps()); const auto &newModalProps = *std::static_pointer_cast(props); - - // currently Modal only gets Destroyed by closing the window - if (newModalProps.visible) { - EnsureModalCreated(); - ShowOnUIThread(); - } - + newModalProps.visible ? m_isVisible = true : m_isVisible = false; base_type::updateProps(props, oldProps); } -void WindowsModalHostComponentView::updateLayoutMetrics( - facebook::react::LayoutMetrics const &layoutMetrics, - facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept { - Super::updateLayoutMetrics(layoutMetrics, oldLayoutMetrics); - - // Temporary placeholder for Modal, draws on main hwnd - if (m_layoutMetrics.frame.size != layoutMetrics.frame.size || - m_layoutMetrics.pointScaleFactor != layoutMetrics.pointScaleFactor || m_layoutMetrics.frame.size.width == 0) { - // Always make visual a min size, so that even if its laid out at zero size, its clear an unimplemented view was - // rendered - float width = std::max(m_layoutMetrics.frame.size.width, 200.0f); - float height = std::max(m_layoutMetrics.frame.size.width, 50.0f); - - winrt::Windows::Foundation::Size surfaceSize = { - width * m_layoutMetrics.pointScaleFactor, height * m_layoutMetrics.pointScaleFactor}; - auto drawingSurface = m_compContext.CreateDrawingSurfaceBrush( - surfaceSize, - winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, - winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied); - - drawingSurface.HorizontalAlignmentRatio(0.f); - drawingSurface.VerticalAlignmentRatio(0.f); - drawingSurface.Stretch(winrt::Microsoft::ReactNative::Composition::Experimental::CompositionStretch::None); - Visual().as().Brush(drawingSurface); - Visual().Size(surfaceSize); - Visual().Offset({ - layoutMetrics.frame.origin.x * layoutMetrics.pointScaleFactor, - layoutMetrics.frame.origin.y * layoutMetrics.pointScaleFactor, - 0.0f, - }); - - POINT offset; - { - ::Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw( - drawingSurface, m_layoutMetrics.pointScaleFactor, &offset); - if (auto d2dDeviceContext = autoDraw.GetRenderTarget()) { - d2dDeviceContext->Clear(D2D1::ColorF(D2D1::ColorF::Blue, 0.3f)); - assert(d2dDeviceContext->GetUnitMode() == D2D1_UNIT_MODE_DIPS); - - float offsetX = static_cast(offset.x / m_layoutMetrics.pointScaleFactor); - float offsetY = static_cast(offset.y / m_layoutMetrics.pointScaleFactor); - - winrt::com_ptr spTextFormat; - winrt::check_hresult(::Microsoft::ReactNative::DWriteFactory()->CreateTextFormat( - L"Segoe UI", - nullptr, // Font collection (nullptr sets it to use the system font collection). - DWRITE_FONT_WEIGHT_REGULAR, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - 12, - L"", - spTextFormat.put())); - - winrt::com_ptr textBrush; - winrt::check_hresult( - d2dDeviceContext->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), textBrush.put())); - - const D2D1_RECT_F rect = { - static_cast(offset.x), static_cast(offset.y), width + offset.x, height + offset.y}; - - auto label = ::Microsoft::Common::Unicode::Utf8ToUtf16(std::string("This is a Modal")); - d2dDeviceContext->DrawText( - label.c_str(), - static_cast(label.length()), - spTextFormat.get(), - rect, - textBrush.get(), - D2D1_DRAW_TEXT_OPTIONS_NONE, - DWRITE_MEASURING_MODE_NATURAL); - } - } - } -} - -void WindowsModalHostComponentView::updateState( - facebook::react::State::Shared const &state, - facebook::react::State::Shared const &oldState) noexcept {} - facebook::react::SharedViewProps WindowsModalHostComponentView::defaultProps() noexcept { static auto const defaultProps = std::make_shared(); return defaultProps; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h index 547b9f3cc50..a53fb56da16 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h @@ -9,34 +9,38 @@ #include "Composition.WindowsModalHostComponentView.g.h" #include "../CompositionViewComponentView.h" +#include #include namespace winrt::Microsoft::ReactNative::Composition::implementation { struct WindowsModalHostComponentView - : WindowsModalHostComponentViewT { - using Super = WindowsModalHostComponentViewT; + : WindowsModalHostComponentViewT { + using Super = WindowsModalHostComponentViewT; + + ~WindowsModalHostComponentView(); [[nodiscard]] static winrt::Microsoft::ReactNative::ComponentView Create( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept; + winrt::Microsoft::ReactNative::Composition::Experimental::IVisual VisualToMountChildrenInto() noexcept override; void MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept override; void UnmountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept override; - void HandleCommand(const winrt::Microsoft::ReactNative::HandleCommandArgs &args) noexcept override; - void updateState(facebook::react::State::Shared const &state, facebook::react::State::Shared const &oldState) noexcept - override; - void updateProps(facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept - override; + void AdjustWindowSize() noexcept; + void updateLayoutMetrics( facebook::react::LayoutMetrics const &layoutMetrics, facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept override; + + void updateProps(facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept + override; static facebook::react::SharedViewProps defaultProps() noexcept; const facebook::react::ModalHostViewProps &modalHostViewProps() const noexcept; bool focusable() const noexcept override; @@ -57,7 +61,9 @@ struct WindowsModalHostComponentView private: HWND m_hwnd{nullptr}; - winrt::Microsoft::ReactNative::ReactContext m_context; + uint64_t m_prevWindowID; + bool m_isVisible{false}; + winrt::Microsoft::ReactNative::ReactNativeIsland m_reactNativeIsland; }; } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp index 96bdb1b91c6..127e73e50d8 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp @@ -127,6 +127,20 @@ ReactNativeIsland::ReactNativeIsland(const winrt::Microsoft::UI::Composition::Co InitTextScaleMultiplier(); } +// Constructor to initialize ReactNativeIsland with context and componentView +ReactNativeIsland::ReactNativeIsland( + const winrt::Microsoft::UI::Composition::Compositor &compositor, + winrt::Microsoft::ReactNative::IReactContext context, + winrt::Microsoft::ReactNative::ComponentView componentView) noexcept + : m_compositor(compositor), + m_context(context), + m_layoutConstraints({{0, 0}, {0, 0}, winrt::Microsoft::ReactNative::LayoutDirection::Undefined}), + m_isFragment(true) { + m_rootTag = componentView.Tag(); + InitTextScaleMultiplier(); + AddFragmentCompositionEventHandler(context, componentView); +} + ReactNativeIsland::ReactNativeIsland() noexcept : ReactNativeIsland(nullptr) {} ReactNativeIsland::~ReactNativeIsland() noexcept { @@ -152,6 +166,7 @@ ReactNative::IReactViewHost ReactNativeIsland::ReactViewHost() noexcept { } void ReactNativeIsland::ReactViewHost(winrt::Microsoft::ReactNative::IReactViewHost const &value) noexcept { + assert(!m_isFragment); // make sure this isn't a FragmentIsalnd if (m_reactViewHost == value) { return; } @@ -404,7 +419,7 @@ void ReactNativeIsland::InitRootView( m_context = winrt::Microsoft::ReactNative::ReactContext(std::move(context)); m_reactViewOptions = std::move(viewOptions); - m_CompositionEventHandler = std::make_shared<::Microsoft::ReactNative::CompositionEventHandler>(m_context, *this); + m_CompositionEventHandler = std::make_shared<::Microsoft::ReactNative::CompositionEventHandler>(m_context, *this, -1); m_CompositionEventHandler->Initialize(); UpdateRootViewInternal(); @@ -412,6 +427,27 @@ void ReactNativeIsland::InitRootView( m_isInitialized = true; } +void ReactNativeIsland::AddFragmentCompositionEventHandler( + winrt::Microsoft::ReactNative::IReactContext context, + winrt::Microsoft::ReactNative::ComponentView componentView) noexcept { + m_uiDispatcher = context.Properties() + .Get(winrt::Microsoft::ReactNative::ReactDispatcherHelper::UIDispatcherProperty()) + .try_as(); + VerifyElseCrash(m_uiDispatcher.HasThreadAccess()); + VerifyElseCrash(m_rootTag != -1); + auto uiManager = ::Microsoft::ReactNative::FabricUIManager::FromProperties( + winrt::Microsoft::ReactNative::ReactPropertyBag(context.Properties())); + + if (!m_CompositionEventHandler) { + // Create CompositionEventHandler if not already created + m_context = winrt::Microsoft::ReactNative::ReactContext(context); + m_CompositionEventHandler = + std::make_shared<::Microsoft::ReactNative::CompositionEventHandler>(m_context, *this, componentView.Tag()); + m_CompositionEventHandler->Initialize(); + m_isInitialized = true; + } +} + void ReactNativeIsland::UpdateRootView() noexcept { VerifyElseCrash(m_uiDispatcher.HasThreadAccess()); VerifyElseCrash(m_isInitialized); @@ -834,7 +870,9 @@ void ReactNativeIsland::OnMounted() noexcept { return; m_mounted = true; if (auto componentView = GetComponentView()) { - componentView->onMounted(); + if (!componentView->isMounted()) { + componentView->onMounted(); + } } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h index f280ddc6a1d..b74be73460b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h @@ -48,6 +48,10 @@ struct ReactNativeIsland ~ReactNativeIsland() noexcept; ReactNativeIsland(const winrt::Microsoft::UI::Composition::Compositor &compositor) noexcept; + ReactNativeIsland( + const winrt::Microsoft::UI::Composition::Compositor &compositor, + winrt::Microsoft::ReactNative::IReactContext context, + winrt::Microsoft::ReactNative::ComponentView componentView) noexcept; winrt::Microsoft::UI::Content::ContentIsland Island(); // property ReactViewHost @@ -117,6 +121,10 @@ struct ReactNativeIsland const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer, facebook::react::Tag tag) noexcept; + void AddFragmentCompositionEventHandler( + winrt::Microsoft::ReactNative::IReactContext context, + winrt::Microsoft::ReactNative::ComponentView componentView) noexcept; + public: // IReactViewInstance UI-thread implementation void InitRootView( winrt::Microsoft::ReactNative::IReactContext &&context, @@ -136,6 +144,7 @@ struct ReactNativeIsland #endif HWND m_hwnd{0}; + bool m_isFragment{false}; bool m_isInitialized{false}; bool m_isJSViewAttached{false}; bool m_hasRenderedVisual{false}; diff --git a/vnext/Microsoft.ReactNative/ReactNativeIsland.idl b/vnext/Microsoft.ReactNative/ReactNativeIsland.idl index 5c1b3f1311f..453bbb25107 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeIsland.idl +++ b/vnext/Microsoft.ReactNative/ReactNativeIsland.idl @@ -78,11 +78,12 @@ namespace Microsoft.ReactNative DOC_STRING("A windows composition component that hosts React Native UI elements.") runtimeclass ReactNativeIsland { - DOC_STRING("Creates a new instance of @ReactNativeIsland.") + DOC_STRING("Creates a new instance of @ReactNativeIsland. Can be implemented with a ReactViewHost or a ComponentView with reactContext") ReactNativeIsland(); #ifdef USE_WINUI3 ReactNativeIsland(Microsoft.UI.Composition.Compositor compositor); + ReactNativeIsland(Microsoft.UI.Composition.Compositor compositor, Microsoft.ReactNative.IReactContext context, Microsoft.ReactNative.ComponentView componentView); #endif DOC_STRING( @@ -97,7 +98,7 @@ namespace Microsoft.ReactNative DOC_STRING("ScaleFactor for this windows (DPI/96)") Single ScaleFactor {get; set;}; - + Single FontSizeMultiplier { get; }; DOC_STRING("Move focus to this @ReactNativeIsland") diff --git a/vnext/overrides.json b/vnext/overrides.json index e9c13f29b08..fde5c51b794 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -632,6 +632,12 @@ "baseFile": "packages/react-native/Libraries/Utilities/Platform.android.js", "baseHash": "33f07d6fddb5290d05c8d4cc490312e3af88c70b" }, + { + "type": "copy", + "file": "src-win/src/private/reactdevtools/ReactDevToolsSettingsManager.windows.js", + "baseFile": "packages/react-native/src/private/reactdevtools/ReactDevToolsSettingsManager.android.js", + "baseHash": "df41b76dc3d2df9455fae588748261d7b0a22d01" + }, { "type": "platform", "file": "src-win/src/private/specs/modules/NativeAppTheme.js" @@ -647,12 +653,6 @@ "file": "src-win/src/private/specs/modules/NativePlatformConstantsWindows.js", "baseFile": "packages/react-native/src/private/specs/modules/NativePlatformConstantsAndroid.js", "baseHash": "fa0f34a2de33b641bd63863629087644796d8b59" - }, - { - "type": "copy", - "file": "src-win/src/private/reactdevtools/ReactDevToolsSettingsManager.windows.js", - "baseFile": "packages/react-native/src/private/reactdevtools/ReactDevToolsSettingsManager.android.js", - "baseHash": "df41b76dc3d2df9455fae588748261d7b0a22d01" } ] } \ No newline at end of file