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..34ec0adf53a
--- /dev/null
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp
@@ -0,0 +1,98 @@
+// 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
+
+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 SetXamlRoot + VisualTreeHelper)
+
+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: Register XamlRoot to enable popup dismissal when scroll begins.
+ // This is the GENERIC pattern that ANY 3rd party XAML component should use:
+ // 1. Create your XamlIsland and set its Content
+ // 2. Call SetXamlRoot() with the content's XamlRoot
+ // When the parent ScrollView starts scrolling, ContentIslandComponentView will use
+ // VisualTreeHelper.GetOpenPopupsForXamlRoot() to find and close ALL open popups.
+ // This works for ComboBox, DatePicker, TimePicker, Flyouts, etc. - any XAML popup!
+ m_comboBox.Loaded([islandView, this](auto const &, auto const &) {
+ // XamlRoot is available after the element is loaded
+ if (auto xamlRoot = m_comboBox.XamlRoot()) {
+ islandView.SetXamlRoot(xamlRoot);
+ }
+ });
+
+ 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);
+ }
+ });
+ }
+
+ private:
+ winrt::Microsoft::UI::Xaml::XamlIsland m_xamlIsland{nullptr};
+ winrt::Microsoft::UI::Xaml::Controls::ComboBox m_comboBox{nullptr};
+ winrt::event_token m_selectionChangedToken{};
+};
+
+} // 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..6e5e4c0200a 100644
--- a/vnext/Microsoft.ReactNative/CompositionComponentView.idl
+++ b/vnext/Microsoft.ReactNative/CompositionComponentView.idl
@@ -11,161 +11,128 @@ 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);
+};
+
+// Delegate for popup dismissal callback (Issue #15557)
+// 3rd party XAML components can register this callback to receive dismiss notifications
+[experimental][webhosthidden] runtimeclass ContentIslandComponentView : ViewComponentView {
+ void Connect(Microsoft.UI.Content.ContentIsland contentIsland);
+
+ // Issue #15557: Register the XamlRoot for this ContentIsland to enable popup dismissal.
+ // When a parent ScrollView starts scrolling, DismissPopups() will be called which uses
+ // VisualTreeHelper.GetOpenPopupsForXamlRoot() to find and close all open XAML popups.
+ // 3rd party XAML components (ComboBox, DatePicker, etc.) should call this after creating
+ // their XamlIsland: islandView.SetXamlRoot(m_xamlIsland.Content().XamlRoot());
+ void SetXamlRoot(Microsoft.UI.Xaml.XamlRoot xamlRoot);
+};
+
+[experimental][webhosthidden][default_interface] runtimeclass SwitchComponentView : ViewComponentView{};
+
+[experimental][webhosthidden][default_interface] runtimeclass RootComponentView : ViewComponentView {
+ Microsoft.ReactNative.ComponentView GetFocusedComponent();
+ Microsoft.ReactNative.ReactNativeIsland ReactNativeIsland {
+ get;
};
-
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass SwitchComponentView : ViewComponentView {
+ DOC_STRING("Is non-null if this RootComponentView is the content of a portal")
+ PortalComponentView Portal {
+ get;
};
+};
- [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] 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]
- 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 DebuggingOverlayComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass DebuggingOverlayComponentView : ViewComponentView {
- };
+[experimental][webhosthidden][default_interface] runtimeclass ActivityIndicatorComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass ActivityIndicatorComponentView : ViewComponentView {
- };
+[experimental][webhosthidden][default_interface] runtimeclass WindowsModalHostComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass WindowsModalHostComponentView : ViewComponentView {
+[experimental][webhosthidden][default_interface] runtimeclass ImageComponentView : ViewComponentView {
+ Microsoft.ReactNative.ImageProps ViewProps {
+ get;
};
+};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass ImageComponentView : ViewComponentView {
- Microsoft.ReactNative.ImageProps ViewProps { get; };
- };
+[experimental][webhosthidden][default_interface] runtimeclass ParagraphComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass ParagraphComponentView : ViewComponentView {
- };
+[experimental][webhosthidden][default_interface] runtimeclass ScrollViewComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass ScrollViewComponentView : ViewComponentView {
- };
+[experimental][webhosthidden][default_interface] runtimeclass UnimplementedNativeViewComponentView
+ : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass UnimplementedNativeViewComponentView : ViewComponentView {
- };
+[experimental][webhosthidden][default_interface] runtimeclass WindowsTextInputComponentView : ViewComponentView{};
- [experimental]
- [webhosthidden]
- [default_interface]
- runtimeclass WindowsTextInputComponentView : ViewComponentView {
- };
-
-} // namespace Microsoft.ReactNative
+} // namespace Microsoft.ReactNative. Composition
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
index 41215fea0e2..d82d5a8b484 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
@@ -11,6 +11,8 @@
#include
#include
#include
+#include
+#include
#include
#include "CompositionContextHelper.h"
#include "RootComponentView.h"
@@ -166,6 +168,30 @@ void ContentIslandComponentView::onGotFocus(
m_navigationHost.NavigateFocus(winrt::Microsoft::UI::Input::FocusNavigationRequest::Create(navigationReason));
}
+// Issue #15557: Allow 3rd party XAML components to register their XamlRoot for popup dismissal.
+// This enables the generic tree-walking approach to close all open popups when scroll begins.
+void ContentIslandComponentView::SetXamlRoot(winrt::Microsoft::UI::Xaml::XamlRoot const &xamlRoot) noexcept {
+ m_xamlRoot = xamlRoot;
+}
+
+// Issue #15557: Dismiss any open XAML popups when scroll begins.
+// This uses VisualTreeHelper.GetOpenPopupsForXamlRoot() to find all open popups
+// and closes them - implementing light dismiss behavior for ANY XAML control.
+// This is the generic approach that works for all 3rd party XAML components.
+void ContentIslandComponentView::DismissPopups() noexcept {
+ if (m_xamlRoot) {
+ // Get all open popups for this XamlRoot using VisualTreeHelper
+ auto openPopups = winrt::Microsoft::UI::Xaml::Media::VisualTreeHelper::GetOpenPopupsForXamlRoot(m_xamlRoot);
+
+ // Close each open popup
+ for (const auto &popup : openPopups) {
+ if (popup.IsOpen()) {
+ popup.IsOpen(false);
+ }
+ }
+ }
+}
+
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 e6f5fe8808a..0b47887f738 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
#include "CompositionHelpers.h"
#include "CompositionViewComponentView.h"
@@ -47,6 +48,12 @@ struct ContentIslandComponentView : ContentIslandComponentViewT
#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,46 @@ 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: Dismiss XAML popups in child ContentIslandComponentView instances when scroll begins.
+// This implements light dismiss behavior for controls like ComboBox dropdown.
+void ScrollViewComponentView::DismissChildContentIslandPopups() noexcept {
+ // Helper lambda to recursively find and dismiss popups in all ContentIslandComponentView children
+ std::function dismissPopupsRecursively =
+ [&dismissPopupsRecursively](const winrt::Microsoft::ReactNative::ComponentView &view) {
+ // Check if this view is a ContentIslandComponentView
+ if (auto contentIsland =
+ view.try_as()) {
+ winrt::get_self(contentIsland)->DismissPopups();
+ }
+
+ // Recursively check children
+ for (auto child : view.Children()) {
+ dismissPopupsRecursively(child);
+ }
+ };
+
+ // Start recursive search from this ScrollView's children
+ for (auto child : Children()) {
+ dismissPopupsRecursively(child);
+ }
+}
} // 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..7f38e2bf1f9 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h
@@ -129,6 +129,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: Dismiss XAML popups in 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;