diff --git a/change/@react-native-windows-automation-0a641d65-6bc9-4769-8666-f856ff15f040.json b/change/@react-native-windows-automation-0a641d65-6bc9-4769-8666-f856ff15f040.json
new file mode 100644
index 00000000000..db317b9bef6
--- /dev/null
+++ b/change/@react-native-windows-automation-0a641d65-6bc9-4769-8666-f856ff15f040.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Fix XAML popup positioning and light dismiss in ScrollView (#15557)",
+ "packageName": "@react-native-windows/automation",
+ "email": "nitchaudhary@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@react-native-windows-automation-channel-e7ad73ac-7704-441b-81a3-2d8f97ece377.json b/change/@react-native-windows-automation-channel-e7ad73ac-7704-441b-81a3-2d8f97ece377.json
new file mode 100644
index 00000000000..3c13d2e78d7
--- /dev/null
+++ b/change/@react-native-windows-automation-channel-e7ad73ac-7704-441b-81a3-2d8f97ece377.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Fix XAML popup positioning and light dismiss in ScrollView (#15557)",
+ "packageName": "@react-native-windows/automation-channel",
+ "email": "nitchaudhary@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@react-native-windows-automation-commands-6b4b918c-021a-41a9-8eac-5ed58b0cec22.json b/change/@react-native-windows-automation-commands-6b4b918c-021a-41a9-8eac-5ed58b0cec22.json
new file mode 100644
index 00000000000..1f3e4e525ce
--- /dev/null
+++ b/change/@react-native-windows-automation-commands-6b4b918c-021a-41a9-8eac-5ed58b0cec22.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Fix XAML popup positioning and light dismiss in ScrollView (#15557)",
+ "packageName": "@react-native-windows/automation-commands",
+ "email": "nitchaudhary@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/react-native-windows-83ceb93b-b350-41ed-a00b-0cd4927d42cb.json b/change/react-native-windows-83ceb93b-b350-41ed-a00b-0cd4927d42cb.json
new file mode 100644
index 00000000000..864a7eab89c
--- /dev/null
+++ b/change/react-native-windows-83ceb93b-b350-41ed-a00b-0cd4927d42cb.json
@@ -0,0 +1,7 @@
+{
+ "comment": "Fix XAML popup positioning and light dismiss in ScrollView (#15557)",
+ "type": "prerelease",
+ "packageName": "react-native-windows",
+ "email": "nitchaudhary@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/playground/Samples/xamlPopupBug.tsx b/packages/playground/Samples/xamlPopupBug.tsx
new file mode 100644
index 00000000000..db5459005eb
--- /dev/null
+++ b/packages/playground/Samples/xamlPopupBug.tsx
@@ -0,0 +1,152 @@
+/**
+ * XAML Popup Positioning Bug Repro - Issue #15557
+ *
+ * HOW TO REPRO:
+ * 1. Run this sample in Playground
+ * 2. SCROLL DOWN in the ScrollView
+ * 3. Click on the ComboBox to open the dropdown popup
+ * 4. BUG: The popup appears at the WRONG position!
+ *
+ * The popup offset = how much you scrolled
+ */
+
+import React from 'react';
+import {AppRegistry, ScrollView, View, Text, StyleSheet} from 'react-native';
+import {ComboBox} from 'sample-custom-component';
+
+const XamlPopupBugRepro = () => {
+ const [selectedValue, setSelectedValue] = React.useState('(click to select)');
+
+ return (
+
+ {/* Header - Fixed at top */}
+
+ XAML Popup Bug Repro #15557
+ Selected: {selectedValue}
+
+
+ {/* Instructions */}
+
+ 1. SCROLL DOWN in the box below
+ 2. Click a ComboBox to open dropdown
+ 3. See the popup at WRONG position!
+
+
+ {/* Scrollable area with ComboBoxes */}
+
+
+ SCROLL DOWN
+
+
+
+ Keep scrolling...
+
+
+
+ Almost there...
+
+
+ {/* First ComboBox */}
+
+ ComboBox 1 - Click me!
+ {
+ setSelectedValue(`CB1: ${e.nativeEvent.selectedValue}`);
+ }}
+ />
+
+
+
+ More space...
+
+
+ {/* Second ComboBox */}
+
+ ComboBox 2 - Click me!
+ {
+ setSelectedValue(`CB2: ${e.nativeEvent.selectedValue}`);
+ }}
+ />
+
+
+
+ End of content
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#1a1a2e',
+ },
+ header: {
+ padding: 20,
+ backgroundColor: '#16213e',
+ alignItems: 'center',
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#fff',
+ },
+ subtitle: {
+ fontSize: 16,
+ color: '#0f0',
+ marginTop: 5,
+ },
+ instructions: {
+ padding: 15,
+ backgroundColor: '#0f3460',
+ },
+ step: {
+ fontSize: 18,
+ color: '#fff',
+ marginVertical: 3,
+ },
+ scrollView: {
+ flex: 1,
+ margin: 10,
+ borderWidth: 3,
+ borderColor: '#e94560',
+ borderRadius: 10,
+ },
+ spacer: {
+ height: 200,
+ justifyContent: 'center',
+ alignItems: 'center',
+ margin: 10,
+ borderRadius: 10,
+ },
+ spacerText: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#fff',
+ },
+ comboBoxContainer: {
+ margin: 10,
+ padding: 15,
+ backgroundColor: '#fff',
+ borderRadius: 10,
+ borderWidth: 3,
+ borderColor: '#e94560',
+ },
+ comboLabel: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 10,
+ color: '#1a1a2e',
+ },
+ comboBox: {
+ width: 350,
+ height: 60,
+ },
+});
+
+AppRegistry.registerComponent('Bootstrap', () => XamlPopupBugRepro);
+export default XamlPopupBugRepro;
diff --git a/packages/playground/windows/playground-composition/Playground-Composition.cpp b/packages/playground/windows/playground-composition/Playground-Composition.cpp
index 0ca8491496f..e13b70f8238 100644
--- a/packages/playground/windows/playground-composition/Playground-Composition.cpp
+++ b/packages/playground/windows/playground-composition/Playground-Composition.cpp
@@ -379,7 +379,8 @@ struct WindowData {
LR"(Samples\mouse)", LR"(Samples\scrollViewSnapSample)",
LR"(Samples\simple)", LR"(Samples\text)",
LR"(Samples\textinput)", LR"(Samples\ticTacToe)",
- LR"(Samples\view)", LR"(Samples\debugTest01)"};
+ LR"(Samples\view)", LR"(Samples\debugTest01)",
+ LR"(Samples\xamlPopupBug)"};
static INT_PTR CALLBACK Bundle(HWND hwnd, UINT message, WPARAM wparam, LPARAM /*lparam*/) noexcept {
switch (message) {
diff --git a/packages/sample-custom-component/src/FabricXamlComboBoxNativeComponent.ts b/packages/sample-custom-component/src/FabricXamlComboBoxNativeComponent.ts
new file mode 100644
index 00000000000..6ede7ffac5b
--- /dev/null
+++ b/packages/sample-custom-component/src/FabricXamlComboBoxNativeComponent.ts
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT License.
+ * @format
+ * @flow
+ */
+
+'use strict';
+
+// ComboBox component for testing XAML popup positioning bug #15557
+// The ComboBox dropdown popup should appear at the correct position after scrolling
+
+import {codegenNativeComponent} from 'react-native';
+import type {ViewProps} from 'react-native';
+import type {
+ DirectEventHandler,
+ Int32,
+} from 'react-native/Libraries/Types/CodegenTypes';
+
+type SelectionChangedEvent = Readonly<{
+ selectedIndex: Int32;
+ selectedValue: string;
+}>;
+
+export interface ComboBoxProps extends ViewProps {
+ selectedIndex?: Int32;
+ placeholder?: string;
+ onSelectionChanged?: DirectEventHandler;
+}
+
+export default codegenNativeComponent('ComboBox');
diff --git a/packages/sample-custom-component/src/index.ts b/packages/sample-custom-component/src/index.ts
index 49b2bd07d43..eb312b6a4dd 100644
--- a/packages/sample-custom-component/src/index.ts
+++ b/packages/sample-custom-component/src/index.ts
@@ -5,6 +5,8 @@ import DrawingIsland from './DrawingIsland';
import CalendarView from './FabricXamlCalendarViewNativeComponent'
+import ComboBox from './FabricXamlComboBoxNativeComponent'
+
import CustomAccessibility from './CustomAccessibilityNativeComponent';
export {
@@ -13,4 +15,5 @@ export {
MovingLight,
MovingLightHandle,
CalendarView,
-};
\ No newline at end of file
+ ComboBox,
+};
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp
new file mode 100644
index 00000000000..6b8f57b865c
--- /dev/null
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+// ComboBox component for testing XAML popup positioning bug #15557
+#include "pch.h"
+
+#include "ComboBox.h"
+
+#if defined(RNW_NEW_ARCH)
+
+#include "codegen/react/components/SampleCustomComponent/ComboBox.g.h"
+
+#include
+#include
+#include
+
+namespace winrt::SampleCustomComponent {
+
+// ComboBox component to test popup positioning issue #15557
+// When inside a ScrollView, the dropdown popup should appear at the correct position
+// Bug 1: After scrolling, the popup appears at the wrong offset (FIXED via LayoutMetricsChanged)
+// Bug 2: When popup is open and user scrolls, popup should dismiss (FIXED via DismissPopupsRequest event)
+
+struct ComboBoxComponentView : public winrt::implements,
+ Codegen::BaseComboBox {
+ void InitializeContentIsland(
+ const winrt::Microsoft::ReactNative::Composition::ContentIslandComponentView &islandView) noexcept {
+ m_xamlIsland = winrt::Microsoft::UI::Xaml::XamlIsland{};
+ m_comboBox = winrt::Microsoft::UI::Xaml::Controls::ComboBox{};
+
+ // Add default items
+ m_comboBox.Items().Append(winrt::box_value(L"Option 1 - Select me after scrolling"));
+ m_comboBox.Items().Append(winrt::box_value(L"Option 2 - Test popup position"));
+ m_comboBox.Items().Append(winrt::box_value(L"Option 3 - Bug #15557"));
+ m_comboBox.Items().Append(winrt::box_value(L"Option 4 - Popup should be here"));
+ m_comboBox.Items().Append(winrt::box_value(L"Option 5 - Not somewhere else!"));
+
+ m_comboBox.PlaceholderText(L"Click to open dropdown...");
+ m_comboBox.FontSize(20);
+ m_comboBox.HorizontalAlignment(winrt::Microsoft::UI::Xaml::HorizontalAlignment::Stretch);
+ m_comboBox.VerticalAlignment(winrt::Microsoft::UI::Xaml::VerticalAlignment::Center);
+
+ m_xamlIsland.Content(m_comboBox);
+ islandView.Connect(m_xamlIsland.ContentIsland());
+
+ // Issue #15557 Bug 2 Fix: Subscribe to DismissPopupsRequest event to close popups when scroll begins.
+ // This is the pattern that ANY 3rd party XAML component should use:
+ // 1. Subscribe to the DismissPopupsRequest event
+ // 2. When the event fires, use VisualTreeHelper to find and close your open popups
+ // This works for ComboBox, DatePicker, TimePicker, Flyouts, etc. - any XAML popup!
+ m_dismissPopupsRequestToken = islandView.DismissPopupsRequest(
+ [this](winrt::Windows::Foundation::IInspectable const &, winrt::Windows::Foundation::IInspectable const &) {
+ DismissPopups();
+ });
+
+ m_selectionChangedToken =
+ m_comboBox.SelectionChanged([this](
+ winrt::Windows::Foundation::IInspectable const &,
+ winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const &) {
+ if (auto emitter = EventEmitter()) {
+ Codegen::ComboBox_OnSelectionChanged args;
+ args.selectedIndex = m_comboBox.SelectedIndex();
+ if (m_comboBox.SelectedItem()) {
+ auto selectedText = winrt::unbox_value(m_comboBox.SelectedItem());
+ args.selectedValue = winrt::to_string(selectedText);
+ } else {
+ args.selectedValue = "";
+ }
+ emitter->onSelectionChanged(args);
+ }
+ });
+ }
+
+ // Dismiss any open popups for this component's XamlRoot
+ void DismissPopups() noexcept {
+ if (auto xamlRoot = m_comboBox.XamlRoot()) {
+ auto openPopups = winrt::Microsoft::UI::Xaml::Media::VisualTreeHelper::GetOpenPopupsForXamlRoot(xamlRoot);
+ for (const auto &popup : openPopups) {
+ if (popup.IsOpen()) {
+ popup.IsOpen(false);
+ }
+ }
+ }
+ }
+
+ private:
+ winrt::Microsoft::UI::Xaml::XamlIsland m_xamlIsland{nullptr};
+ winrt::Microsoft::UI::Xaml::Controls::ComboBox m_comboBox{nullptr};
+ winrt::event_token m_selectionChangedToken{};
+ winrt::event_token m_dismissPopupsRequestToken{};
+};
+
+} // namespace winrt::SampleCustomComponent
+
+void RegisterComboBoxComponentView(winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) {
+ winrt::SampleCustomComponent::Codegen::RegisterComboBoxNativeComponent<
+ winrt::SampleCustomComponent::ComboBoxComponentView>(
+ packageBuilder,
+ [](const winrt::Microsoft::ReactNative::Composition::IReactCompositionViewComponentBuilder &builder) {
+ builder.SetContentIslandComponentViewInitializer(
+ [](const winrt::Microsoft::ReactNative::Composition::ContentIslandComponentView &islandView) noexcept {
+ auto userData = winrt::make_self();
+ userData->InitializeContentIsland(islandView);
+ islandView.UserData(*userData);
+ });
+ });
+}
+
+#endif // defined(RNW_NEW_ARCH)
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.h b/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.h
new file mode 100644
index 00000000000..9cbbe36b992
--- /dev/null
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.h
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+
+#include
+
+void RegisterComboBoxComponentView(winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder);
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp
index 29f8c8905e1..8f19b9be9cf 100644
--- a/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp
@@ -8,6 +8,7 @@
#endif
#include "CalendarView.h"
+#include "ComboBox.h"
#include "CustomAccessibility.h"
#include "DrawingIsland.h"
#include "MovingLight.h"
@@ -24,6 +25,7 @@ void ReactPackageProvider::CreatePackage(IReactPackageBuilder const &packageBuil
RegisterMovingLightNativeComponent(packageBuilder);
RegisterCalendarViewComponentView(packageBuilder);
RegisterCustomAccessibilityComponentView(packageBuilder);
+ RegisterComboBoxComponentView(packageBuilder);
#endif // #ifdef RNW_NEW_ARCH
}
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj
index 2a9dae1e5fe..8462d529796 100644
--- a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj
@@ -101,6 +101,7 @@
+
DrawingIsland.idl
@@ -126,6 +127,7 @@
ReactPackageProvider.idl
+
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj.filters b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj.filters
index 362d95add21..6b6b592ca9f 100644
--- a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj.filters
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj.filters
@@ -1,4 +1,4 @@
-
+
@@ -27,6 +27,9 @@
Header Files
+
+ Header Files
+
@@ -38,6 +41,9 @@
Source Files
+
+ Source Files
+
diff --git a/vnext/Microsoft.ReactNative/CompositionComponentView.idl b/vnext/Microsoft.ReactNative/CompositionComponentView.idl
index d155d3290b6..2192d4aa72d 100644
--- a/vnext/Microsoft.ReactNative/CompositionComponentView.idl
+++ b/vnext/Microsoft.ReactNative/CompositionComponentView.idl
@@ -11,161 +11,127 @@ import "ReactNativeIsland.idl";
#include "DocString.h"
-namespace Microsoft.ReactNative.Composition
-{
-
- [flags]
- [webhosthidden]
- [experimental]
- enum ComponentViewFeatures
- {
- None = 0x00000000,
- NativeBorder = 0x00000001,
- ShadowProps = 0x00000002,
- Background = 0x00000004,
- FocusVisual = 0x00000008,
-
- Default = 0x0000000F, // ShadowProps | NativeBorder | Background | FocusVisual
+namespace Microsoft.ReactNative.Composition {
+
+[flags][webhosthidden][experimental] enum ComponentViewFeatures {
+ None = 0x00000000,
+ NativeBorder = 0x00000001,
+ ShadowProps = 0x00000002,
+ Background = 0x00000004,
+ FocusVisual = 0x00000008,
+
+ Default = 0x0000000F, // ShadowProps | NativeBorder | Background | FocusVisual
+};
+
+namespace Experimental {
+[webhosthidden][experimental] interface IInternalComponentView {
+ ICompositionContext CompositionContext {
+ get;
};
-
- namespace Experimental {
- [webhosthidden]
- [experimental]
- interface IInternalComponentView
- {
- ICompositionContext CompositionContext { get; };
- }
- }
-
- // [exclusiveto(ComponentView)]
- // [uuid(ABFAC092-E527-47DC-9CF9-7A4003B0AFB0)]
- // interface IComponentViewFactory
- // {
- // }
-
- // [composable(IComponentViewFactory, protected)]
- [experimental]
- [webhosthidden]
- unsealed runtimeclass ComponentView : Microsoft.ReactNative.ComponentView {
- Microsoft.UI.Composition.Compositor Compositor { get; };
- RootComponentView Root { get; };
- Theme Theme;
-
- event Windows.Foundation.EventHandler ThemeChanged;
- Boolean CapturePointer(Microsoft.ReactNative.Composition.Input.Pointer pointer);
- void ReleasePointerCapture(Microsoft.ReactNative.Composition.Input.Pointer pointer);
+}
+} // namespace Experimental
+
+// [exclusiveto(ComponentView)]
+// [uuid(ABFAC092-E527-47DC-9CF9-7A4003B0AFB0)]
+// interface IComponentViewFactory
+// {
+// }
+
+// [composable(IComponentViewFactory, protected)]
+[experimental][webhosthidden] unsealed runtimeclass ComponentView : Microsoft.ReactNative.ComponentView {
+ Microsoft.UI.Composition.Compositor Compositor {
+ get;
+ };
+ RootComponentView Root {
+ get;
};
+ Theme Theme;
- namespace Experimental {
+ event Windows.Foundation.EventHandler ThemeChanged;
+ Boolean CapturePointer(Microsoft.ReactNative.Composition.Input.Pointer pointer);
+ void ReleasePointerCapture(Microsoft.ReactNative.Composition.Input.Pointer pointer);
+};
- [webhosthidden]
- [experimental]
- delegate Microsoft.ReactNative.Composition.Experimental.IVisual CreateInternalVisualDelegate(Microsoft.ReactNative.ComponentView view);
+namespace Experimental {
- [webhosthidden]
- [experimental]
- DOC_STRING("Custom ViewComponents need to implement this interface to be able to provide a custom"
+[webhosthidden][experimental] delegate Microsoft.ReactNative.Composition.Experimental.IVisual
+CreateInternalVisualDelegate(Microsoft.ReactNative.ComponentView view);
+
+[webhosthidden][experimental] DOC_STRING(
+ "Custom ViewComponents need to implement this interface to be able to provide a custom"
" visual using the composition context that allows custom compositors. This is only required for"
" custom components that need to support running in RNW instances with custom compositors. Most"
- " custom components can just set CreateVisualHandler on ViewComponentView."
- " This will be removed in a future version")
- interface IInternalCreateVisual
- {
- CreateInternalVisualDelegate CreateInternalVisualHandler;
- }
- }
-
- // [exclusiveto(ViewComponentView)]
- // [uuid(756AA1DF-ED74-467E-9BAA-3797B39B1875)]
- // interface IViewComponentViewFactory
- // {
- // }
-
- // [composable(IViewComponentViewFactory, protected)]
- [experimental]
- [webhosthidden]
- unsealed runtimeclass ViewComponentView : ComponentView {
-
- Microsoft.ReactNative.ViewProps ViewProps { get; };
+ " custom components can just set CreateVisualHandler on ViewComponentView."
+ " This will be removed in a future version") interface IInternalCreateVisual {
+ CreateInternalVisualDelegate CreateInternalVisualHandler;
+}
+} // namespace Experimental
+
+// [exclusiveto(ViewComponentView)]
+// [uuid(756AA1DF-ED74-467E-9BAA-3797B39B1875)]
+// interface IViewComponentViewFactory
+// {
+// }
+
+// [composable(IViewComponentViewFactory, protected)]
+[experimental][webhosthidden] unsealed runtimeclass ViewComponentView : ComponentView {
+ Microsoft.ReactNative.ViewProps ViewProps {
+ get;
};
+};
- [experimental]
- [webhosthidden]
- runtimeclass ContentIslandComponentView : ViewComponentView {
- void Connect(Microsoft.UI.Content.ContentIsland contentIsland);
- };
+[experimental][webhosthidden] runtimeclass ContentIslandComponentView : ViewComponentView {
+ void Connect(Microsoft.UI.Content.ContentIsland contentIsland);
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass SwitchComponentView : ViewComponentView {
- };
+ // Issue #15557: Event fired when a parent ScrollView starts scrolling.
+ // 3rd party XAML components should subscribe to this event to dismiss any open popups.
+ event Windows.Foundation.EventHandler DismissPopupsRequest;
+};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass RootComponentView : ViewComponentView {
- Microsoft.ReactNative.ComponentView GetFocusedComponent();
- Microsoft.ReactNative.ReactNativeIsland ReactNativeIsland { get; };
- DOC_STRING("Is non-null if this RootComponentView is the content of a portal")
- PortalComponentView Portal { get; };
- };
+[experimental][webhosthidden][default_interface] runtimeclass SwitchComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- DOC_STRING("Used to implement UI that is hosted outside the main UI tree, such as modal.")
- runtimeclass PortalComponentView : Microsoft.ReactNative.ComponentView {
- RootComponentView ContentRoot { get; };
+[experimental][webhosthidden][default_interface] runtimeclass RootComponentView : ViewComponentView {
+ Microsoft.ReactNative.ComponentView GetFocusedComponent();
+ Microsoft.ReactNative.ReactNativeIsland ReactNativeIsland {
+ get;
};
-
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass DebuggingOverlayComponentView : ViewComponentView {
+ DOC_STRING("Is non-null if this RootComponentView is the content of a portal")
+ PortalComponentView Portal {
+ get;
};
+};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass ActivityIndicatorComponentView : ViewComponentView {
+[experimental][webhosthidden][default_interface] DOC_STRING(
+ "Used to implement UI that is hosted outside the main UI tree, such as modal.") runtimeclass PortalComponentView
+ : Microsoft.ReactNative.ComponentView {
+ RootComponentView ContentRoot {
+ get;
};
+};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass WindowsModalHostComponentView : ViewComponentView {
- };
+[experimental][webhosthidden][default_interface] runtimeclass DebuggingOverlayComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass ImageComponentView : ViewComponentView {
- Microsoft.ReactNative.ImageProps ViewProps { get; };
- };
+[experimental][webhosthidden][default_interface] runtimeclass ActivityIndicatorComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass ParagraphComponentView : ViewComponentView {
- };
+[experimental][webhosthidden][default_interface] runtimeclass WindowsModalHostComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass ScrollViewComponentView : ViewComponentView {
+[experimental][webhosthidden][default_interface] runtimeclass ImageComponentView : ViewComponentView {
+ Microsoft.ReactNative.ImageProps ViewProps {
+ get;
};
+};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass UnimplementedNativeViewComponentView : ViewComponentView {
- };
+[experimental][webhosthidden][default_interface] runtimeclass ParagraphComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass WindowsTextInputComponentView : ViewComponentView {
- };
-
-} // namespace Microsoft.ReactNative
+[experimental][webhosthidden][default_interface] runtimeclass ScrollViewComponentView : ViewComponentView {
+ // Issue #15557: Event fired when scroll drag begins.
+ // ContentIslandComponentView uses this to know when to dismiss popups (light dismiss).
+ event Windows.Foundation.EventHandler ScrollBeginDrag;
+};
+
+[experimental][webhosthidden][default_interface] runtimeclass UnimplementedNativeViewComponentView
+ : ViewComponentView{};
+
+[experimental][webhosthidden][default_interface] runtimeclass WindowsTextInputComponentView : ViewComponentView{};
+
+} // namespace Microsoft.ReactNative. Composition
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
index 84d73ef2899..420f3a4102f 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
@@ -14,6 +14,7 @@
#include
#include "CompositionContextHelper.h"
#include "RootComponentView.h"
+#include "ScrollViewComponentView.h"
#include "Composition.ContentIslandComponentView.g.cpp"
@@ -49,6 +50,14 @@ void ContentIslandComponentView::OnMounted() noexcept {
.as());
m_childSiteLink.ActualSize({m_layoutMetrics.frame.size.width, m_layoutMetrics.frame.size.height});
+ // Issue #15557: Set initial LocalToParentTransformMatrix synchronously before Connect.
+ // This fixes popup position being wrong even without scrolling.
+ // Note: getClientRect() returns physical pixels, but LocalToParentTransformMatrix expects DIPs.
+ auto clientRect = getClientRect();
+ float scaleFactor = m_layoutMetrics.pointScaleFactor;
+ m_childSiteLink.LocalToParentTransformMatrix(winrt::Windows::Foundation::Numerics::make_float4x4_translation(
+ static_cast(clientRect.left) / scaleFactor, static_cast(clientRect.top) / scaleFactor, 0.0f));
+
m_navigationHost = winrt::Microsoft::UI::Input::InputFocusNavigationHost::GetForSiteLink(m_childSiteLink);
m_navigationHostDepartFocusRequestedToken =
@@ -80,12 +89,34 @@ void ContentIslandComponentView::OnMounted() noexcept {
strongThis->ParentLayoutChanged();
}
}));
+
+ // Issue #15557: Register for ScrollBeginDrag on parent ScrollViews for light dismiss.
+ // This is more efficient than walking the tree on every scroll begin.
+ if (auto scrollView = view.try_as()) {
+ auto token =
+ scrollView.ScrollBeginDrag([wkThis = get_weak()](const winrt::IInspectable &, const winrt::IInspectable &) {
+ if (auto strongThis = wkThis.get()) {
+ strongThis->FireDismissPopupsRequest();
+ }
+ });
+ m_scrollBeginDragSubscriptions.push_back({scrollView, token});
+ }
+
view = view.Parent();
}
}
void ContentIslandComponentView::OnUnmounted() noexcept {
m_layoutMetricChangedRevokers.clear();
+
+ // Issue #15557: Unsubscribe from parent ScrollView events
+ for (auto &subscription : m_scrollBeginDragSubscriptions) {
+ if (auto scrollView = subscription.scrollView.get()) {
+ scrollView.ScrollBeginDrag(subscription.token);
+ }
+ }
+ m_scrollBeginDragSubscriptions.clear();
+
if (m_navigationHostDepartFocusRequestedToken && m_navigationHost) {
m_navigationHost.DepartFocusRequested(m_navigationHostDepartFocusRequestedToken);
m_navigationHostDepartFocusRequestedToken = {};
@@ -93,21 +124,25 @@ void ContentIslandComponentView::OnUnmounted() noexcept {
}
void ContentIslandComponentView::ParentLayoutChanged() noexcept {
- if (m_layoutChangePosted)
- return;
-
- m_layoutChangePosted = true;
- ReactContext().UIDispatcher().Post([wkThis = get_weak()]() {
- if (auto strongThis = wkThis.get()) {
- auto clientRect = strongThis->getClientRect();
-
- strongThis->m_childSiteLink.LocalToParentTransformMatrix(
- winrt::Windows::Foundation::Numerics::make_float4x4_translation(
- static_cast(clientRect.left), static_cast(clientRect.top), 0.0f));
-
- strongThis->m_layoutChangePosted = false;
- }
- });
+ // Issue #15557: Update transform synchronously to ensure correct popup position
+ // when user clicks. Async updates via UIDispatcher().Post() were causing the
+ // popup to open with stale transform values.
+ //
+ // Note: The original async approach was for batching notifications during layout passes.
+ // However, LocalToParentTransformMatrix is a cheap call (just sets a matrix), and
+ // synchronous updates are required to ensure correct popup position when clicked.
+ //
+ // getClientRect() returns values in physical pixels (scaled by pointScaleFactor),
+ // but LocalToParentTransformMatrix expects logical pixels (DIPs). We need to divide
+ // by the scale factor to convert.
+ auto clientRect = getClientRect();
+ float scaleFactor = m_layoutMetrics.pointScaleFactor;
+
+ float x = static_cast(clientRect.left) / scaleFactor;
+ float y = static_cast(clientRect.top) / scaleFactor;
+
+ m_childSiteLink.LocalToParentTransformMatrix(
+ winrt::Windows::Foundation::Numerics::make_float4x4_translation(x, y, 0.0f));
}
winrt::Windows::Foundation::IInspectable ContentIslandComponentView::CreateAutomationProvider() noexcept {
@@ -166,6 +201,22 @@ void ContentIslandComponentView::onGotFocus(
m_navigationHost.NavigateFocus(winrt::Microsoft::UI::Input::FocusNavigationRequest::Create(navigationReason));
}
+// Issue #15557: Fire event to notify 3P component to dismiss popups when scroll begins.
+// The 3P component is responsible for closing its own popups.
+void ContentIslandComponentView::FireDismissPopupsRequest() noexcept {
+ m_dismissPopupsRequestEvent(*this, nullptr);
+}
+
+// Issue #15557: Event accessors for DismissPopupsRequest
+winrt::event_token ContentIslandComponentView::DismissPopupsRequest(
+ winrt::Windows::Foundation::EventHandler const &handler) noexcept {
+ return m_dismissPopupsRequestEvent.add(handler);
+}
+
+void ContentIslandComponentView::DismissPopupsRequest(winrt::event_token const &token) noexcept {
+ m_dismissPopupsRequestEvent.remove(token);
+}
+
ContentIslandComponentView::~ContentIslandComponentView() noexcept {
if (m_navigationHostDepartFocusRequestedToken && m_navigationHost) {
m_navigationHost.DepartFocusRequested(m_navigationHostDepartFocusRequestedToken);
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h
index 7c85ee1cc01..fd96d641b46 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h
@@ -47,6 +47,14 @@ struct ContentIslandComponentView : ContentIslandComponentViewT const &handler) noexcept;
+ void DismissPopupsRequest(winrt::event_token const &token) noexcept;
+
ContentIslandComponentView(
const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
facebook::react::Tag tag,
@@ -68,8 +76,19 @@ struct ContentIslandComponentView : ContentIslandComponentViewT scrollView;
+ winrt::event_token token;
+ };
+ std::vector m_scrollBeginDragSubscriptions;
+
// Automation
void ConfigureChildSiteLinkAutomation() noexcept;
+
+ // Issue #15557: Event for notifying 3P components to dismiss popups when scroll begins
+ winrt::event>
+ m_dismissPopupsRequestEvent;
};
} // namespace winrt::Microsoft::ReactNative::Composition::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp
index ca5b83decdc..c60afe5b26f 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp
@@ -6,6 +6,7 @@
#include "ScrollViewComponentView.h"
+#include
#include
#pragma warning(push)
@@ -19,6 +20,8 @@
#include
#include
#include
+#include
+#include "ContentIslandComponentView.h"
#include "JSValueReader.h"
#include "RootComponentView.h"
@@ -1325,6 +1328,11 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp
m_allowNextScrollNoMatterWhat = false;
}
}
+
+ // Issue #15557: Fire LayoutMetricsChanged to notify ContentIslandComponentView instances
+ // that scroll position has changed, so they can update their LocalToParentTransformMatrix
+ // for correct XAML popup positioning
+ FireLayoutMetricsChangedForScrollPositionChange();
});
m_scrollBeginDragRevoker = m_scrollVisual.ScrollBeginDrag(
@@ -1332,6 +1340,9 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp
[this](
winrt::IInspectable const & /*sender*/,
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
+ // Issue #15557: Dismiss any open XAML popups when scroll begins (light dismiss behavior)
+ DismissChildContentIslandPopups();
+
m_allowNextScrollNoMatterWhat = true; // Ensure next scroll event is recorded, regardless of throttle
updateStateWithContentOffset();
auto eventEmitter = GetEventEmitter();
@@ -1478,4 +1489,41 @@ void ScrollViewComponentView::updateShowsVerticalScrollIndicator(bool value) noe
void ScrollViewComponentView::updateDecelerationRate(float value) noexcept {
m_scrollVisual.SetDecelerationRate({value, value, value});
}
+
+// Issue #15557: Fire LayoutMetricsChanged to notify ContentIslandComponentView instances
+// that scroll position has changed, so they can update their LocalToParentTransformMatrix
+// for correct XAML popup positioning.
+void ScrollViewComponentView::FireLayoutMetricsChangedForScrollPositionChange() noexcept {
+ // Create LayoutMetricsChangedArgs with same old/new metrics
+ // The actual scroll offset is handled in getClientOffset() which ContentIslandComponentView
+ // uses when calculating the transform matrix via getClientRect()
+ winrt::Microsoft::ReactNative::LayoutMetrics metrics{
+ {m_layoutMetrics.frame.origin.x,
+ m_layoutMetrics.frame.origin.y,
+ m_layoutMetrics.frame.size.width,
+ m_layoutMetrics.frame.size.height},
+ m_layoutMetrics.pointScaleFactor};
+
+ m_layoutMetricsChangedEvent(
+ *this, winrt::make(metrics, metrics));
+}
+
+// Issue #15557: Event accessors for ScrollBeginDrag
+winrt::event_token ScrollViewComponentView::ScrollBeginDrag(
+ winrt::Windows::Foundation::EventHandler const &handler) noexcept {
+ return m_scrollBeginDragEvent.add(handler);
+}
+
+void ScrollViewComponentView::ScrollBeginDrag(winrt::event_token const &token) noexcept {
+ m_scrollBeginDragEvent.remove(token);
+}
+
+// Issue #15557: Fire ScrollBeginDrag event to notify registered ContentIslandComponentView instances.
+// ContentIslandComponentViews register during their OnMounted by walking up the tree and subscribing
+// to this event on any parent ScrollViewComponentViews. This is more efficient than walking the tree
+// on every scroll begin.
+void ScrollViewComponentView::DismissChildContentIslandPopups() noexcept {
+ // Fire the event to all registered listeners (ContentIslandComponentViews that are descendants of this ScrollView)
+ m_scrollBeginDragEvent(*this, nullptr);
+}
} // namespace winrt::Microsoft::ReactNative::Composition::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h
index 495f0a1e2c4..af282edb57a 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h
@@ -118,6 +118,11 @@ struct ScrollInteractionTrackerOwner : public winrt::implements<
double getVerticalSize() noexcept;
double getHorizontalSize() noexcept;
+ // Issue #15557: Event accessors for ScrollBeginDrag (used by ContentIslandComponentView for light dismiss)
+ winrt::event_token ScrollBeginDrag(
+ winrt::Windows::Foundation::EventHandler const &handler) noexcept;
+ void ScrollBeginDrag(winrt::event_token const &token) noexcept;
+
private:
void updateDecelerationRate(float value) noexcept;
void updateContentVisualSize() noexcept;
@@ -129,6 +134,10 @@ struct ScrollInteractionTrackerOwner : public winrt::implements<
bool scrollRight(float delta, bool animate) noexcept;
void updateBackgroundColor(const facebook::react::SharedColor &color) noexcept;
void updateStateWithContentOffset() noexcept;
+ // Issue #15557: Notify ContentIslandComponentView instances that scroll position has changed
+ void FireLayoutMetricsChangedForScrollPositionChange() noexcept;
+ // Issue #15557: Fire DismissPopupsRequest event on child ContentIslandComponentView instances when scroll begins
+ void DismissChildContentIslandPopups() noexcept;
facebook::react::ScrollViewEventEmitter::Metrics getScrollMetrics(
facebook::react::SharedViewEventEmitter const &eventEmitter,
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) noexcept;
@@ -160,6 +169,10 @@ struct ScrollInteractionTrackerOwner : public winrt::implements<
bool m_allowNextScrollNoMatterWhat{false};
std::chrono::steady_clock::time_point m_lastScrollEventTime{};
std::shared_ptr m_state;
+
+ // Issue #15557: Event for notifying ContentIslandComponentView instances when scroll begins
+ winrt::event>
+ m_scrollBeginDragEvent;
};
} // namespace winrt::Microsoft::ReactNative::Composition::implementation