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;