From 4de944fbc44674e7ec99d61cfe00bd7b9ae3c786 Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Wed, 21 Jan 2026 13:17:38 +0530 Subject: [PATCH 01/10] Fix XAML popup positioning and light dismiss in ScrollView (#15557) - Add SetXamlRoot() API on ContentIslandComponentView for 3rd party XAML components - Add DismissPopups() using VisualTreeHelper.GetOpenPopupsForXamlRoot() - Fire LayoutMetricsChanged on scroll to update popup positions - Dismiss child ContentIsland popups when scroll begins - Add ComboBox sample component demonstrating the pattern - Add xamlPopupBug test sample for playground-composition --- packages/playground/Samples/xamlPopupBug.tsx | 152 +++++++++++ .../Playground-Composition.cpp | 3 +- .../src/FabricXamlComboBoxNativeComponent.ts | 31 +++ packages/sample-custom-component/src/index.ts | 5 +- .../SampleCustomComponent/ComboBox.cpp | 98 +++++++ .../windows/SampleCustomComponent/ComboBox.h | 7 + .../ReactPackageProvider.cpp | 2 + .../SampleCustomComponent.vcxproj | 2 + .../SampleCustomComponent.vcxproj.filters | 8 +- .../CompositionComponentView.idl | 239 ++++++++---------- .../ContentIslandComponentView.cpp | 26 ++ .../Composition/ContentIslandComponentView.h | 10 + .../Composition/ScrollViewComponentView.cpp | 53 ++++ .../Composition/ScrollViewComponentView.h | 4 + 14 files changed, 501 insertions(+), 139 deletions(-) create mode 100644 packages/playground/Samples/xamlPopupBug.tsx create mode 100644 packages/sample-custom-component/src/FabricXamlComboBoxNativeComponent.ts create mode 100644 packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp create mode 100644 packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.h 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; From 3b3aaaf84f8b283cd4e02d2c3de88eada1ebe1fc Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Wed, 21 Jan 2026 14:06:23 +0530 Subject: [PATCH 02/10] Change files --- ...ative-windows-83ceb93b-b350-41ed-a00b-0cd4927d42cb.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-83ceb93b-b350-41ed-a00b-0cd4927d42cb.json 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" +} From 1df8e5d8c5418330de0f6bdd211f303106ab7e41 Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Sun, 25 Jan 2026 11:30:49 +0530 Subject: [PATCH 03/10] Fix XAML popup positioning and light dismiss in ScrollView (#15557) - Set LocalToParentTransformMatrix synchronously before Connect() to fix initial position - Fire LayoutMetricsChanged on scroll to update position after scrolling - Add DismissPopupsRequest event for 3P components to implement light dismiss - Update ComboBox sample to use event-based approach (core remains XAML-agnostic) --- .../SampleCustomComponent/ComboBox.cpp | 36 +++++++++++------ .../CompositionComponentView.idl | 12 ++---- .../ContentIslandComponentView.cpp | 40 +++++++++---------- .../Composition/ContentIslandComponentView.h | 16 ++++---- .../Composition/ScrollViewComponentView.cpp | 16 ++++---- .../Composition/ScrollViewComponentView.h | 2 +- 6 files changed, 63 insertions(+), 59 deletions(-) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp index 34ec0adf53a..6b8f57b865c 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp +++ b/packages/sample-custom-component/windows/SampleCustomComponent/ComboBox.cpp @@ -11,13 +11,14 @@ #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 SetXamlRoot + VisualTreeHelper) +// Bug 2: When popup is open and user scrolls, popup should dismiss (FIXED via DismissPopupsRequest event) struct ComboBoxComponentView : public winrt::implements, Codegen::BaseComboBox { @@ -41,19 +42,15 @@ struct ComboBoxComponentView : public winrt::implements DismissPopupsRequest; }; [experimental][webhosthidden][default_interface] runtimeclass SwitchComponentView : ViewComponentView{}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp index d82d5a8b484..c2c9c4fed30 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -11,8 +11,6 @@ #include #include #include -#include -#include #include #include "CompositionContextHelper.h" #include "RootComponentView.h" @@ -51,6 +49,12 @@ 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. + auto clientRect = getClientRect(); + m_childSiteLink.LocalToParentTransformMatrix(winrt::Windows::Foundation::Numerics::make_float4x4_translation( + static_cast(clientRect.left), static_cast(clientRect.top), 0.0f)); + m_navigationHost = winrt::Microsoft::UI::Input::InputFocusNavigationHost::GetForSiteLink(m_childSiteLink); m_navigationHostDepartFocusRequestedToken = @@ -168,28 +172,20 @@ 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: 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: 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); - } - } - } +// 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 { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h index 0b47887f738..eed79b4d174 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h @@ -9,7 +9,6 @@ #include #include #include -#include #include #include "CompositionHelpers.h" #include "CompositionViewComponentView.h" @@ -48,11 +47,13 @@ struct ContentIslandComponentView : ContentIslandComponentViewT const &handler) noexcept; + void DismissPopupsRequest(winrt::event_token const &token) noexcept; ContentIslandComponentView( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, @@ -82,8 +83,9 @@ struct ContentIslandComponentView : ContentIslandComponentViewT> + 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 e37cc719bd2..af7248d38e2 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -1508,27 +1508,27 @@ void ScrollViewComponentView::FireLayoutMetricsChangedForScrollPositionChange() *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. +// Issue #15557: Fire DismissPopupsRequest event on child ContentIslandComponentView instances when scroll begins. +// This notifies 3P components to dismiss their own popups - implementing light dismiss behavior. 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) { + // Helper lambda to recursively find ContentIslandComponentView children and fire the event + std::function fireEventRecursively = + [&fireEventRecursively](const winrt::Microsoft::ReactNative::ComponentView &view) { // Check if this view is a ContentIslandComponentView if (auto contentIsland = view.try_as()) { - winrt::get_self(contentIsland)->DismissPopups(); + winrt::get_self(contentIsland)->FireDismissPopupsRequest(); } // Recursively check children for (auto child : view.Children()) { - dismissPopupsRecursively(child); + fireEventRecursively(child); } }; // Start recursive search from this ScrollView's children for (auto child : Children()) { - dismissPopupsRecursively(child); + fireEventRecursively(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 7f38e2bf1f9..d1770e3b523 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h @@ -131,7 +131,7 @@ struct ScrollInteractionTrackerOwner : public winrt::implements< 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 + // 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, From 7917f1a492911844c1a4eb6358a6d2ac9f55a151 Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Sun, 25 Jan 2026 19:08:55 +0530 Subject: [PATCH 04/10] Fix XAML popup positioning - convert to DIPs and update synchronously Issue #15557: Fix popup positioning for XAML controls in ScrollView Bug 1 (Position Fix): - Convert getClientRect() physical pixels to DIPs by dividing by pointScaleFactor - LocalToParentTransformMatrix expects logical pixels (DIPs), not physical pixels - Update transform synchronously instead of async to avoid race conditions - Set initial transform in OnMounted() before Connect() Bug 2 (Light Dismiss Fix): - Add DismissPopupsRequest event for 3P components to handle popup dismissal - ScrollView fires event when scroll begins via DismissChildContentIslandPopups() - Core remains XAML-agnostic - 3P components handle their own popups --- .../ContentIslandComponentView.cpp | 33 ++++++++-------- .../Composition/ScrollViewComponentView.cpp | 38 ++++++++++--------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp index c2c9c4fed30..a284ea60671 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -51,9 +51,12 @@ void ContentIslandComponentView::OnMounted() noexcept { // 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(); - m_childSiteLink.LocalToParentTransformMatrix(winrt::Windows::Foundation::Numerics::make_float4x4_translation( - static_cast(clientRect.left), static_cast(clientRect.top), 0.0f)); + 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); @@ -99,21 +102,21 @@ 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(); + // 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: 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; - strongThis->m_childSiteLink.LocalToParentTransformMatrix( - winrt::Windows::Foundation::Numerics::make_float4x4_translation( - static_cast(clientRect.left), static_cast(clientRect.top), 0.0f)); + float x = static_cast(clientRect.left) / scaleFactor; + float y = static_cast(clientRect.top) / scaleFactor; - strongThis->m_layoutChangePosted = false; - } - }); + m_childSiteLink.LocalToParentTransformMatrix( + winrt::Windows::Foundation::Numerics::make_float4x4_translation(x, y, 0.0f)); } winrt::Windows::Foundation::IInspectable ContentIslandComponentView::CreateAutomationProvider() noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index af7248d38e2..1169f60689d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -468,28 +468,30 @@ struct ScrollBarComponent { } winrt::com_ptr spTextFormat; - winrt::check_hresult(::Microsoft::ReactNative::DWriteFactory()->CreateTextFormat( - L"Segoe Fluent Icons", - nullptr, // Font collection (nullptr sets it to use the system font collection). - DWRITE_FONT_WEIGHT_REGULAR, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - 8, // Xaml resource: ScrollBarButtonArrowIconFontSize - L"", - spTextFormat.put())); + winrt::check_hresult( + ::Microsoft::ReactNative::DWriteFactory()->CreateTextFormat( + L"Segoe Fluent Icons", + nullptr, // Font collection (nullptr sets it to use the system font collection). + DWRITE_FONT_WEIGHT_REGULAR, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + 8, // Xaml resource: ScrollBarButtonArrowIconFontSize + L"", + spTextFormat.put())); winrt::check_hresult(spTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER)); winrt::com_ptr spTextLayout; - winrt::check_hresult(::Microsoft::ReactNative::DWriteFactory()->CreateTextLayout( - m_vertical ? ((region == ScrollbarHitRegion::ArrowFirst) ? L"\uEDDB" : L"\uEDDC") - : ((region == ScrollbarHitRegion::ArrowFirst) ? L"\uEDD9" : L"\uEDDA"), - 1, // The length of the string. - spTextFormat.get(), // The text format to apply to the string (contains font information, etc). - (m_arrowSize / m_scaleFactor), // The width of the layout box. - (m_arrowSize / m_scaleFactor), // The height of the layout box. - spTextLayout.put() // The IDWriteTextLayout interface pointer. - )); + winrt::check_hresult( + ::Microsoft::ReactNative::DWriteFactory()->CreateTextLayout( + m_vertical ? ((region == ScrollbarHitRegion::ArrowFirst) ? L"\uEDDB" : L"\uEDDC") + : ((region == ScrollbarHitRegion::ArrowFirst) ? L"\uEDD9" : L"\uEDDA"), + 1, // The length of the string. + spTextFormat.get(), // The text format to apply to the string (contains font information, etc). + (m_arrowSize / m_scaleFactor), // The width of the layout box. + (m_arrowSize / m_scaleFactor), // The height of the layout box. + spTextLayout.put() // The IDWriteTextLayout interface pointer. + )); POINT offset; { From 6d19597cfad76242fd90034430cebf1178428bb6 Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Mon, 26 Jan 2026 10:05:08 +0530 Subject: [PATCH 05/10] Apply clang-format --- .../ContentIslandComponentView.cpp | 5 +-- .../Composition/ScrollViewComponentView.cpp | 38 +++++++++---------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp index a284ea60671..dfd4d898fc0 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -54,9 +54,8 @@ void ContentIslandComponentView::OnMounted() noexcept { // 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_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); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index 1169f60689d..af7248d38e2 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -468,30 +468,28 @@ struct ScrollBarComponent { } winrt::com_ptr spTextFormat; - winrt::check_hresult( - ::Microsoft::ReactNative::DWriteFactory()->CreateTextFormat( - L"Segoe Fluent Icons", - nullptr, // Font collection (nullptr sets it to use the system font collection). - DWRITE_FONT_WEIGHT_REGULAR, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - 8, // Xaml resource: ScrollBarButtonArrowIconFontSize - L"", - spTextFormat.put())); + winrt::check_hresult(::Microsoft::ReactNative::DWriteFactory()->CreateTextFormat( + L"Segoe Fluent Icons", + nullptr, // Font collection (nullptr sets it to use the system font collection). + DWRITE_FONT_WEIGHT_REGULAR, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + 8, // Xaml resource: ScrollBarButtonArrowIconFontSize + L"", + spTextFormat.put())); winrt::check_hresult(spTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER)); winrt::com_ptr spTextLayout; - winrt::check_hresult( - ::Microsoft::ReactNative::DWriteFactory()->CreateTextLayout( - m_vertical ? ((region == ScrollbarHitRegion::ArrowFirst) ? L"\uEDDB" : L"\uEDDC") - : ((region == ScrollbarHitRegion::ArrowFirst) ? L"\uEDD9" : L"\uEDDA"), - 1, // The length of the string. - spTextFormat.get(), // The text format to apply to the string (contains font information, etc). - (m_arrowSize / m_scaleFactor), // The width of the layout box. - (m_arrowSize / m_scaleFactor), // The height of the layout box. - spTextLayout.put() // The IDWriteTextLayout interface pointer. - )); + winrt::check_hresult(::Microsoft::ReactNative::DWriteFactory()->CreateTextLayout( + m_vertical ? ((region == ScrollbarHitRegion::ArrowFirst) ? L"\uEDDB" : L"\uEDDC") + : ((region == ScrollbarHitRegion::ArrowFirst) ? L"\uEDD9" : L"\uEDDA"), + 1, // The length of the string. + spTextFormat.get(), // The text format to apply to the string (contains font information, etc). + (m_arrowSize / m_scaleFactor), // The width of the layout box. + (m_arrowSize / m_scaleFactor), // The height of the layout box. + spTextLayout.put() // The IDWriteTextLayout interface pointer. + )); POINT offset; { From e8cfe9946db5747af0f55265cdfae46430bfe930 Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Tue, 27 Jan 2026 19:53:01 +0530 Subject: [PATCH 06/10] Revert IDL formatting, keep only DismissPopupsRequest event --- .../CompositionComponentView.idl | 235 ++++++++++-------- 1 file changed, 138 insertions(+), 97 deletions(-) diff --git a/vnext/Microsoft.ReactNative/CompositionComponentView.idl b/vnext/Microsoft.ReactNative/CompositionComponentView.idl index 440562232d8..6a7c494fb72 100644 --- a/vnext/Microsoft.ReactNative/CompositionComponentView.idl +++ b/vnext/Microsoft.ReactNative/CompositionComponentView.idl @@ -11,124 +11,165 @@ 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 Experimental { -[webhosthidden][experimental] interface IInternalComponentView { - ICompositionContext CompositionContext { - get; +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 - -// [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 { + [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 { + namespace Experimental { -[webhosthidden][experimental] delegate Microsoft.ReactNative.Composition.Experimental.IVisual -CreateInternalVisualDelegate(Microsoft.ReactNative.ComponentView view); + [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" + [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; -} -} // 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; + " 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; }; }; -}; - -[experimental][webhosthidden] runtimeclass ContentIslandComponentView : ViewComponentView { - void Connect(Microsoft.UI.Content.ContentIsland contentIsland); - // Issue #15557: Event fired when a parent ScrollView starts scrolling. - // 3rd party XAML components should subscribe to this event to dismiss any open popups. - // Example: comboBox dropdown, DatePicker calendar, Flyouts, etc. - event Windows.Foundation.EventHandler DismissPopupsRequest; -}; + [experimental] + [webhosthidden] + runtimeclass ContentIslandComponentView : ViewComponentView { + void Connect(Microsoft.UI.Content.ContentIsland contentIsland); -[experimental][webhosthidden][default_interface] runtimeclass SwitchComponentView : ViewComponentView{}; - -[experimental][webhosthidden][default_interface] runtimeclass RootComponentView : ViewComponentView { - Microsoft.ReactNative.ComponentView GetFocusedComponent(); - Microsoft.ReactNative.ReactNativeIsland ReactNativeIsland { - get; + // 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; }; - 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; }; + DOC_STRING("Is non-null if this RootComponentView is the content of a portal") + PortalComponentView Portal { get; }; }; -}; -[experimental][webhosthidden][default_interface] runtimeclass DebuggingOverlayComponentView : 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 ActivityIndicatorComponentView : ViewComponentView{}; + [experimental] + [webhosthidden] + [default_interface] + runtimeclass DebuggingOverlayComponentView : ViewComponentView { + }; -[experimental][webhosthidden][default_interface] runtimeclass WindowsModalHostComponentView : ViewComponentView{}; + [experimental] + [webhosthidden] + [default_interface] + runtimeclass ActivityIndicatorComponentView : ViewComponentView { + }; -[experimental][webhosthidden][default_interface] runtimeclass ImageComponentView : ViewComponentView { - Microsoft.ReactNative.ImageProps ViewProps { - get; + [experimental] + [webhosthidden] + [default_interface] + runtimeclass WindowsModalHostComponentView : ViewComponentView { }; -}; -[experimental][webhosthidden][default_interface] runtimeclass ParagraphComponentView : ViewComponentView{}; + [experimental] + [webhosthidden] + [default_interface] + runtimeclass ImageComponentView : ViewComponentView { + Microsoft.ReactNative.ImageProps ViewProps { get; }; + }; -[experimental][webhosthidden][default_interface] runtimeclass ScrollViewComponentView : ViewComponentView{}; + [experimental] + [webhosthidden] + [default_interface] + runtimeclass ParagraphComponentView : ViewComponentView { + }; -[experimental][webhosthidden][default_interface] runtimeclass UnimplementedNativeViewComponentView - : ViewComponentView{}; + [experimental] + [webhosthidden] + [default_interface] + runtimeclass ScrollViewComponentView : ViewComponentView { + }; -[experimental][webhosthidden][default_interface] runtimeclass WindowsTextInputComponentView : ViewComponentView{}; + [experimental] + [webhosthidden] + [default_interface] + runtimeclass UnimplementedNativeViewComponentView : ViewComponentView { + }; -} // namespace Microsoft.ReactNative. Composition + [experimental] + [webhosthidden] + [default_interface] + runtimeclass WindowsTextInputComponentView : ViewComponentView { + }; + +} // namespace Microsoft.ReactNative From b9a0fed19cfb43009a0a7b940d488970c86e4923 Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Wed, 28 Jan 2026 22:55:55 +0530 Subject: [PATCH 07/10] Change files --- ...ws-automation-0a641d65-6bc9-4769-8666-f856ff15f040.json | 7 +++++++ ...ation-channel-e7ad73ac-7704-441b-81a3-2d8f97ece377.json | 7 +++++++ ...tion-commands-6b4b918c-021a-41a9-8eac-5ed58b0cec22.json | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 change/@react-native-windows-automation-0a641d65-6bc9-4769-8666-f856ff15f040.json create mode 100644 change/@react-native-windows-automation-channel-e7ad73ac-7704-441b-81a3-2d8f97ece377.json create mode 100644 change/@react-native-windows-automation-commands-6b4b918c-021a-41a9-8eac-5ed58b0cec22.json 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" +} From 9c258b5c045339ecb3d3d26991006ca35a689c75 Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Wed, 28 Jan 2026 22:56:23 +0530 Subject: [PATCH 08/10] Optimize light dismiss: register during mount instead of tree walk on scroll --- .../CompositionComponentView.idl | 238 ++++++++---------- .../ContentIslandComponentView.cpp | 29 ++- .../Composition/ContentIslandComponentView.h | 7 + .../Composition/ScrollViewComponentView.cpp | 35 ++- .../Composition/ScrollViewComponentView.h | 9 + 5 files changed, 159 insertions(+), 159 deletions(-) diff --git a/vnext/Microsoft.ReactNative/CompositionComponentView.idl b/vnext/Microsoft.ReactNative/CompositionComponentView.idl index 6a7c494fb72..2192d4aa72d 100644 --- a/vnext/Microsoft.ReactNative/CompositionComponentView.idl +++ b/vnext/Microsoft.ReactNative/CompositionComponentView.idl @@ -11,165 +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); - // 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; - }; + // 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 SwitchComponentView : ViewComponentView { - }; +[experimental][webhosthidden][default_interface] runtimeclass SwitchComponentView : ViewComponentView{}; - [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 RootComponentView : ViewComponentView { + Microsoft.ReactNative.ComponentView GetFocusedComponent(); + Microsoft.ReactNative.ReactNativeIsland ReactNativeIsland { + 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; }; + DOC_STRING("Is non-null if this RootComponentView is the content of a portal") + PortalComponentView Portal { + get; }; +}; - [experimental] - [webhosthidden] - [default_interface] - runtimeclass DebuggingOverlayComponentView : 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 ActivityIndicatorComponentView : ViewComponentView { - }; +[experimental][webhosthidden][default_interface] runtimeclass DebuggingOverlayComponentView : ViewComponentView{}; - [experimental] - [webhosthidden] - [default_interface] - runtimeclass WindowsModalHostComponentView : ViewComponentView { - }; +[experimental][webhosthidden][default_interface] runtimeclass ActivityIndicatorComponentView : ViewComponentView{}; - [experimental] - [webhosthidden] - [default_interface] - runtimeclass ImageComponentView : ViewComponentView { - Microsoft.ReactNative.ImageProps ViewProps { get; }; - }; +[experimental][webhosthidden][default_interface] runtimeclass WindowsModalHostComponentView : ViewComponentView{}; - [experimental] - [webhosthidden] - [default_interface] - runtimeclass ParagraphComponentView : ViewComponentView { +[experimental][webhosthidden][default_interface] runtimeclass ImageComponentView : ViewComponentView { + Microsoft.ReactNative.ImageProps ViewProps { + get; }; +}; - [experimental] - [webhosthidden] - [default_interface] - runtimeclass ScrollViewComponentView : ViewComponentView { - }; +[experimental][webhosthidden][default_interface] runtimeclass ParagraphComponentView : ViewComponentView{}; - [experimental] - [webhosthidden] - [default_interface] - runtimeclass UnimplementedNativeViewComponentView : ViewComponentView { - }; +[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 WindowsTextInputComponentView : ViewComponentView { - }; - -} // namespace Microsoft.ReactNative +[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 dfd4d898fc0..1ab8fa0931d 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" @@ -88,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 = {}; @@ -105,7 +128,11 @@ void ContentIslandComponentView::ParentLayoutChanged() noexcept { // when user clicks. Async updates via UIDispatcher().Post() were causing the // popup to open with stale transform values. // - // Note: getClientRect() returns values in physical pixels (scaled by pointScaleFactor), + // 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(); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h index eed79b4d174..b47388ae19b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h @@ -76,6 +76,13 @@ struct ContentIslandComponentView : ContentIslandComponentViewT scrollView; + winrt::event_token token; + }; + std::vector m_scrollBeginDragSubscriptions; + // Automation void ConfigureChildSiteLinkAutomation() noexcept; winrt::event_token m_fragmentRootAutomationProviderRequestedToken{}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index af7248d38e2..c60afe5b26f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -1508,27 +1508,22 @@ void ScrollViewComponentView::FireLayoutMetricsChangedForScrollPositionChange() *this, winrt::make(metrics, metrics)); } -// Issue #15557: Fire DismissPopupsRequest event on child ContentIslandComponentView instances when scroll begins. -// This notifies 3P components to dismiss their own popups - implementing light dismiss behavior. -void ScrollViewComponentView::DismissChildContentIslandPopups() noexcept { - // Helper lambda to recursively find ContentIslandComponentView children and fire the event - std::function fireEventRecursively = - [&fireEventRecursively](const winrt::Microsoft::ReactNative::ComponentView &view) { - // Check if this view is a ContentIslandComponentView - if (auto contentIsland = - view.try_as()) { - winrt::get_self(contentIsland)->FireDismissPopupsRequest(); - } +// Issue #15557: Event accessors for ScrollBeginDrag +winrt::event_token ScrollViewComponentView::ScrollBeginDrag( + winrt::Windows::Foundation::EventHandler const &handler) noexcept { + return m_scrollBeginDragEvent.add(handler); +} - // Recursively check children - for (auto child : view.Children()) { - fireEventRecursively(child); - } - }; +void ScrollViewComponentView::ScrollBeginDrag(winrt::event_token const &token) noexcept { + m_scrollBeginDragEvent.remove(token); +} - // Start recursive search from this ScrollView's children - for (auto child : Children()) { - fireEventRecursively(child); - } +// 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 d1770e3b523..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; @@ -164,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 From 703e4188471583b1695155b3891452636815fa65 Mon Sep 17 00:00:00 2001 From: Suraj Raykar <168889106+ssuraj2504@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:06:01 +0530 Subject: [PATCH 09/10] docs: fix broken markdown links in pipeline (#15594) * docs: fix broken markdown links * Address reviewer feedback * Remove obsolete docs and clean conflict markers * Delete managedCodeGen.md --------- Co-authored-by: Protik Biswas Co-authored-by: Vineeth <66076509+vineethkuttan@users.noreply.github.com> --- README.md | 2 +- docs/attack-surface-analyzer.md | 1 - docs/managedCodeGen.md | 38 --------------------------------- 3 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 docs/managedCodeGen.md diff --git a/README.md b/README.md index fca6bff31ad..ba73b3047b8 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ React Native has [great documentation](https://reactnative.dev/docs/getting-star - Check the [samples repo](https://github.com/microsoft/react-native-windows-samples) for more standalone samples. - The [React Native Gallery](https://github.com/microsoft/react-native-gallery) app demonstrates various components in an interactive way. - Check out the [React Native Developer Blog](https://devblogs.microsoft.com/react-native/) to see examples from past conference talks, blog posts, and more. -- For more sample code browse the [RNTester folder](https://github.com/microsoft/react-native-windows/tree/main/packages/e2e-test-app-fabric/windows/RNTesterApp) in the GitHub web UI. +- For more sample code browse the [RNTester folder](https://github.com/microsoft/react-native-windows/tree/main/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric) in the GitHub web UI. ## 📢 Contributing See [Contributing guidelines](https://github.com/microsoft/react-native-windows/blob/main/CONTRIBUTING.md) for how to setup your fork of the repo and start a PR to contribute to React Native for Windows. diff --git a/docs/attack-surface-analyzer.md b/docs/attack-surface-analyzer.md index 92bd7af1727..a677f243dcd 100644 --- a/docs/attack-surface-analyzer.md +++ b/docs/attack-surface-analyzer.md @@ -196,7 +196,6 @@ dotnet --version - [Microsoft Attack Surface Analyzer GitHub](https://github.com/microsoft/AttackSurfaceAnalyzer) - [ASA Documentation](https://github.com/microsoft/AttackSurfaceAnalyzer/wiki) -- [SDL Requirements](https://liquid.microsoft.com/Web/Object/Read/MS.Security/Requirements/Microsoft.Security.AccessControl.10011) - [ASA CLI Usage](https://github.com/microsoft/AttackSurfaceAnalyzer/wiki/Command-Line-Usage) ## Support diff --git a/docs/managedCodeGen.md b/docs/managedCodeGen.md deleted file mode 100644 index 2f33659fbc9..00000000000 --- a/docs/managedCodeGen.md +++ /dev/null @@ -1,38 +0,0 @@ -# Code generation for managed react-native modules - -This feature is applicable to C# implemented modules or `ViewManager`s. Its objective is to ensure the code needed to integrate with the `react-native` application is constructed at build time instead of at runtime. - -In earlier versions of react-native-windows managed modules registered themselves based on reflection at runtime. This method has a few disadvantages: -1) the error handling was limited and only available at runtime. -2) The cost for doing reflection was paid each and every time the app ran. -3) .NET Native - which is used by UWP apps - has somewhat restrictions on what types/APIs can be reflected upon and complex ways of working around it. -Doing it at build time should result in developers seeing all errors up front, rather than only seeing errors for the code being tested. And the runtime cost is not incurred every single time. - -## How it works -This change adds a new targets file that is imported from the [CSharpLib.targets](https://github.com/microsoft/react-native-windows/blob/main/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.targets) and [CSharpApp.targets](https://github.com/microsoft/react-native-windows/blob/main/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.targets). That will enable the codegen for managed projects. - -Those targets will collect all the C# source files, assembly references and defines from the app (or library) and invoke the CodeGen tool to generate a C# file. This is very similar to how the translation of Xaml to csharp code behind files works. -The generated C# file will contain a class called `ReactPackageProvider` in the default namespace of your project. The generate class implements `Microsoft.ReactNative.IReactPackageProvider`. The implementation of the generated class will: - * Register the detected ViewManagers (classes implementing `Microsoft.ReactNative.IViewManager`) - * Register the detected Modules (classes with attribute `[Microsoft.ReactNative.Managed.ReactModule]`) - * Generate and register serializers for types that need to be serialized for the registred modules. - * Generate and register custom serialization methods. - -### In source mode -The targets file hooks up an MSBuild target that will ensure the tool is compiled, deployed and runnable based on the tools sources. The codegen tool is defined in the Microsoft.ReactNative.Managed.CodeGen project. It will build and deploy the tool in the current configuraiton of your project (debug or release) but will always build and run it in platform x64. - -### In NuGet mode (Future) -The tool will be shipped as a binary and the steps won't have to build or deploy the tool. The shipped version in the NuGet package will only be release win-x64. -When building the apps that are checked in to our repo for testing, we have to remain running off the source version as MSBuild and NuGet are not flexible enough to build and consume a NuGet package in the same build. - -## Turning on/off -Currently the feature is behind an MSBuild property `$(ReactNativeCodeGenEnabled)`. -The default at the moment is false, it is only turned on for a single project for now which is the [SampleLibraryCS.csproj](https://github.com/microsoft/react-native-windows/blob/main/packages/sample-apps/windows/SampleLibraryCS/SampleLibraryCS.csproj) project, to prove it is stable. We will slowly enable it for all projects in the repo and then make it the default. - -## MSBuild/NuGet Complications -MSBuild and NuGet spent a long time fighting me in mixing a NetCoreApp3.1 executable and WinRT apps in the same solution and the same build. ProjectReferences cannot be used so I had to use the `` task directly in the targets and it was tricky making it build from both the customer apps as well as our main build solution and unittest (Microsoft.ReactNative.sln). In the end there are a few hacks in place to make this work. -The hookups in MSBuild do not use the latest BeforeTargets/AfterTargets feature to schedule the main work. I opted to follow the same pattern that the Xaml Codegen uses, so the React CodeGen runs after all the other codegen tools (like resgen, xamlcompile etc) to ensure people familiar with those extensions know how to work with the React Managed CodeGen. - -## Future improvements - * Add support for `Task` async methods. [#5143](https://github.com/microsoft/react-native-windows/issues/5143) - * Ship it as a NuGet package [#4546](https://github.com/microsoft/react-native-windows/issues/4546) From ab1521228baab262659500bc5a6f5dc3cc279526 Mon Sep 17 00:00:00 2001 From: Vineeth <66076509+vineethkuttan@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:20:57 +0530 Subject: [PATCH 10/10] Bring Narrator focus to XAML island (#15611) * Use automationoption as frameworkbased for childsite * Change files * remove tokens associated with fragment based --- ...-6aa7fa60-ce9f-4693-b4ec-7fe965283489.json | 7 ++ .../ContentIslandComponentView.cpp | 73 ++----------------- .../Composition/ContentIslandComponentView.h | 4 - 3 files changed, 13 insertions(+), 71 deletions(-) create mode 100644 change/react-native-windows-6aa7fa60-ce9f-4693-b4ec-7fe965283489.json diff --git a/change/react-native-windows-6aa7fa60-ce9f-4693-b4ec-7fe965283489.json b/change/react-native-windows-6aa7fa60-ce9f-4693-b4ec-7fe965283489.json new file mode 100644 index 00000000000..b6786f16eaa --- /dev/null +++ b/change/react-native-windows-6aa7fa60-ce9f-4693-b4ec-7fe965283489.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Use automationoption as frameworkbased for childsite", + "packageName": "react-native-windows", + "email": "66076509+vineethkuttan@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp index 1ab8fa0931d..420f3a4102f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -222,24 +222,6 @@ ContentIslandComponentView::~ContentIslandComponentView() noexcept { m_navigationHost.DepartFocusRequested(m_navigationHostDepartFocusRequestedToken); m_navigationHostDepartFocusRequestedToken = {}; } - if (m_childSiteLink) { - if (m_fragmentRootAutomationProviderRequestedToken) { - m_childSiteLink.FragmentRootAutomationProviderRequested(m_fragmentRootAutomationProviderRequestedToken); - m_fragmentRootAutomationProviderRequestedToken = {}; - } - if (m_parentAutomationProviderRequestedToken) { - m_childSiteLink.ParentAutomationProviderRequested(m_parentAutomationProviderRequestedToken); - m_parentAutomationProviderRequestedToken = {}; - } - if (m_nextSiblingAutomationProviderRequestedToken) { - m_childSiteLink.NextSiblingAutomationProviderRequested(m_nextSiblingAutomationProviderRequestedToken); - m_nextSiblingAutomationProviderRequestedToken = {}; - } - if (m_previousSiblingAutomationProviderRequestedToken) { - m_childSiteLink.PreviousSiblingAutomationProviderRequested(m_previousSiblingAutomationProviderRequestedToken); - m_previousSiblingAutomationProviderRequestedToken = {}; - } - } if (m_islandToConnect) { m_islandToConnect.Close(); } @@ -281,56 +263,13 @@ void ContentIslandComponentView::prepareForRecycle() noexcept { } void ContentIslandComponentView::ConfigureChildSiteLinkAutomation() noexcept { - // This automation mode must be set before connecting the child ContentIsland. - // It puts the child content into a mode where it won't own its own framework root. Instead, the child island's - // automation peers will use the same framework root as the automation peer of this ContentIslandComponentView. - m_childSiteLink.AutomationOption(winrt::Microsoft::UI::Content::ContentAutomationOptions::FragmentBased); - - // These events are raised in response to the child ContentIsland asking for providers. - // For example, the ContentIsland.FragmentRootAutomationProvider property will return - // the provider we provide here in FragmentRootAutomationProviderRequested. - - // We capture "this" as a raw pointer because ContentIslandComponentView doesn't currently support weak ptrs. - // It's safe because we disconnect these events in the destructor. - - m_fragmentRootAutomationProviderRequestedToken = m_childSiteLink.FragmentRootAutomationProviderRequested( - [this]( - const winrt::Microsoft::UI::Content::IContentSiteAutomation &, - const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) { - // The child island's fragment tree doesn't have its own fragment root. - // Here's how we can provide the correct fragment root to the child's UIA logic. - winrt::com_ptr fragmentRoot{nullptr}; - auto uiaProvider = this->EnsureUiaProvider(); - uiaProvider.as()->get_FragmentRoot(fragmentRoot.put()); - args.AutomationProvider(fragmentRoot.as()); - args.Handled(true); - }); - - m_parentAutomationProviderRequestedToken = m_childSiteLink.ParentAutomationProviderRequested( - [this]( - const winrt::Microsoft::UI::Content::IContentSiteAutomation &, - const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) { - auto uiaProvider = this->EnsureUiaProvider(); - args.AutomationProvider(uiaProvider); - args.Handled(true); - }); - - m_nextSiblingAutomationProviderRequestedToken = m_childSiteLink.NextSiblingAutomationProviderRequested( - [](const winrt::Microsoft::UI::Content::IContentSiteAutomation &, - const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) { - // The ContentIsland will always be the one and only child of this node, so it won't have siblings. - args.AutomationProvider(nullptr); - args.Handled(true); - }); - - m_previousSiblingAutomationProviderRequestedToken = m_childSiteLink.PreviousSiblingAutomationProviderRequested( - [](const winrt::Microsoft::UI::Content::IContentSiteAutomation &, - const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) { - // The ContentIsland will always be the one and only child of this node, so it won't have siblings. - args.AutomationProvider(nullptr); - args.Handled(true); - }); + // Use FrameworkBased to let the XamlIsland manage its own framework-level accessibility tree + // and raise focus events naturally. This tells the system that the child island has its own + // framework (WinUI/XAML) that manages automation. + m_childSiteLink.AutomationOption(winrt::Microsoft::UI::Content::ContentAutomationOptions::FrameworkBased); + // When using FrameworkBased mode, we don't register automation callbacks - let the XamlIsland handle its own UIA + // tree. if (m_innerAutomationProvider) { m_innerAutomationProvider->SetChildSiteLink(m_childSiteLink); } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h index b47388ae19b..fd96d641b46 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h @@ -85,10 +85,6 @@ struct ContentIslandComponentView : ContentIslandComponentViewT>