From c95825d8a7795bfa63041d1959ef8993907f9ab8 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:40:08 -0800 Subject: [PATCH 01/13] Focusable `Pressable`s should take focus when they are clicked on (#15327) * Pressables should take focus on press * Change files * fix --- ...-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json | 7 + .../PressableComponentTest.test.ts.snap | 23 +++ vnext/overrides.json | 6 + .../Components/Pressable/Pressable.d.ts | 175 ++++++++++++++++++ .../Components/Pressable/Pressable.windows.js | 23 ++- 5 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json create mode 100644 vnext/src-win/Libraries/Components/Pressable/Pressable.d.ts diff --git a/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json b/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json new file mode 100644 index 00000000000..b9a7adab163 --- /dev/null +++ b/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Pressables should take focus on press", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap index 233307c0d84..5124f964cad 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap @@ -846,6 +846,13 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] "Name": "pressOut", "TextRangePattern.GetText": "pressOut", }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "focus", + "TextRangePattern.GetText": "focus", + }, { "AutomationId": "", "ControlType": 50020, @@ -891,6 +898,10 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", "_Props": {}, }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, ], }, "Visual Tree": { @@ -991,6 +1002,18 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] }, ], }, + { + "Offset": "11, 85, 0", + "Size": "874, 20", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "874, 20", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, } diff --git a/vnext/overrides.json b/vnext/overrides.json index 54b13fbe234..0bd1c97711d 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -383,6 +383,12 @@ "type": "platform", "file": "src-win/Libraries/Components/Popup/PopupNativeComponent.js" }, + { + "type": "derived", + "file": "src-win/Libraries/Components/Pressable/Pressable.d.ts", + "baseFile": "packages/react-native/Libraries/Components/Pressable/Pressable.d.ts", + "baseHash": "d4dd8fc82436617bfeca9807be274aa5416d748c" + }, { "type": "patch", "file": "src-win/Libraries/Components/Pressable/Pressable.windows.js", diff --git a/vnext/src-win/Libraries/Components/Pressable/Pressable.d.ts b/vnext/src-win/Libraries/Components/Pressable/Pressable.d.ts new file mode 100644 index 00000000000..334ba2c9816 --- /dev/null +++ b/vnext/src-win/Libraries/Components/Pressable/Pressable.d.ts @@ -0,0 +1,175 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import type * as React from 'react'; +import {Insets} from '../../../types/public/Insets'; +import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet'; +import {ViewStyle} from '../../StyleSheet/StyleSheetTypes'; +import { + GestureResponderEvent, + MouseEvent, + NativeSyntheticEvent, + TargetedEvent, +} from '../../Types/CoreEventTypes'; +import {View} from '../View/View'; +import {AccessibilityProps} from '../View/ViewAccessibility'; +import {ViewProps} from '../View/ViewPropTypes'; + +export interface PressableStateCallbackType { + readonly pressed: boolean; +} + +export interface PressableAndroidRippleConfig { + color?: null | ColorValue | undefined; + borderless?: null | boolean | undefined; + radius?: null | number | undefined; + foreground?: null | boolean | undefined; +} + +export interface PressableProps + extends AccessibilityProps, + Omit { + /** + * Called when the hover is activated to provide visual feedback. + */ + onHoverIn?: null | ((event: MouseEvent) => void) | undefined; + + /** + * Called when the hover is deactivated to undo visual feedback. + */ + onHoverOut?: null | ((event: MouseEvent) => void) | undefined; + + /** + * Called when a single tap gesture is detected. + */ + onPress?: null | ((event: GestureResponderEvent) => void) | undefined; + + /** + * Called when a touch is engaged before `onPress`. + */ + onPressIn?: null | ((event: GestureResponderEvent) => void) | undefined; + + /** + * Called when a touch is released before `onPress`. + */ + onPressOut?: null | ((event: GestureResponderEvent) => void) | undefined; + + /** + * Called when a long-tap gesture is detected. + */ + onLongPress?: null | ((event: GestureResponderEvent) => void) | undefined; + + /** + * Called after the element loses focus. + * @platform macos windows + */ + onBlur?: + | null + | ((event: NativeSyntheticEvent) => void) + | undefined; + + /** + * Called after the element is focused. + * @platform macos windows + */ + onFocus?: + | null + | ((event: NativeSyntheticEvent) => void) + | undefined; + + /** + * Either children or a render prop that receives a boolean reflecting whether + * the component is currently pressed. + */ + children?: + | React.ReactNode + | ((state: PressableStateCallbackType) => React.ReactNode) + | undefined; + + /** + * Whether a press gesture can be interrupted by a parent gesture such as a + * scroll event. Defaults to true. + */ + cancelable?: null | boolean | undefined; + + /** + * Duration to wait after hover in before calling `onHoverIn`. + * @platform macos windows + */ + delayHoverIn?: number | null | undefined; + + /** + * Duration to wait after hover out before calling `onHoverOut`. + * @platform macos windows + */ + delayHoverOut?: number | null | undefined; + + /** + * Duration (in milliseconds) from `onPressIn` before `onLongPress` is called. + */ + delayLongPress?: null | number | undefined; + + /** + * Whether the press behavior is disabled. + */ + disabled?: null | boolean | undefined; + + //[ Windows + /** + * When the pressable is pressed it will take focus + * Default value: true + */ + focusOnPress?: null | boolean | undefined; + // Windows] + + /** + * Additional distance outside of this view in which a press is detected. + */ + hitSlop?: null | Insets | number | undefined; + + /** + * Additional distance outside of this view in which a touch is considered a + * press before `onPressOut` is triggered. + */ + pressRetentionOffset?: null | Insets | number | undefined; + + /** + * If true, doesn't play system sound on touch. + */ + android_disableSound?: null | boolean | undefined; + + /** + * Enables the Android ripple effect and configures its color. + */ + android_ripple?: null | PressableAndroidRippleConfig | undefined; + + /** + * Used only for documentation or testing (e.g. snapshot testing). + */ + testOnly_pressed?: null | boolean | undefined; + + /** + * Either view styles or a function that receives a boolean reflecting whether + * the component is currently pressed and returns view styles. + */ + style?: + | StyleProp + | ((state: PressableStateCallbackType) => StyleProp) + | undefined; + + /** + * Duration (in milliseconds) to wait after press down before calling onPressIn. + */ + unstable_pressDelay?: number | undefined; +} + +// TODO use React.AbstractComponent when available +export const Pressable: React.ForwardRefExoticComponent< + PressableProps & React.RefAttributes +>; diff --git a/vnext/src-win/Libraries/Components/Pressable/Pressable.windows.js b/vnext/src-win/Libraries/Components/Pressable/Pressable.windows.js index b5aa60f12af..487fa72a60f 100644 --- a/vnext/src-win/Libraries/Components/Pressable/Pressable.windows.js +++ b/vnext/src-win/Libraries/Components/Pressable/Pressable.windows.js @@ -71,6 +71,14 @@ type PressableBaseProps = $ReadOnly<{ */ disabled?: ?boolean, + // [Windows + /** + * When the pressable is pressed it will take focus + * Default value: true + */ + focusOnPress?: ?boolean, + // Windows] + /** * Additional distance outside of this view in which a press is detected. */ @@ -238,6 +246,7 @@ function Pressable({ delayLongPress, disabled, focusable, + focusOnPress, // Windows hitSlop, onBlur, onFocus, @@ -309,6 +318,16 @@ function Pressable({ hitSlop, }; + const onPressWithFocus = React.useCallback( + (args: GestureResponderEvent) => { + if (focusable !== false && focusOnPress !== false) { + viewRef?.current?.focus(); + } + onPress?.(args); + }, + [focusOnPress, onPress, focusable], + ); + const config = useMemo( () => ({ cancelable, @@ -325,7 +344,7 @@ function Pressable({ onHoverIn, onHoverOut, onLongPress, - onPress, + onPress: onPressWithFocus, onPressIn(event: GestureResponderEvent): void { if (android_rippleConfig != null) { android_rippleConfig.onPressIn(event); @@ -369,7 +388,7 @@ function Pressable({ onHoverIn, onHoverOut, onLongPress, - onPress, + onPressWithFocus, onPressIn, onPressMove, onPressOut, From 3db7c0dfba28fbbcecf1d9219d55cb78248153f2 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:46:10 -0800 Subject: [PATCH 02/13] Add TSF support to TextInput (#15363) * Add TSF support to TextInput * Change files --- ...ative-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json | 7 +++++++ .../TextInput/WindowsTextInputComponentView.cpp | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json diff --git a/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json b/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json new file mode 100644 index 00000000000..35ffe563a9d --- /dev/null +++ b/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Add TSF support to TextInput", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index 070a9169c39..fec096fd8c2 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -1782,6 +1782,9 @@ WindowsTextInputComponentView::createVisual() noexcept { LRESULT res; winrt::check_hresult(m_textServices->TxSendMessage(EM_SETTEXTMODE, TM_PLAINTEXT, 0, &res)); + // Enable TSF support + winrt::check_hresult(m_textServices->TxSendMessage(EM_SETEDITSTYLE, SES_USECTF, SES_USECTF, nullptr)); + m_caretVisual = m_compContext.CreateCaretVisual(); visual.InsertAt(m_caretVisual.InnerVisual(), 0); m_caretVisual.IsVisible(false); From fdc68628474a9d83e3515f2d1a07cdaa94c5c10e Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:01:51 -0800 Subject: [PATCH 03/13] Tooltip positioned incorrectly on non 100% scale factor (#15382) * Tooltip positioned incorrectly on non 100% scale factor * Change files --- ...ative-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json | 7 +++++++ .../Fabric/Composition/TooltipService.cpp | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json diff --git a/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json b/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json new file mode 100644 index 00000000000..0b29256a101 --- /dev/null +++ b/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Tooltip positioned incorrectly on non 100% scale factor", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp index 86285df5e5d..e65dac0061a 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp @@ -267,7 +267,7 @@ void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentV static_cast((tm.width + tooltipHorizontalPadding + tooltipHorizontalPadding) * scaleFactor); tooltipData->height = static_cast((tm.height + tooltipTopPadding + tooltipBottomPadding) * scaleFactor); - POINT pt = {static_cast(m_pos.X), static_cast(m_pos.Y)}; + POINT pt = {static_cast(m_pos.X * scaleFactor), static_cast(m_pos.Y * scaleFactor)}; ClientToScreen(parentHwnd, &pt); RegisterTooltipWndClass(); From 1b61c79d2dd36eec62ff26c4f44cc1aac210ba78 Mon Sep 17 00:00:00 2001 From: Ivan Golubev Date: Sat, 6 Dec 2025 15:11:43 +0000 Subject: [PATCH 04/13] Fix stackoverflow in StructInfo (#15454) * Fix stackoverflow in StructInfo Fix for the stack-buffer-overflow when using the FieldInfo like this: inline winrt::Microsoft::ReactNative::FieldMap GetStructInfo(AppStateSpec_AppState*) noexcept { winrt::Microsoft::ReactNative::FieldMap fieldMap { {L"app_state", &AppStateSpec_AppState::app_state}, // ^^^^^^^^^ when constructing the std::pair, temporary stack variables are created. }; return fieldMap; } but here in react-native-windows\vnext\Microsoft.ReactNative.Cxx\StructInfo.h the FieldInfo is trying to dereference and store data from a pointer (&fieldPtr) that points to a temporary object on the stack: m_fieldPtrStore{*reinterpret_cast(&fieldPtr)} AddressSanitizer detects the stack-buffer-overflow (accessing memory past the temporary's lifetime) and terminates the process. Instead, we avoid double indirection by using a memcopy. * Change files --------- Co-authored-by: Vladimir Morozov --- ...ive-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json | 7 +++++++ vnext/Microsoft.ReactNative.Cxx/StructInfo.h | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json diff --git a/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json b/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json new file mode 100644 index 00000000000..f93fe3ceb8d --- /dev/null +++ b/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fix stackoverflow in StructInfo", + "packageName": "react-native-windows", + "email": "vmorozov@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative.Cxx/StructInfo.h b/vnext/Microsoft.ReactNative.Cxx/StructInfo.h index c92d2d40cfe..9594f13c72c 100644 --- a/vnext/Microsoft.ReactNative.Cxx/StructInfo.h +++ b/vnext/Microsoft.ReactNative.Cxx/StructInfo.h @@ -81,7 +81,7 @@ struct FieldInfo { FieldInfo(TValue TClass::*fieldPtr) noexcept : m_fieldReader{FieldReader}, m_fieldWriter{FieldWriter}, - m_fieldPtrStore{*reinterpret_cast(&fieldPtr)} { + m_fieldPtrStore{StoreFieldPtr(fieldPtr)} { static_assert(sizeof(m_fieldPtrStore) >= sizeof(fieldPtr)); } @@ -94,6 +94,13 @@ struct FieldInfo { } private: + template + static uintptr_t StoreFieldPtr(TValue TClass::*fieldPtr) noexcept { + uintptr_t result{}; + std::memcpy(&result, &fieldPtr, sizeof(fieldPtr)); + return result; + } + FieldReaderType m_fieldReader; FieldWriterType m_fieldWriter; const uintptr_t m_fieldPtrStore; From 2a02670bbc43efe1a961f5610e4b92ac8c8c7a23 Mon Sep 17 00:00:00 2001 From: Ritoban Dutta <124308320+ritoban23@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:24:44 +0530 Subject: [PATCH 05/13] [Fabric] Fix UIA_LiveSettingPropertyId to use VT_I4 datatype instead of VT_BSTR (#15438) * [Fabric] Fix UIA_LiveSettingPropertyId to use VT_I4 datatype instead of VT_BSTR Fixes #15050 Use GetLiveSetting() conversion function to properly convert accessibilityLiveRegion string values to long (VT_I4) datatype as required by UIA_LiveSettingPropertyId. * call function with namespace --------- Co-authored-by: vineethkuttan <66076509+vineethkuttan@users.noreply.github.com> --- ...ative-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json | 7 +++++++ .../Fabric/Composition/CompositionViewComponentView.cpp | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json diff --git a/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json b/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json new file mode 100644 index 00000000000..c59a5341ba5 --- /dev/null +++ b/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "[Fabric] Fix UIA_LiveSettingPropertyId to use VT_I4 datatype instead of VT_BSTR", + "packageName": "react-native-windows", + "email": "ankudutt101@gmail.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index b53b7057344..b41120fe159 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -801,8 +801,8 @@ void ComponentView::updateAccessibilityProps( winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( EnsureUiaProvider(), UIA_LiveSettingPropertyId, - oldViewProps.accessibilityLiveRegion, - newViewProps.accessibilityLiveRegion); + winrt::Microsoft::ReactNative::implementation::GetLiveSetting(oldViewProps.accessibilityLiveRegion), + winrt::Microsoft::ReactNative::implementation::GetLiveSetting(newViewProps.accessibilityLiveRegion)); winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( EnsureUiaProvider(), UIA_LevelPropertyId, oldViewProps.accessibilityLevel, newViewProps.accessibilityLevel); From 4add534204033e8d09700b30c3b1543d5d7a3457 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:24:25 +0530 Subject: [PATCH 06/13] Defer UIA accessibility provider initialization until requested (#14756) * Initial plan for issue * Defer UIA provider initialization to GetPatternProvider Co-authored-by: chrisglein <26607885+chrisglein@users.noreply.github.com> * Add change file for UIA provider initialization optimization Co-authored-by: dannyvv <11037542+dannyvv@users.noreply.github.com> * Format code with clang-format to fix spacing Co-authored-by: acoates-ms <30809111+acoates-ms@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: chrisglein <26607885+chrisglein@users.noreply.github.com> Co-authored-by: dannyvv <11037542+dannyvv@users.noreply.github.com> Co-authored-by: acoates-ms <30809111+acoates-ms@users.noreply.github.com> Co-authored-by: Vineeth <66076509+vineethkuttan@users.noreply.github.com> --- ...-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json | 7 +++++ .../CompositionDynamicAutomationProvider.cpp | 29 ++++++++++--------- 2 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json diff --git a/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json b/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json new file mode 100644 index 00000000000..bfa96624efb --- /dev/null +++ b/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Defer UIA accessibility provider initialization until requested", + "packageName": "react-native-windows", + "email": "198982749+Copilot@users.noreply.github.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index 6ce1074a63c..8e1f2d5dbae 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -2,6 +2,7 @@ #include "CompositionDynamicAutomationProvider.h" #include #include +#include #include #include #include @@ -45,19 +46,6 @@ CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider( if (props->accessibilityState.has_value() && props->accessibilityState->selected.has_value()) { AddSelectionItemsToContainer(this); } - - if (strongView.try_as() || - strongView.try_as()) { - m_textProvider = winrt::make( - strongView.as(), this) - .try_as(); - } - - if (strongView.try_as()) { - m_annotationProvider = winrt::make( - strongView.as(), this) - .try_as(); - } } CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider( @@ -297,16 +285,31 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE if (patternId == UIA_TextPatternId && (strongView.try_as() || strongView.try_as())) { + if (!m_textProvider) { + m_textProvider = winrt::make( + strongView.as(), this) + .try_as(); + } m_textProvider.as().copy_to(pRetVal); } if (patternId == UIA_TextPattern2Id && strongView.try_as()) { + if (!m_textProvider) { + m_textProvider = winrt::make( + strongView.as(), this) + .try_as(); + } m_textProvider.as().copy_to(pRetVal); } if (patternId == UIA_AnnotationPatternId && strongView.try_as() && accessibilityAnnotationHasValue(props->accessibilityAnnotation)) { + if (!m_annotationProvider) { + m_annotationProvider = winrt::make( + strongView.as(), this) + .try_as(); + } m_annotationProvider.as().copy_to(pRetVal); } From be67c80b8900be8295210e708c00119cfdd5d77d Mon Sep 17 00:00:00 2001 From: Protik Biswas <219775028+protikbiswas100@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:25:04 +0530 Subject: [PATCH 07/13] Accessibility and UIA Support for XAML Fabric implementation (#15466) * accessibility and UIA support * adding lint and formatting fix * adding accessibility and UIA support for XAML fabric * Change files --------- Co-authored-by: Protik Biswas --- ...-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json | 7 +++++++ .../ContentIslandComponentView.cpp | 21 +++++++++++++++++++ .../Composition/ContentIslandComponentView.h | 3 +++ 3 files changed, 31 insertions(+) create mode 100644 change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json diff --git a/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json b/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json new file mode 100644 index 00000000000..ba2817c6e9c --- /dev/null +++ b/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "adding accessibility and UIA support for XAML fabric", + "packageName": "react-native-windows", + "email": "protikbiswas100@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp index 57aa7d14656..f2a84a4c646 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -126,6 +126,27 @@ bool ContentIslandComponentView::focusable() const noexcept { return true; } +facebook::react::Tag ContentIslandComponentView::hitTest( + facebook::react::Point pt, + facebook::react::Point &localPt, + bool ignorePointerEvents) const noexcept { + facebook::react::Point ptLocal{pt.x - m_layoutMetrics.frame.origin.x, pt.y - m_layoutMetrics.frame.origin.y}; + + // Check if the point is within the bounds of this ContentIslandComponentView. + // This ensures that hit tests correctly return this view's tag for UIA purposes, + // even when the actual content (XAML buttons, etc.) is hosted in the ContentIsland. + auto props = viewProps(); + if ((ignorePointerEvents || props->pointerEvents == facebook::react::PointerEventsMode::Auto || + props->pointerEvents == facebook::react::PointerEventsMode::BoxOnly) && + ptLocal.x >= 0 && ptLocal.x <= m_layoutMetrics.frame.size.width && ptLocal.y >= 0 && + ptLocal.y <= m_layoutMetrics.frame.size.height) { + localPt = ptLocal; + return Tag(); + } + + return -1; +} + // Helper to convert a FocusNavigationDirection to a FocusNavigationReason. winrt::Microsoft::UI::Input::FocusNavigationReason GetFocusNavigationReason( winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h index f530baa3400..013ae66f2f0 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h @@ -40,6 +40,9 @@ struct ContentIslandComponentView : ContentIslandComponentViewT Date: Mon, 5 Jan 2026 13:30:26 +0530 Subject: [PATCH 08/13] Fabric : Implements selectable prop for (#15473) * visual studio 2026 strict check fix * Implement text selection with drag highlight for selectable prop * implemented copy to clipboard * CTRL + A to select all text * Double-click on a word in selectable text selects the word * right click on selected text provides context menu * fixes unselect after CTRL + A selection * implements I-beam cursor for selectable text * default selection color cleanup * yarn lint:fix and format * Change files * nit * nit * review comments ( double click , theme ( use of system api) , Capture pointer ) * removed weak_ref of ComponentView rather take ReactTaggedView * yarn format * review comments * nit * review comments : nit * invalid/null tag returns -1 for ReactTaggedView * support CJK selcetion using icu.h * update Desktop.DLL with icu.lib * CJK word boundary using dictionary * review comments * yarn lint:fix and format * Add IcuUtils.cpp to project files for CJK support * remove ICUUtils from Microsoft.ReactNative.vcxproj already added to shared --- ...-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json | 7 + packages/playground/Samples/text.tsx | 49 ++ .../React.Windows.Desktop.DLL.vcxproj | 1 + .../Composition/CompositionEventHandler.cpp | 8 + .../Composition/ParagraphComponentView.cpp | 553 +++++++++++++++++- .../Composition/ParagraphComponentView.h | 52 ++ .../Fabric/Composition/RootComponentView.cpp | 15 + .../Fabric/Composition/RootComponentView.h | 7 + .../Fabric/Composition/Theme.cpp | 6 + .../Fabric/ReactTaggedView.h | 2 +- .../WindowsTextLayoutManager.h | 3 +- .../Microsoft.ReactNative.vcxproj | 1 + .../Microsoft.ReactNative/Utils/IcuUtils.cpp | 84 +++ vnext/Microsoft.ReactNative/Utils/IcuUtils.h | 42 ++ vnext/Shared/Shared.vcxitems | 1 + vnext/Shared/Shared.vcxitems.filters | 1 + 16 files changed, 827 insertions(+), 5 deletions(-) create mode 100644 change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json create mode 100644 vnext/Microsoft.ReactNative/Utils/IcuUtils.cpp create mode 100644 vnext/Microsoft.ReactNative/Utils/IcuUtils.h diff --git a/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json b/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json new file mode 100644 index 00000000000..60476319eb0 --- /dev/null +++ b/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implements selectable for ", + "packageName": "react-native-windows", + "email": "74712637+iamAbhi-916@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/playground/Samples/text.tsx b/packages/playground/Samples/text.tsx index a3a20d59fab..e5ce13a5090 100644 --- a/packages/playground/Samples/text.tsx +++ b/packages/playground/Samples/text.tsx @@ -20,6 +20,23 @@ export default class Bootstrap extends React.Component { selectable={true}> Click here : This is a text with a tooltip. + + + Text Selection Test + + This text is SELECTABLE. Try clicking and dragging to select it. + + + Hello 世界世界 World - Double-click to test CJK word selection! + + + This text is NOT selectable (selectable=false). + + + This text has no selectable prop (default behavior). + + + Bootstrap); diff --git a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj index b8ea2a82103..b3c58d57a35 100644 --- a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj +++ b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj @@ -107,6 +107,7 @@ Version.lib; Dwmapi.lib; WindowsApp_downlevel.lib; + icu.lib; %(AdditionalDependencies) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index 9bb904ec04a..d60b545b49e 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -15,6 +15,7 @@ #include #include "Composition.Input.h" #include "CompositionViewComponentView.h" +#include "ParagraphComponentView.h" #include "ReactNativeIsland.h" #include "RootComponentView.h" @@ -1101,6 +1102,13 @@ void CompositionEventHandler::onPointerExited( void CompositionEventHandler::onPointerPressed( const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint, winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept { + namespace Composition = winrt::Microsoft::ReactNative::Composition; + + // Clears any active text selection when left pointer is pressed + if (pointerPoint.Properties().PointerUpdateKind() != Composition::Input::PointerUpdateKind::RightButtonPressed) { + RootComponentView().ClearCurrentTextSelection(); + } + PointerId pointerId = pointerPoint.PointerId(); auto staleTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp index 407022de250..e5cbd80f851 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp @@ -8,17 +8,45 @@ #include #include +#include +#include #include #include #include #include #include +#include +#include #include "CompositionDynamicAutomationProvider.h" #include "CompositionHelpers.h" +#include "RootComponentView.h" #include "TextDrawing.h" namespace winrt::Microsoft::ReactNative::Composition::implementation { +// Automatically restores the original DPI of a render target +struct DpiRestorer { + ID2D1RenderTarget *renderTarget = nullptr; + float originalDpiX = 0.0f; + float originalDpiY = 0.0f; + + void operator()(ID2D1RenderTarget *) const noexcept { + if (renderTarget) { + renderTarget->SetDpi(originalDpiX, originalDpiY); + } + } +}; + +inline std::unique_ptr +MakeDpiGuard(ID2D1RenderTarget &renderTarget, float newDpiX, float newDpiY) noexcept { + float originalDpiX, originalDpiY; + renderTarget.GetDpi(&originalDpiX, &originalDpiY); + renderTarget.SetDpi(newDpiX, newDpiY); + + return std::unique_ptr( + &renderTarget, DpiRestorer{&renderTarget, originalDpiX, originalDpiY}); +} + ParagraphComponentView::ParagraphComponentView( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, @@ -28,7 +56,8 @@ ParagraphComponentView::ParagraphComponentView( compContext, tag, reactContext, - ComponentViewFeatures::Default & ~ComponentViewFeatures::Background) {} + // Disable Background (text draws its own) and FocusVisual (selection highlight is the focus indicator) + ComponentViewFeatures::Default & ~ComponentViewFeatures::Background & ~ComponentViewFeatures::FocusVisual) {} void ParagraphComponentView::MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, @@ -71,6 +100,14 @@ void ParagraphComponentView::updateProps( m_textLayout = nullptr; } + // Clear selection if text becomes non-selectable + if (oldViewProps.isSelectable != newViewProps.isSelectable) { + if (!newViewProps.isSelectable) { + ClearSelection(); + } + m_requireRedraw = true; + } + Super::updateProps(props, oldProps); } @@ -131,6 +168,108 @@ void ParagraphComponentView::updateTextAlignment( m_textLayout = nullptr; } +bool ParagraphComponentView::IsTextSelectableAtPoint(facebook::react::Point pt) noexcept { + // paragraph-level selectable prop is enabled + const auto &props = paragraphProps(); + if (!props.isSelectable) { + return false; + } + + // Finds which text fragment was hit + if (m_attributedStringBox.getValue().getFragments().size() && m_textLayout) { + BOOL isTrailingHit = false; + BOOL isInside = false; + DWRITE_HIT_TEST_METRICS metrics; + winrt::check_hresult(m_textLayout->HitTestPoint(pt.x, pt.y, &isTrailingHit, &isInside, &metrics)); + + if (isInside) { + uint32_t textPosition = metrics.textPosition; + + // Finds which fragment contains this text position + for (auto fragment : m_attributedStringBox.getValue().getFragments()) { + if (textPosition < fragment.string.length()) { + return true; + } + textPosition -= static_cast(fragment.string.length()); + } + } + } + + return false; +} + +std::optional ParagraphComponentView::GetTextPositionAtPoint(facebook::react::Point pt) noexcept { + if (!m_textLayout) { + return std::nullopt; + } + + BOOL isTrailingHit = FALSE; + BOOL isInside = FALSE; + DWRITE_HIT_TEST_METRICS metrics = {}; + + // Convert screen coordinates to character position + HRESULT hr = m_textLayout->HitTestPoint(pt.x, pt.y, &isTrailingHit, &isInside, &metrics); + if (FAILED(hr) || !isInside) { + return std::nullopt; + } + + // Calculates the actual character position + // If isTrailingHit is true, the point is closer to the trailing edge of the character, + // so we should return the next character position (for cursor positioning) + return static_cast(metrics.textPosition + isTrailingHit); +} + +std::optional ParagraphComponentView::GetClampedTextPosition(facebook::react::Point pt) noexcept { + if (!m_textLayout) { + return std::nullopt; + } + + const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)}; + if (utf16Text.empty()) { + return std::nullopt; + } + + DWRITE_TEXT_METRICS textMetrics; + if (FAILED(m_textLayout->GetMetrics(&textMetrics))) { + return std::nullopt; + } + + // Clamp the point to the text bounds for hit testing + const float clampedX = std::max(0.0f, std::min(pt.x, textMetrics.width)); + const float clampedY = std::max(0.0f, std::min(pt.y, textMetrics.height)); + + BOOL isTrailingHit = FALSE; + BOOL isInside = FALSE; + DWRITE_HIT_TEST_METRICS metrics = {}; + + HRESULT hr = m_textLayout->HitTestPoint(clampedX, clampedY, &isTrailingHit, &isInside, &metrics); + if (FAILED(hr)) { + return std::nullopt; + } + + int32_t result = static_cast(metrics.textPosition); + if (pt.x > textMetrics.width) { + // Dragging right - go to end of character + result = static_cast(metrics.textPosition + metrics.length); + } else if (pt.x < 0) { + // Dragging left - go to start of character + result = static_cast(metrics.textPosition); + } else if (isTrailingHit) { + // Inside bounds, trailing hit + result += 1; + } + + if (pt.y > textMetrics.height) { + // Dragging below - select to end of text + result = static_cast(utf16Text.length()); + } else if (pt.y < 0) { + // Dragging above - select to start of text + result = 0; + } + + return result; +} + void ParagraphComponentView::OnRenderingDeviceLost() noexcept { DrawText(); } @@ -263,6 +402,76 @@ void ParagraphComponentView::onThemeChanged() noexcept { } // Renders the text into our composition surface +void ParagraphComponentView::DrawSelectionHighlight( + ID2D1RenderTarget &renderTarget, + float offsetX, + float offsetY, + float pointScaleFactor) noexcept { + if (!m_selectionStart || !m_selectionEnd || !m_textLayout) { + return; + } + + // During drag, selection may not be normalized yet, using min/max for rendering + const int32_t selStart = std::min(*m_selectionStart, *m_selectionEnd); + const int32_t selEnd = std::max(*m_selectionStart, *m_selectionEnd); + if (selEnd <= selStart) { + return; + } + + // Scale offset to match text layout coordinates (same as RenderText) + const float scaledOffsetX = offsetX / pointScaleFactor; + const float scaledOffsetY = offsetY / pointScaleFactor; + + // Set DPI to match text rendering + const float dpi = pointScaleFactor * 96.0f; + std::unique_ptr dpiGuard = MakeDpiGuard(renderTarget, dpi, dpi); + + // Get the hit test metrics for the selected text range + UINT32 actualCount = 0; + HRESULT hr = m_textLayout->HitTestTextRange( + static_cast(selStart), + static_cast(selEnd - selStart), + scaledOffsetX, + scaledOffsetY, + nullptr, + 0, + &actualCount); + + if (actualCount == 0) { + return; + } + + std::vector hitTestMetrics(actualCount); + hr = m_textLayout->HitTestTextRange( + static_cast(selStart), + static_cast(selEnd - selStart), + scaledOffsetX, + scaledOffsetY, + hitTestMetrics.data(), + actualCount, + &actualCount); + + if (FAILED(hr)) { + return; + } + + // TODO: use prop selectionColor if provided + winrt::com_ptr selectionBrush; + const D2D1_COLOR_F selectionColor = theme()->D2DPlatformColor("Highlight@40"); + hr = renderTarget.CreateSolidColorBrush(selectionColor, selectionBrush.put()); + + if (FAILED(hr)) { + return; + } + + // Draw rectangles for each hit test metric + for (UINT32 i = 0; i < actualCount; i++) { + const auto &metric = hitTestMetrics[i]; + const D2D1_RECT_F rect = {metric.left, metric.top, metric.left + metric.width, metric.top + metric.height}; + renderTarget.FillRectangle(&rect, selectionBrush.get()); + } +} + void ParagraphComponentView::DrawText() noexcept { if (!m_drawingSurface || theme()->IsEmpty()) return; @@ -281,13 +490,20 @@ void ParagraphComponentView::DrawText() noexcept { viewProps()->backgroundColor ? theme()->D2DColor(*viewProps()->backgroundColor) : D2D1::ColorF(D2D1::ColorF::Black, 0.0f)); const auto &props = paragraphProps(); + + // Calculate text offset + const float textOffsetX = static_cast(offset.x) + m_layoutMetrics.contentInsets.left; + const float textOffsetY = static_cast(offset.y) + m_layoutMetrics.contentInsets.top; + + // Draw selection highlight behind text + DrawSelectionHighlight(*d2dDeviceContext, textOffsetX, textOffsetY, m_layoutMetrics.pointScaleFactor); + RenderText( *d2dDeviceContext, *m_textLayout, m_attributedStringBox.getValue(), props.textAttributes, - {static_cast(offset.x) + m_layoutMetrics.contentInsets.left, - static_cast(offset.y) + m_layoutMetrics.contentInsets.top}, + {textOffsetX, textOffsetY}, m_layoutMetrics.pointScaleFactor, *theme()); @@ -299,6 +515,324 @@ void ParagraphComponentView::DrawText() noexcept { } } +void ParagraphComponentView::ClearSelection() noexcept { + const bool hadSelection = (m_selectionStart || m_selectionEnd || m_isSelecting); + m_selectionStart = std::nullopt; + m_selectionEnd = std::nullopt; + m_isSelecting = false; + if (hadSelection) { + // Clears selection highlight + DrawText(); + } +} + +void ParagraphComponentView::OnPointerPressed( + const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept { + // Only handle selection if text is selectable + const auto &props = paragraphProps(); + if (!props.isSelectable) { + Super::OnPointerPressed(args); + return; + } + + auto pp = args.GetCurrentPoint(-1); + + // Ignores right-click + if (pp.Properties().PointerUpdateKind() == + winrt::Microsoft::ReactNative::Composition::Input::PointerUpdateKind::RightButtonPressed) { + args.Handled(true); + return; + } + + auto position = pp.Position(); + + facebook::react::Point localPt{ + position.X - m_layoutMetrics.frame.origin.x, position.Y - m_layoutMetrics.frame.origin.y}; + + std::optional charPosition = GetTextPositionAtPoint(localPt); + + if (charPosition) { + if (auto root = rootComponentView()) { + root->ClearCurrentTextSelection(); + } + + // Check for double-click + auto now = std::chrono::steady_clock::now(); + auto timeSinceLastClick = std::chrono::duration_cast(now - m_lastClickTime); + const UINT doubleClickTime = GetDoubleClickTime(); + const bool isDoubleClick = (timeSinceLastClick.count() < static_cast(doubleClickTime)) && + m_lastClickPosition && (std::abs(*charPosition - *m_lastClickPosition) <= 1); + + // Update last click tracking + m_lastClickTime = now; + m_lastClickPosition = charPosition; + + if (isDoubleClick) { + SelectWordAtPosition(*charPosition); + m_isSelecting = false; + } else { + // Single-click: start drag selection + m_selectionStart = charPosition; + m_selectionEnd = charPosition; + m_isSelecting = true; + + // Tracks selection even when the mouse moves outside the component bounds + CapturePointer(args.Pointer()); + } + + if (auto root = rootComponentView()) { + root->SetViewWithTextSelection(*get_strong()); + } + + // Focuses so we receive onLostFocus when clicking elsewhere + if (auto root = rootComponentView()) { + root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None); + } + + args.Handled(true); + } else { + ClearSelection(); + m_lastClickPosition = std::nullopt; + Super::OnPointerPressed(args); + } +} + +void ParagraphComponentView::OnPointerMoved( + const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept { + // Only track movement if we're actively selecting + if (!m_isSelecting) { + Super::OnPointerMoved(args); + return; + } + + auto pp = args.GetCurrentPoint(static_cast(Tag())); + auto position = pp.Position(); + + facebook::react::Point localPt{position.X, position.Y}; + std::optional charPosition = GetClampedTextPosition(localPt); + + if (charPosition && charPosition != m_selectionEnd) { + m_selectionEnd = charPosition; + DrawText(); + args.Handled(true); + } +} + +void ParagraphComponentView::OnPointerReleased( + const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept { + // Check for right-click to show context menu + auto pp = args.GetCurrentPoint(-1); + if (pp.Properties().PointerUpdateKind() == + winrt::Microsoft::ReactNative::Composition::Input::PointerUpdateKind::RightButtonReleased) { + const auto &props = paragraphProps(); + if (props.isSelectable) { + ShowContextMenu(); + args.Handled(true); + return; + } + } + + if (!m_isSelecting) { + Super::OnPointerReleased(args); + return; + } + + m_isSelecting = false; + + ReleasePointerCapture(args.Pointer()); + + if (!m_selectionStart || !m_selectionEnd || *m_selectionStart == *m_selectionEnd) { + m_selectionStart = std::nullopt; + m_selectionEnd = std::nullopt; + } else { + SetSelection(*m_selectionStart, *m_selectionEnd); + } + + args.Handled(true); +} + +void ParagraphComponentView::onLostFocus( + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { + ClearSelection(); + + Super::onLostFocus(args); +} + +void ParagraphComponentView::OnPointerCaptureLost() noexcept { + // Pointer capture was lost stop any active selection drag + if (m_isSelecting) { + m_isSelecting = false; + + if (!m_selectionStart || !m_selectionEnd || *m_selectionStart == *m_selectionEnd) { + m_selectionStart = std::nullopt; + m_selectionEnd = std::nullopt; + } else { + SetSelection(*m_selectionStart, *m_selectionEnd); + } + } + + Super::OnPointerCaptureLost(); +} + +std::string ParagraphComponentView::GetSelectedText() const noexcept { + if (!m_selectionStart || !m_selectionEnd) { + return ""; + } + + const int32_t selStart = std::min(*m_selectionStart, *m_selectionEnd); + const int32_t selEnd = std::max(*m_selectionStart, *m_selectionEnd); + + if (selEnd <= selStart) { + return ""; + } + + const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)}; + + if (selStart >= static_cast(utf16Text.length())) { + return ""; + } + + const int32_t clampedEnd = std::min(selEnd, static_cast(utf16Text.length())); + const std::wstring selectedUtf16 = + utf16Text.substr(static_cast(selStart), static_cast(clampedEnd - selStart)); + return ::Microsoft::Common::Unicode::Utf16ToUtf8(selectedUtf16); +} + +void ParagraphComponentView::CopySelectionToClipboard() noexcept { + const std::string selectedText = GetSelectedText(); + if (selectedText.empty()) { + return; + } + + // Convert UTF-8 to wide string for Windows clipboard + const std::wstring wideText = ::Microsoft::Common::Unicode::Utf8ToUtf16(selectedText); + + winrt::Windows::ApplicationModel::DataTransfer::DataPackage dataPackage; + dataPackage.SetText(wideText); + winrt::Windows::ApplicationModel::DataTransfer::Clipboard::SetContent(dataPackage); +} + +void ParagraphComponentView::SelectWordAtPosition(int32_t charPosition) noexcept { + const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)}; + const int32_t textLength = static_cast(utf16Text.length()); + + if (utf16Text.empty() || charPosition < 0 || charPosition >= textLength) { + return; + } + + int32_t wordStart = charPosition; + int32_t wordEnd = charPosition; + + ::Microsoft::ReactNative::IcuUtils::WordBreakIterator wordBreaker(utf16Text.c_str(), textLength); + const bool icuSuccess = wordBreaker.IsValid() && wordBreaker.GetWordBoundaries(charPosition, wordStart, wordEnd); + + if (!icuSuccess) { + wordStart = charPosition; + wordEnd = charPosition; + + while (wordStart > 0) { + int32_t prevPos = ::Microsoft::ReactNative::IcuUtils::MoveToPreviousCodePoint(utf16Text.c_str(), wordStart); + ::Microsoft::ReactNative::IcuUtils::UChar32 prevCp = + ::Microsoft::ReactNative::IcuUtils::GetCodePointAt(utf16Text.c_str(), textLength, prevPos); + if (!::Microsoft::ReactNative::IcuUtils::IsAlphanumeric(prevCp)) { + break; + } + wordStart = prevPos; + } + + while (wordEnd < textLength) { + ::Microsoft::ReactNative::IcuUtils::UChar32 cp = + ::Microsoft::ReactNative::IcuUtils::GetCodePointAt(utf16Text.c_str(), textLength, wordEnd); + if (!::Microsoft::ReactNative::IcuUtils::IsAlphanumeric(cp)) { + break; + } + wordEnd = ::Microsoft::ReactNative::IcuUtils::MoveToNextCodePoint(utf16Text.c_str(), textLength, wordEnd); + } + } + + if (wordEnd > wordStart) { + SetSelection(wordStart, wordEnd); + DrawText(); + } +} + +void ParagraphComponentView::SetSelection(int32_t start, int32_t end) noexcept { + m_selectionStart = std::min(start, end); + m_selectionEnd = std::max(start, end); +} + +void ParagraphComponentView::ShowContextMenu() noexcept { + HMENU menu = CreatePopupMenu(); + if (!menu) { + return; + } + + const bool hasSelection = (m_selectionStart && m_selectionEnd && *m_selectionStart != *m_selectionEnd); + const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)}; + const bool hasText = !utf16Text.empty(); + + // Add menu items (1 = Copy, 2 = Select All) + AppendMenuW(menu, MF_STRING | (hasSelection ? 0 : MF_GRAYED), 1, L"Copy"); + AppendMenuW(menu, MF_STRING | (hasText ? 0 : MF_GRAYED), 2, L"Select All"); + + // Get cursor position for menu placement + POINT cursorPos; + GetCursorPos(&cursorPos); + + const HWND hwnd = GetActiveWindow(); + + const int cmd = TrackPopupMenu( + menu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY, cursorPos.x, cursorPos.y, 0, hwnd, NULL); + + if (cmd == 1) { + // Copy + CopySelectionToClipboard(); + } else if (cmd == 2) { + SetSelection(0, static_cast(utf16Text.length())); + DrawText(); + } + + DestroyMenu(menu); +} + +void ParagraphComponentView::OnKeyDown( + const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept { + const bool isCtrlDown = + (args.KeyboardSource().GetKeyState(winrt::Windows::System::VirtualKey::Control) & + winrt::Microsoft::UI::Input::VirtualKeyStates::Down) == winrt::Microsoft::UI::Input::VirtualKeyStates::Down; + + // Handle Ctrl+C for copy + if (isCtrlDown && args.Key() == winrt::Windows::System::VirtualKey::C) { + if (m_selectionStart && m_selectionEnd && *m_selectionStart != *m_selectionEnd) { + CopySelectionToClipboard(); + args.Handled(true); + return; + } + } + + // Handle Ctrl+A for select all + if (isCtrlDown && args.Key() == winrt::Windows::System::VirtualKey::A) { + const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)}; + if (!utf16Text.empty()) { + if (auto root = rootComponentView()) { + root->ClearCurrentTextSelection(); + } + + SetSelection(0, static_cast(utf16Text.length())); + + if (auto root = rootComponentView()) { + root->SetViewWithTextSelection(*get_strong()); + } + + DrawText(); + args.Handled(true); + return; + } + } + + Super::OnKeyDown(args); +} + std::string ParagraphComponentView::DefaultControlType() const noexcept { return "text"; } @@ -307,6 +841,19 @@ std::string ParagraphComponentView::DefaultAccessibleName() const noexcept { return m_attributedStringBox.getValue().getString(); } +bool ParagraphComponentView::focusable() const noexcept { + // Text is focusable when it's selectable or when explicitly marked as focusable via props + return paragraphProps().isSelectable || viewProps()->focusable; +} + +std::pair ParagraphComponentView::cursor() const noexcept { + // Returns I-beam cursor for selectable text + if (paragraphProps().isSelectable) { + return {facebook::react::Cursor::Text, nullptr}; + } + return Super::cursor(); +} + winrt::Microsoft::ReactNative::ComponentView ParagraphComponentView::Create( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h index 648b6e41a04..91290a80c12 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "CompositionHelpers.h" #include "CompositionViewComponentView.h" @@ -48,6 +49,28 @@ struct ParagraphComponentView : ParagraphComponentViewT cursor() const noexcept override; + + // Called when losing focus, when another text starts selection, or when clicking outside text bounds. + void ClearSelection() noexcept; + + // Text selection pointer event handlers + void OnPointerPressed( + const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept override; + void OnPointerMoved( + const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept override; + void OnPointerReleased( + const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept override; + void OnPointerCaptureLost() noexcept override; + void onLostFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept override; + + // Keyboard event handler for copy + void OnKeyDown(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept override; + ParagraphComponentView( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, @@ -56,7 +79,28 @@ struct ParagraphComponentView : ParagraphComponentViewT &fbAlignment) noexcept; + bool IsTextSelectableAtPoint(facebook::react::Point pt) noexcept; + std::optional GetTextPositionAtPoint(facebook::react::Point pt) noexcept; + std::optional GetClampedTextPosition(facebook::react::Point pt) noexcept; + std::string GetSelectedText() const noexcept; + + // Copies currently selected text to the system clipboard + void CopySelectionToClipboard() noexcept; + + // Selects the word at the given character position + void SelectWordAtPosition(int32_t charPosition) noexcept; + + // Shows a context menu with Copy/Select All options on right-click + void ShowContextMenu() noexcept; + + // m_selectionStart <= m_selectionEnd + void SetSelection(int32_t start, int32_t end) noexcept; winrt::com_ptr<::IDWriteTextLayout> m_textLayout; facebook::react::AttributedStringBox m_attributedStringBox; @@ -64,6 +108,14 @@ struct ParagraphComponentView : ParagraphComponentViewT m_selectionStart; + std::optional m_selectionEnd; + bool m_isSelecting{false}; + + // Double-click detection + std::chrono::steady_clock::time_point m_lastClickTime{}; + std::optional m_lastClickPosition; }; } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp index 2777b76799b..b5ca1da68c1 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -9,6 +9,7 @@ #include #include #include "CompositionRootAutomationProvider.h" +#include "ParagraphComponentView.h" #include "ReactNativeIsland.h" #include "Theme.h" @@ -349,4 +350,18 @@ HWND RootComponentView::GetHwndForParenting() noexcept { return base_type::GetHwndForParenting(); } +void RootComponentView::ClearCurrentTextSelection() noexcept { + if (auto view = m_viewWithTextSelection.view()) { + if (auto paragraphView = view.try_as()) { + paragraphView->ClearSelection(); + } + } + m_viewWithTextSelection = + ::Microsoft::ReactNative::ReactTaggedView{winrt::Microsoft::ReactNative::ComponentView{nullptr}}; +} + +void RootComponentView::SetViewWithTextSelection(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + m_viewWithTextSelection = ::Microsoft::ReactNative::ReactTaggedView{view}; +} + } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h index 3a037fc6d61..dc8fe578692 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include "CompositionViewComponentView.h" @@ -86,6 +87,9 @@ struct RootComponentView : RootComponentViewT m_wkRootView{nullptr}; winrt::weak_ref m_wkPortal{nullptr}; bool m_visualAddedToIsland{false}; + + ::Microsoft::ReactNative::ReactTaggedView m_viewWithTextSelection{ + winrt::Microsoft::ReactNative::ComponentView{nullptr}}; }; } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Theme.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Theme.cpp index f194e8bca69..7a7e59b502a 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Theme.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Theme.cpp @@ -174,6 +174,12 @@ bool Theme::TryGetPlatformColor(const std::string &platformColor, winrt::Windows return true; } + if (platformColor == "Highlight@40" && TryGetPlatformColor("Highlight", color)) { + color.A = static_cast(static_cast(color.A) * 0.4f); + m_colorCache[platformColor] = std::make_pair(true, color); + return true; + } + auto uiColor = s_uiColorTypes.find(platformColor); if (uiColor != s_uiColorTypes.end()) { auto uiSettings{winrt::Windows::UI::ViewManagement::UISettings()}; diff --git a/vnext/Microsoft.ReactNative/Fabric/ReactTaggedView.h b/vnext/Microsoft.ReactNative/Fabric/ReactTaggedView.h index 37b9059a066..612c13562d6 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ReactTaggedView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ReactTaggedView.h @@ -16,7 +16,7 @@ namespace Microsoft::ReactNative { */ struct ReactTaggedView { ReactTaggedView(const winrt::Microsoft::ReactNative::ComponentView &componentView) - : m_view(componentView), m_tag(componentView.Tag()) {} + : m_view(componentView), m_tag(componentView ? componentView.Tag() : -1) {} winrt::Microsoft::ReactNative::ComponentView view() noexcept { if (!m_view) { diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.h index 898545c3610..c29e2801b15 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.h +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.h @@ -56,8 +56,9 @@ class WindowsTextLayoutManager : public TextLayoutManager { TextMeasurement::Attachments &attachments, float minimumFontScale) noexcept; - private: static winrt::hstring GetTransformedText(const AttributedStringBox &attributedStringBox); + + private: static void GetTextLayout( const AttributedStringBox &attributedStringBox, const ParagraphAttributes ¶graphAttributes, diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index f8097e89ed4..57553f1cd9e 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -313,6 +313,7 @@ + diff --git a/vnext/Microsoft.ReactNative/Utils/IcuUtils.cpp b/vnext/Microsoft.ReactNative/Utils/IcuUtils.cpp new file mode 100644 index 00000000000..696c4cc007f --- /dev/null +++ b/vnext/Microsoft.ReactNative/Utils/IcuUtils.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "IcuUtils.h" +#include + +namespace Microsoft::ReactNative::IcuUtils { + +void UBreakIteratorDeleter::operator()(void *ptr) const noexcept { + if (ptr) { + ubrk_close(static_cast(ptr)); + } +} + +WordBreakIterator::WordBreakIterator(const wchar_t *text, int32_t length) noexcept : m_length(length) { + UErrorCode status = U_ZERO_ERROR; + auto *iter = ubrk_open(UBRK_WORD, nullptr, reinterpret_cast(text), length, &status); + if (U_SUCCESS(status)) { + m_breakIterator.reset(static_cast(iter)); + } +} + +bool WordBreakIterator::IsValid() const noexcept { + return m_breakIterator != nullptr; +} + +bool WordBreakIterator::GetWordBoundaries(int32_t position, int32_t &outStart, int32_t &outEnd) const noexcept { + if (!m_breakIterator || position < 0 || position >= m_length) { + return false; + } + + auto *iter = static_cast(m_breakIterator.get()); + + int32_t start = ubrk_preceding(iter, position + 1); + if (start == UBRK_DONE) { + start = 0; + } + + int32_t end = ubrk_following(iter, position); + if (end == UBRK_DONE) { + end = m_length; + } + + int32_t ruleStatus = ubrk_getRuleStatus(iter); + if (ruleStatus == UBRK_WORD_NONE) { + return false; + } + + outStart = start; + outEnd = end; + return true; +} + +bool IsAlphanumeric(UChar32 codePoint) noexcept { + return u_isalnum(codePoint) != 0; +} + +UChar32 GetCodePointAt(const wchar_t *str, int32_t length, int32_t pos) noexcept { + if (!str || length <= 0 || pos < 0 || pos >= length) { + return 0; + } + UChar32 cp; + U16_GET(str, 0, pos, length, cp); + return cp; +} + +int32_t MoveToPreviousCodePoint(const wchar_t *str, int32_t pos) noexcept { + if (!str || pos <= 0) { + return 0; + } + U16_BACK_1(str, 0, pos); + return pos; +} + +int32_t MoveToNextCodePoint(const wchar_t *str, int32_t length, int32_t pos) noexcept { + if (!str || length <= 0 || pos >= length) { + return length; + } + U16_FWD_1(str, pos, length); + return pos; +} + +} // namespace Microsoft::ReactNative::IcuUtils diff --git a/vnext/Microsoft.ReactNative/Utils/IcuUtils.h b/vnext/Microsoft.ReactNative/Utils/IcuUtils.h new file mode 100644 index 00000000000..e8603272917 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Utils/IcuUtils.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +// ICU utilities wrapped in a namespace to avoid UChar naming conflicts with Folly's FBString. +// Folly has a template parameter named 'UChar' which conflicts with ICU's global UChar typedef. +namespace Microsoft::ReactNative::IcuUtils { + +using UChar32 = int32_t; + +struct UBreakIteratorDeleter { + void operator()(void *ptr) const noexcept; +}; + +class WordBreakIterator { + public: + WordBreakIterator(const wchar_t *text, int32_t length) noexcept; + + WordBreakIterator(const WordBreakIterator &) = delete; + WordBreakIterator &operator=(const WordBreakIterator &) = delete; + WordBreakIterator(WordBreakIterator &&) = default; + WordBreakIterator &operator=(WordBreakIterator &&) = default; + + bool IsValid() const noexcept; + + bool GetWordBoundaries(int32_t position, int32_t &outStart, int32_t &outEnd) const noexcept; + + private: + std::unique_ptr m_breakIterator{nullptr}; + int32_t m_length = 0; +}; + +bool IsAlphanumeric(UChar32 codePoint) noexcept; +UChar32 GetCodePointAt(const wchar_t *str, int32_t length, int32_t pos) noexcept; +int32_t MoveToPreviousCodePoint(const wchar_t *str, int32_t pos) noexcept; +int32_t MoveToNextCodePoint(const wchar_t *str, int32_t length, int32_t pos) noexcept; + +} // namespace Microsoft::ReactNative::IcuUtils diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index a96f1ffc601..7391a74eca3 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -519,6 +519,7 @@ + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index 518f4ce1800..0c2512eaf47 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -105,6 +105,7 @@ Source Files\Modules + From 01cfa0dbdcdd0b80160c0445a504ae56fe8eaaf8 Mon Sep 17 00:00:00 2001 From: Abhijeet Jha <74712637+iamAbhi-916@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:06:04 +0530 Subject: [PATCH 09/13] Fix DPI scaling for debugging overlay highlights (#15479) * Fix DPI scaling for debugging overlay highlights * Change files * pointScaleFactor from m_layoutMetrics as its member of base class ComponentView --- ...ative-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json | 7 +++++++ .../Fabric/Composition/DebuggingOverlayComponentView.cpp | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json diff --git a/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json b/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json new file mode 100644 index 00000000000..5ef35d7d587 --- /dev/null +++ b/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fix DPI scaling for debugging overlay highlights", + "packageName": "react-native-windows", + "email": "74712637+iamAbhi-916@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp index 84452bf67aa..365ba85cdb4 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp @@ -86,10 +86,11 @@ void DebuggingOverlayComponentView::HandleCommand( if (auto root = rootComponentView()) { auto rootVisual = root->OuterVisual(); auto brush = m_compContext.CreateColorBrush({204, 200, 230, 255}); + float scaleFactor = m_layoutMetrics.pointScaleFactor; for (auto &element : elements) { auto overlayVisual = m_compContext.CreateSpriteVisual(); - overlayVisual.Size({element.width, element.height}); - overlayVisual.Offset({element.x, element.y, 0.0f}); + overlayVisual.Size({element.width * scaleFactor, element.height * scaleFactor}); + overlayVisual.Offset({element.x * scaleFactor, element.y * scaleFactor, 0.0f}); overlayVisual.Brush(brush); rootVisual.InsertAt(overlayVisual, root->overlayIndex() + m_activeOverlays); From 7f1ac0936e782b9fe051aa86a079ac4fce1a2af6 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Fri, 9 Jan 2026 07:58:02 -0800 Subject: [PATCH 10/13] Add ability to customize native accessibility of custom native components (#15532) * Add ability to customize native accessibility of custom native components * format * Change files * fix * fix * Update test * Change files * pacakgelock * snapshots --- ...-78e59fbb-fe4f-4921-b941-78c82219d869.json | 7 + ...-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json | 7 + ...-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json | 7 + .../automation-commands/src/dumpVisualTree.ts | 66 +----- .../generators/GenerateComponentWindows.ts | 14 ++ .../CustomAccessibility.windows.js | 33 +++ .../src/js/utils/RNTesterList.windows.js | 4 + .../test/CustomAccessibilityTest.test.ts | 36 +++ .../CustomAccessibilityTest.test.ts.snap | 84 +++++++ .../__snapshots__/HomeUIADump.test.ts.snap | 121 ++++++++-- .../__snapshots__/snapshotPages.test.js.snap | 33 +++ .../packages.lock.json | 10 + .../windows/RNTesterApp-Fabric.sln | 18 ++ .../RNTesterApp-Fabric/RNTesterApp-Fabric.cpp | 4 + .../RNTesterApp-Fabric.vcxproj | 5 + .../RNTesterApp-Fabric/packages.lock.json | 9 + .../src/CustomAccessibilityNativeComponent.ts | 7 + packages/sample-custom-component/src/index.ts | 5 +- .../CustomAccessibility.cpp | 123 ++++++++++ .../CustomAccessibility.h | 8 + .../ReactPackageProvider.cpp | 2 + .../SampleCustomComponent.vcxproj | 2 + .../SampleCustomComponent/CalendarView.g.h | 14 ++ .../CustomAccessibility.g.h | 211 ++++++++++++++++++ .../SampleCustomComponent/DrawingIsland.g.h | 14 ++ .../SampleCustomComponent/MovingLight.g.h | 14 ++ .../Fabric/ComponentView.cpp | 26 +++ .../Fabric/ComponentView.h | 2 + .../ActivityIndicatorComponentView.cpp | 1 - .../CompositionAnnotationProvider.cpp | 7 +- .../CompositionAnnotationProvider.h | 5 +- .../CompositionDynamicAutomationProvider.cpp | 73 +++--- .../CompositionDynamicAutomationProvider.h | 1 + .../Composition/CompositionTextProvider.cpp | 11 +- .../Composition/CompositionTextProvider.h | 6 +- .../CompositionTextRangeProvider.cpp | 121 +++++----- .../CompositionTextRangeProvider.h | 6 +- .../CompositionViewComponentView.cpp | 28 ++- .../CompositionViewComponentView.h | 13 +- .../ContentIslandComponentView.cpp | 15 +- .../Composition/ContentIslandComponentView.h | 2 +- .../Fabric/Composition/ImageComponentView.cpp | 1 - .../Composition/ParagraphComponentView.cpp | 1 - .../ReactCompositionViewComponentBuilder.cpp | 8 + .../ReactCompositionViewComponentBuilder.h | 3 + .../Fabric/Composition/RootComponentView.cpp | 37 ++- .../Composition/ScrollViewComponentView.cpp | 1 - .../Composition/SwitchComponentView.cpp | 1 - .../WindowsTextInputComponentView.cpp | 1 - .../Fabric/Composition/UiaHelpers.cpp | 38 +++- .../UnimplementedNativeViewComponentView.cpp | 1 - .../IReactViewComponentBuilder.idl | 8 + 52 files changed, 1048 insertions(+), 227 deletions(-) create mode 100644 change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json create mode 100644 change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json create mode 100644 change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json create mode 100644 packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js create mode 100644 packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts create mode 100644 packages/e2e-test-app-fabric/test/__snapshots__/CustomAccessibilityTest.test.ts.snap create mode 100644 packages/sample-custom-component/src/CustomAccessibilityNativeComponent.ts create mode 100644 packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp create mode 100644 packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h create mode 100644 packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h diff --git a/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json b/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json new file mode 100644 index 00000000000..38ef36233e3 --- /dev/null +++ b/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Update to no longer include paper", + "packageName": "@react-native-windows/automation-commands", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json b/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json new file mode 100644 index 00000000000..0a0681435b3 --- /dev/null +++ b/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Add ability to customize native accessibility of custom native components", + "packageName": "@react-native-windows/codegen", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json b/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json new file mode 100644 index 00000000000..ffa6b780976 --- /dev/null +++ b/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Add ability to customize native accessibility of custom native components", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts b/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts index 00fa731102f..890ac8dbae7 100644 --- a/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts +++ b/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts @@ -37,6 +37,7 @@ export type AutomationNode = { AutomationId?: string; ControlType?: number; LocalizedControlType?: string; + Name?: string; __Children?: [AutomationNode]; }; @@ -79,7 +80,7 @@ export default async function dumpVisualTree( removeGuidsFromImageSources?: boolean; additionalProperties?: string[]; }, -): Promise { +): Promise { if (!automationClient) { throw new Error('RPC client is not enabled'); } @@ -93,21 +94,9 @@ export default async function dumpVisualTree( throw new Error(dumpResponse.message); } - const element: UIElement | VisualTree = dumpResponse.result; + const element: VisualTree = dumpResponse.result; - if ('XamlType' in element && opts?.pruneCollapsed !== false) { - pruneCollapsedElements(element); - } - - if ('XamlType' in element && opts?.deterministicOnly !== false) { - removeNonDeterministicProps(element); - } - - if ('XamlType' in element && opts?.removeDefaultProps !== false) { - removeDefaultProps(element); - } - - if (!('XamlType' in element) && opts?.removeGuidsFromImageSources !== false) { + if (opts?.removeGuidsFromImageSources !== false) { removeGuidsFromImageSources(element); } @@ -183,50 +172,3 @@ function removeGuidsFromImageSourcesHelper(node: ComponentNode) { function removeGuidsFromImageSources(visualTree: VisualTree) { removeGuidsFromImageSourcesHelper(visualTree['Component Tree']); } - -/** - * Removes trees of XAML that are not visible. - */ -function pruneCollapsedElements(element: UIElement) { - if (!element.children) { - return; - } - - element.children = element.children.filter( - child => child.Visibility !== 'Collapsed', - ); - - element.children.forEach(pruneCollapsedElements); -} - -/** - * Removes trees of properties that are not deterministic - */ -function removeNonDeterministicProps(element: UIElement) { - if (element.RenderSize) { - // RenderSize is subject to rounding, etc and should mostly be derived from - // other deterministic properties in the tree. - delete element.RenderSize; - } - - if (element.children) { - element.children.forEach(removeNonDeterministicProps); - } -} - -/** - * Removes noise from snapshot by removing properties with the default value - */ -function removeDefaultProps(element: UIElement) { - const defaultValues: [string, unknown][] = [['Tooltip', null]]; - - defaultValues.forEach(([propname, defaultValue]) => { - if (element[propname] === defaultValue) { - delete element[propname]; - } - }); - - if (element.children) { - element.children.forEach(removeDefaultProps); - } -} diff --git a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts index 9a449abde03..b08d0e51f16 100644 --- a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts +++ b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts @@ -141,6 +141,12 @@ struct Base::_COMPONENT_NAME_:: { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + ::_COMPONENT_VIEW_COMMAND_HANDLERS_:: ::_COMPONENT_VIEW_COMMAND_HANDLER_:: @@ -222,6 +228,14 @@ void Register::_COMPONENT_NAME_::NativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &Base::_COMPONENT_NAME_::::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &Base::_COMPONENT_NAME_::::Initialize) { diff --git a/packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js b/packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js new file mode 100644 index 00000000000..591861b8fa6 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js @@ -0,0 +1,33 @@ +'use strict'; + +import React from 'react'; +import {View} from 'react-native'; +import {CustomAccessibility} from 'sample-custom-component'; +import RNTesterText from '../../components/RNTesterText'; + +const CustomAccessibilityExample = () => { + return ( + + The below view should have custom accessibility + + + ); +} + +exports.displayName = 'CustomAccessibilityExample'; +exports.framework = 'React'; +exports.category = 'UI'; +exports.title = 'Custom Native Accessibility Example'; +exports.description = + 'Sample of a Custom Native Component overriding default accessibility'; + +exports.examples = [ + { + title: 'Custom Native Accessibility', + render: function (): React.Node { + return ( + + ); + }, + } +]; diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js index 4fb02a1a99b..a68931103ad 100644 --- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js +++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js @@ -80,6 +80,10 @@ const Components: Array = [ key: 'Moving Light', module: require('../examples-win/NativeComponents/MovingLight'), }, + { + key: 'Custom Native Accessibility', + module: require('../examples-win/NativeComponents/CustomAccessibility'), + }, { key: 'Native Component', module: require('../examples-win/NativeComponents/NativeComponent'), diff --git a/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts new file mode 100644 index 00000000000..31010001cae --- /dev/null +++ b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import {dumpVisualTree} from '@react-native-windows/automation-commands'; +import {goToComponentExample} from './RNTesterNavigation'; +import {app} from '@react-native-windows/automation'; +import {verifyNoErrorLogs} from './Helpers'; + +beforeAll(async () => { + // If window is partially offscreen, tests will fail to click on certain elements + await app.setWindowPosition(0, 0); + await app.setWindowSize(1000, 1250); + await goToComponentExample('Custom Native Accessibility Example'); +}); + +afterEach(async () => { + await verifyNoErrorLogs(); +}); + +describe('Custom Accessibility Tests', () => { + test('Verify custom native component has UIA label from native', async () => { + const nativeComponent = await dumpVisualTree('custom-accessibility-1'); + + // Verify that the native component reports its accessiblity label from the native code + expect(nativeComponent['Automation Tree'].Name).toBe( + 'accessiblity label from native', + ); + + const dump = await dumpVisualTree('custom-accessibility-root-1'); + expect(dump).toMatchSnapshot(); + }); +}); diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/CustomAccessibilityTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/CustomAccessibilityTest.test.ts.snap new file mode 100644 index 00000000000..136049c1614 --- /dev/null +++ b/packages/e2e-test-app-fabric/test/__snapshots__/CustomAccessibilityTest.test.ts.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Custom Accessibility Tests Verify custom native component has UIA label from native 1`] = ` +{ + "Automation Tree": { + "AutomationId": "custom-accessibility-root-1", + "ControlType": 50026, + "LocalizedControlType": "group", + "Name": "example root", + "__Children": [ + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "The below view should have custom accessibility", + "TextRangePattern.GetText": "The below view should have custom accessibility", + }, + { + "AutomationId": "custom-accessibility-1", + "ControlType": 50026, + "LocalizedControlType": "group", + "Name": "accessiblity label from native", + }, + ], + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "example root", + "TestId": "custom-accessibility-root-1", + }, + "__Children": [ + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "accessibility should not show this, as native overrides it", + "TestId": "custom-accessibility-1", + }, + }, + ], + }, + "Visual Tree": { + "Comment": "custom-accessibility-root-1", + "Offset": "0, 0, 0", + "Size": "998, 519", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "998, 19", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "998, 19", + "Visual Type": "SpriteVisual", + }, + ], + }, + { + "Offset": "0, 19, 0", + "Size": "500, 500", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(0, 128, 0, 255)", + }, + "Comment": "custom-accessibility-1", + "Offset": "0, 0, 0", + "Size": "500, 500", + "Visual Type": "SpriteVisual", + }, + ], + }, + ], + }, +} +`; diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap index e768f28a56e..a71b4f9f784 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap @@ -1374,6 +1374,87 @@ exports[`Home UIA Tree Dump Crash 1`] = ` } `; +exports[`Home UIA Tree Dump Custom Native Accessibility Example 1`] = ` +{ + "Automation Tree": { + "AutomationId": "Custom Native Accessibility Example", + "ControlType": 50026, + "IsKeyboardFocusable": true, + "LocalizedControlType": "group", + "Name": "Custom Native Accessibility Example Sample of a Custom Native Component overriding default accessibility", + "__Children": [ + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Custom Native Accessibility Example", + "TextRangePattern.GetText": "Custom Native Accessibility Example", + }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Sample of a Custom Native Component overriding default accessibility", + "TextRangePattern.GetText": "Sample of a Custom Native Component overriding default accessibility", + }, + ], + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "Custom Native Accessibility Example Sample of a Custom Native Component overriding default accessibility", + "TestId": "Custom Native Accessibility Example", + }, + "__Children": [ + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + ], + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 255, 255, 255)", + }, + "Comment": "Custom Native Accessibility Example", + "Offset": "0, 0, 0", + "Size": "966, 78", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "16, 16, 0", + "Size": "290, 25", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "290, 25", + "Visual Type": "SpriteVisual", + }, + ], + }, + { + "Offset": "16, 45, 0", + "Size": "934, 17", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "934, 17", + "Visual Type": "SpriteVisual", + }, + ], + }, + ], + }, +} +`; + exports[`Home UIA Tree Dump Cxx TurboModule 1`] = ` { "Automation Tree": { @@ -1995,12 +2076,12 @@ exports[`Home UIA Tree Dump Fabric Native Component Yoga 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "246, 25", + "Size": "246, 24", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "246, 25", + "Size": "246, 24", "Visual Type": "SpriteVisual", }, ], @@ -2076,12 +2157,12 @@ exports[`Home UIA Tree Dump Fast Path Texts 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "115, 24", + "Size": "115, 25", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "115, 24", + "Size": "115, 25", "Visual Type": "SpriteVisual", }, ], @@ -2476,7 +2557,7 @@ exports[`Home UIA Tree Dump Image 1`] = ` }, "Comment": "Image", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -2800,7 +2881,7 @@ exports[`Home UIA Tree Dump Keyboard extension Example 1`] = ` }, "Comment": "Keyboard extension Example", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -3384,12 +3465,12 @@ exports[`Home UIA Tree Dump LegacySelectableTextTest 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", }, ], @@ -3465,12 +3546,12 @@ exports[`Home UIA Tree Dump LegacyTextHitTestTest 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", }, ], @@ -4096,7 +4177,7 @@ exports[`Home UIA Tree Dump New App Screen 1`] = ` }, "Comment": "New App Screen", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -4258,7 +4339,7 @@ exports[`Home UIA Tree Dump Performance Comparison Examples 1`] = ` }, "Comment": "Performance Comparison Examples", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -4923,12 +5004,12 @@ exports[`Home UIA Tree Dump ScrollViewAnimated 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", }, ], @@ -5004,12 +5085,12 @@ exports[`Home UIA Tree Dump ScrollViewSimpleExample 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", }, ], @@ -5583,7 +5664,7 @@ exports[`Home UIA Tree Dump TextInput 1`] = ` }, "Comment": "TextInput", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -5664,7 +5745,7 @@ exports[`Home UIA Tree Dump TextInputs with key prop 1`] = ` }, "Comment": "TextInputs with key prop", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -6317,12 +6398,12 @@ exports[`Home UIA Tree Dump View 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "38, 25", + "Size": "38, 24", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "38, 25", + "Size": "38, 24", "Visual Type": "SpriteVisual", }, ], diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap index accc9f765a2..d7aada68d93 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap @@ -10923,6 +10923,39 @@ exports[`snapshotAllPages Crash 1`] = ` `; +exports[`snapshotAllPages Custom Native Accessibility Example 1`] = ` + + + The below view should have custom accessibility + + + +`; + exports[`snapshotAllPages DevSettings 1`] = ` #include "winrt/AutomationChannel.h" +// Includes from sample-custom-component +#include + #include "AutolinkedNativeModules.g.h" #include "NativeModules.h" @@ -75,6 +78,7 @@ winrt::Microsoft::ReactNative::ReactNativeHost CreateReactNativeHost( RegisterAutolinkedNativeModulePackages(host.PackageProviders()); host.PackageProviders().Append(winrt::make()); + host.PackageProviders().Append(winrt::SampleCustomComponent::ReactPackageProvider()); #if BUNDLE host.InstanceSettings().JavaScriptBundleFile(L"index.windows"); diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.vcxproj b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.vcxproj index 57bf048a361..3971a57c32f 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.vcxproj +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.vcxproj @@ -128,6 +128,11 @@ + + + {a8da218c-4cb5-48cb-a9ee-9e6337165d07} + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json index 43da89c8432..078aaa3b9c3 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json @@ -108,6 +108,15 @@ "Folly": "[1.0.0, )", "boost": "[1.83.0, )" } + }, + "samplecustomcomponent": { + "type": "Project", + "dependencies": { + "Microsoft.ReactNative": "[1.0.0, )", + "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", + "Microsoft.WindowsAppSDK": "[1.8.251106002, )", + "boost": "[1.83.0, )" + } } }, "native,Version=v0.0/win": { diff --git a/packages/sample-custom-component/src/CustomAccessibilityNativeComponent.ts b/packages/sample-custom-component/src/CustomAccessibilityNativeComponent.ts new file mode 100644 index 00000000000..bfb1595e54f --- /dev/null +++ b/packages/sample-custom-component/src/CustomAccessibilityNativeComponent.ts @@ -0,0 +1,7 @@ +import { codegenNativeComponent } from 'react-native'; +import type { ViewProps } from 'react-native'; + +export interface CustomAccessibilityProps extends ViewProps { +} + +export default codegenNativeComponent('CustomAccessibility'); diff --git a/packages/sample-custom-component/src/index.ts b/packages/sample-custom-component/src/index.ts index 182564080ea..49b2bd07d43 100644 --- a/packages/sample-custom-component/src/index.ts +++ b/packages/sample-custom-component/src/index.ts @@ -1,11 +1,14 @@ import MovingLight from './MovingLight'; -import type {MovingLightHandle} from './MovingLight'; +import type { MovingLightHandle } from './MovingLight'; import DrawingIsland from './DrawingIsland'; import CalendarView from './FabricXamlCalendarViewNativeComponent' +import CustomAccessibility from './CustomAccessibilityNativeComponent'; + export { + CustomAccessibility, DrawingIsland, MovingLight, MovingLightHandle, diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp new file mode 100644 index 00000000000..3c8d96c1773 --- /dev/null +++ b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" + +#include "codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h" + +#ifdef RNW_NEW_ARCH +#include +#include +#include +#include +#include + +namespace winrt::SampleCustomComponent { + +struct CustomAccessibilityAutomationPeer : public winrt::implements< + CustomAccessibilityAutomationPeer, + winrt::IInspectable, + IRawElementProviderFragment, + IRawElementProviderSimple> { + CustomAccessibilityAutomationPeer(const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs &args) + : m_inner(args.DefaultAutomationPeer()) {} + + virtual HRESULT __stdcall Navigate(NavigateDirection direction, IRawElementProviderFragment **pRetVal) override { + winrt::com_ptr innerAsREPF = m_inner.try_as(); + if (!innerAsREPF) + return E_FAIL; + return innerAsREPF->Navigate(direction, pRetVal); + } + + virtual HRESULT __stdcall GetRuntimeId(SAFEARRAY **pRetVal) override { + winrt::com_ptr innerAsREPF = m_inner.try_as(); + if (!innerAsREPF) + return E_FAIL; + return innerAsREPF->GetRuntimeId(pRetVal); + } + + virtual HRESULT __stdcall get_BoundingRectangle(UiaRect *pRetVal) override { + winrt::com_ptr innerAsREPF = m_inner.try_as(); + if (!innerAsREPF) + return E_FAIL; + return innerAsREPF->get_BoundingRectangle(pRetVal); + } + + virtual HRESULT __stdcall GetEmbeddedFragmentRoots(SAFEARRAY **pRetVal) override { + winrt::com_ptr innerAsREPF = m_inner.try_as(); + if (!innerAsREPF) + return E_FAIL; + return innerAsREPF->GetEmbeddedFragmentRoots(pRetVal); + } + + virtual HRESULT __stdcall SetFocus(void) override { + winrt::com_ptr innerAsREPF = m_inner.try_as(); + if (!innerAsREPF) + return E_FAIL; + return innerAsREPF->SetFocus(); + } + + virtual HRESULT __stdcall get_FragmentRoot(IRawElementProviderFragmentRoot **pRetVal) override { + winrt::com_ptr innerAsREPF = m_inner.try_as(); + if (!innerAsREPF) + return E_FAIL; + return innerAsREPF->get_FragmentRoot(pRetVal); + } + + // inherited via IRawElementProviderSimple + virtual HRESULT __stdcall get_ProviderOptions(ProviderOptions *pRetVal) override { + winrt::com_ptr innerAsREPS = m_inner.try_as(); + if (!innerAsREPS) + return E_FAIL; + return innerAsREPS->get_ProviderOptions(pRetVal); + } + + virtual HRESULT __stdcall GetPatternProvider(PATTERNID patternId, IUnknown **pRetVal) override { + winrt::com_ptr innerAsREPS = m_inner.try_as(); + if (!innerAsREPS) + return E_FAIL; + return innerAsREPS->GetPatternProvider(patternId, pRetVal); + } + + virtual HRESULT __stdcall GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) override { + winrt::com_ptr innerAsREPS = m_inner.try_as(); + if (!innerAsREPS) + return E_FAIL; + + if (propertyId == UIA_NamePropertyId) { + pRetVal->vt = VT_BSTR; + pRetVal->bstrVal = SysAllocString(L"accessiblity label from native"); + return pRetVal->bstrVal != nullptr ? S_OK : E_OUTOFMEMORY; + } + + return innerAsREPS->GetPropertyValue(propertyId, pRetVal); + } + + virtual HRESULT __stdcall get_HostRawElementProvider(IRawElementProviderSimple **pRetVal) override { + winrt::com_ptr innerAsREPS = m_inner.try_as(); + if (!innerAsREPS) + return E_FAIL; + return innerAsREPS->get_HostRawElementProvider(pRetVal); + } + + private: + winrt::Windows::Foundation::IInspectable m_inner; +}; + +struct CustomAccessibility : public winrt::implements, + Codegen::BaseCustomAccessibility { + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer( + const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs &args) noexcept override { + return winrt::make(args); + } +}; + +} // namespace winrt::SampleCustomComponent + +void RegisterCustomAccessibilityComponentView( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept { + winrt::SampleCustomComponent::Codegen::RegisterCustomAccessibilityNativeComponent< + winrt::SampleCustomComponent::CustomAccessibility>(packageBuilder, {}); +} + +#endif // #ifdef RNW_NEW_ARCH diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h new file mode 100644 index 00000000000..99c15405141 --- /dev/null +++ b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h @@ -0,0 +1,8 @@ +#pragma once + +#if defined(RNW_NEW_ARCH) + +void RegisterCustomAccessibilityComponentView( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder); + +#endif // defined(RNW_NEW_ARCH) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp index ba3a413f5a6..29f8c8905e1 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 "CustomAccessibility.h" #include "DrawingIsland.h" #include "MovingLight.h" @@ -22,6 +23,7 @@ void ReactPackageProvider::CreatePackage(IReactPackageBuilder const &packageBuil RegisterDrawingIslandComponentView(packageBuilder); RegisterMovingLightNativeComponent(packageBuilder); RegisterCalendarViewComponentView(packageBuilder); + RegisterCustomAccessibilityComponentView(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 a86fa2cd6dd..af442f14fab 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj +++ b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj @@ -103,6 +103,7 @@ + DrawingIsland.idl @@ -115,6 +116,7 @@ + Create diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h index 7df163dc929..2cb98aa9acc 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h @@ -115,6 +115,12 @@ struct BaseCalendarView { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -190,6 +196,14 @@ void RegisterCalendarViewNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseCalendarView::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseCalendarView::Initialize) { diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h new file mode 100644 index 00000000000..d372f4e7d32 --- /dev/null +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h @@ -0,0 +1,211 @@ + +/* + * This file is auto-generated from CustomAccessibilityNativeComponent spec file in flow / TypeScript. + */ +// clang-format off +#pragma once + +#include + +#ifdef RNW_NEW_ARCH +#include + +#include +#include +#endif // #ifdef RNW_NEW_ARCH + +#ifdef RNW_NEW_ARCH + +namespace winrt::SampleCustomComponent::Codegen { + +REACT_STRUCT(CustomAccessibilityProps) +struct CustomAccessibilityProps : winrt::implements { + CustomAccessibilityProps(winrt::Microsoft::ReactNative::ViewProps props, const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom) + : ViewProps(props) + { + if (cloneFrom) { + auto cloneFromProps = cloneFrom.as(); + + } + } + + void SetProp(uint32_t hash, winrt::hstring propName, winrt::Microsoft::ReactNative::IJSValueReader value) noexcept { + winrt::Microsoft::ReactNative::ReadProp(hash, propName, value, *this); + } + + const winrt::Microsoft::ReactNative::ViewProps ViewProps; +}; + +struct CustomAccessibilityEventEmitter { + CustomAccessibilityEventEmitter(const winrt::Microsoft::ReactNative::EventEmitter &eventEmitter) + : m_eventEmitter(eventEmitter) {} + + private: + winrt::Microsoft::ReactNative::EventEmitter m_eventEmitter{nullptr}; +}; + +template +struct BaseCustomAccessibility { + + virtual void UpdateProps( + const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::com_ptr &newProps, + const winrt::com_ptr &/*oldProps*/) noexcept { + m_props = newProps; + } + + // UpdateLayoutMetrics will only be called if this method is overridden + virtual void UpdateLayoutMetrics( + const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::Microsoft::ReactNative::LayoutMetrics &/*newLayoutMetrics*/, + const winrt::Microsoft::ReactNative::LayoutMetrics &/*oldLayoutMetrics*/) noexcept { + } + + // UpdateState will only be called if this method is overridden + virtual void UpdateState( + const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::Microsoft::ReactNative::IComponentState &/*newState*/) noexcept { + } + + virtual void UpdateEventEmitter(const std::shared_ptr &eventEmitter) noexcept { + m_eventEmitter = eventEmitter; + } + + // MountChildComponentView will only be called if this method is overridden + virtual void MountChildComponentView(const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::Microsoft::ReactNative::MountChildComponentViewArgs &/*args*/) noexcept { + } + + // UnmountChildComponentView will only be called if this method is overridden + virtual void UnmountChildComponentView(const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &/*args*/) noexcept { + } + + // Initialize will only be called if this method is overridden + virtual void Initialize(const winrt::Microsoft::ReactNative::ComponentView &/*view*/) noexcept { + } + + // CreateVisual will only be called if this method is overridden + virtual winrt::Microsoft::UI::Composition::Visual CreateVisual(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + return view.as().Compositor().CreateSpriteVisual(); + } + + // FinalizeUpdate will only be called if this method is overridden + virtual void FinalizeUpdate(const winrt::Microsoft::ReactNative::ComponentView &/*view*/, + winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { + } + + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + + + + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } + const winrt::com_ptr& Props() const { return m_props; } + +private: + winrt::com_ptr m_props; + std::shared_ptr m_eventEmitter; +}; + +template +void RegisterCustomAccessibilityNativeComponent( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder, + std::function builderCallback) noexcept { + packageBuilder.as().AddViewComponent( + L"CustomAccessibility", [builderCallback](winrt::Microsoft::ReactNative::IReactViewComponentBuilder const &builder) noexcept { + auto compBuilder = builder.as(); + + builder.SetCreateProps([](winrt::Microsoft::ReactNative::ViewProps props, + const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom) noexcept { + return winrt::make(props, cloneFrom); + }); + + builder.SetUpdatePropsHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::IComponentProps &newProps, + const winrt::Microsoft::ReactNative::IComponentProps &oldProps) noexcept { + auto userData = view.UserData().as(); + userData->UpdateProps(view, newProps ? newProps.as() : nullptr, oldProps ? oldProps.as() : nullptr); + }); + + compBuilder.SetUpdateLayoutMetricsHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::LayoutMetrics &newLayoutMetrics, + const winrt::Microsoft::ReactNative::LayoutMetrics &oldLayoutMetrics) noexcept { + auto userData = view.UserData().as(); + userData->UpdateLayoutMetrics(view, newLayoutMetrics, oldLayoutMetrics); + }); + + builder.SetUpdateEventEmitterHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::EventEmitter &eventEmitter) noexcept { + auto userData = view.UserData().as(); + userData->UpdateEventEmitter(std::make_shared(eventEmitter)); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::FinalizeUpdate != &BaseCustomAccessibility::FinalizeUpdate) { + builder.SetFinalizeUpdateHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + winrt::Microsoft::ReactNative::ComponentViewUpdateMask mask) noexcept { + auto userData = view.UserData().as(); + userData->FinalizeUpdate(view, mask); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::UpdateState != &BaseCustomAccessibility::UpdateState) { + builder.SetUpdateStateHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::IComponentState &newState) noexcept { + auto userData = view.UserData().as(); + userData->UpdateState(view, newState); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::MountChildComponentView != &BaseCustomAccessibility::MountChildComponentView) { + builder.SetMountChildComponentViewHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::MountChildComponentViewArgs &args) noexcept { + auto userData = view.UserData().as(); + return userData->MountChildComponentView(view, args); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::UnmountChildComponentView != &BaseCustomAccessibility::UnmountChildComponentView) { + builder.SetUnmountChildComponentViewHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &args) noexcept { + auto userData = view.UserData().as(); + return userData->UnmountChildComponentView(view, args); + }); + } + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseCustomAccessibility::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + auto userData = winrt::make_self(); + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseCustomAccessibility::Initialize) { + userData->Initialize(view); + } + view.UserData(*userData); + }); + + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateVisual != &BaseCustomAccessibility::CreateVisual) { + compBuilder.SetCreateVisualHandler([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + auto userData = view.UserData().as(); + return userData->CreateVisual(view); + }); + } + + // Allow app to further customize the builder + if (builderCallback) { + builderCallback(compBuilder); + } + }); +} + +} // namespace winrt::SampleCustomComponent::Codegen + +#endif // #ifdef RNW_NEW_ARCH diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h index 32f9101b02e..acb9244cef6 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h @@ -95,6 +95,12 @@ struct BaseDrawingIsland { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -170,6 +176,14 @@ void RegisterDrawingIslandNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseDrawingIsland::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseDrawingIsland::Initialize) { diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h index f7eb7736288..4fbfa7f0d99 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h @@ -136,6 +136,12 @@ struct BaseMovingLight { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + // You must provide an implementation of this method to handle the "setLightOn" command virtual void HandleSetLightOnCommand(bool value) noexcept = 0; @@ -229,6 +235,14 @@ void RegisterMovingLightNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseMovingLight::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseMovingLight::Initialize) { diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp index b1d0593b3c9..6c2aee7ba13 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp @@ -8,6 +8,7 @@ #include "DynamicReader.h" #include "ComponentView.g.cpp" +#include "CreateAutomationPeerArgs.g.h" #include "LayoutMetricsChangedArgs.g.cpp" #include "MountChildComponentViewArgs.g.cpp" #include "UnmountChildComponentViewArgs.g.cpp" @@ -641,7 +642,32 @@ facebook::react::Tag ComponentView::hitTest( return -1; } +struct CreateAutomationPeerArgs + : public winrt::Microsoft::ReactNative::implementation::CreateAutomationPeerArgsT { + CreateAutomationPeerArgs(winrt::Windows::Foundation::IInspectable defaultAutomationPeer) + : m_defaultAutomationPeer(defaultAutomationPeer) {} + + winrt::Windows::Foundation::IInspectable DefaultAutomationPeer() const noexcept { + return m_defaultAutomationPeer; + } + + private: + winrt::Windows::Foundation::IInspectable m_defaultAutomationPeer; +}; + winrt::IInspectable ComponentView::EnsureUiaProvider() noexcept { + if (m_uiaProvider == nullptr) { + if (m_builder && m_builder->CreateAutomationPeerHandler()) { + m_uiaProvider = m_builder->CreateAutomationPeerHandler()( + *this, winrt::make(CreateAutomationProvider())); + } else { + m_uiaProvider = CreateAutomationProvider(); + } + } + return m_uiaProvider; +} + +winrt::Windows::Foundation::IInspectable ComponentView::CreateAutomationProvider() noexcept { return nullptr; } diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h index 66a1de581fd..7700ac9e3fa 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h @@ -208,6 +208,7 @@ struct ComponentView virtual facebook::react::Tag hitTest(facebook::react::Point pt, facebook::react::Point &localPt, bool ignorePointerEvents = false) const noexcept; virtual winrt::Windows::Foundation::IInspectable EnsureUiaProvider() noexcept; + virtual winrt::Windows::Foundation::IInspectable CreateAutomationProvider() noexcept; virtual std::optional getAccessiblityValue() noexcept; virtual void setAcccessiblityValue(std::string &&value) noexcept; virtual bool getAcccessiblityIsReadOnly() noexcept; @@ -265,6 +266,7 @@ struct ComponentView facebook::react::LayoutMetrics m_layoutMetrics; winrt::Windows::Foundation::Collections::IVector m_children{ winrt::single_threaded_vector()}; + winrt::Windows::Foundation::IInspectable m_uiaProvider{nullptr}; winrt::event< winrt::Windows::Foundation::EventHandler> diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp index 7cd4a65d052..8d98ef9840d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp @@ -4,7 +4,6 @@ #pragma once #include "ActivityIndicatorComponentView.h" -#include "CompositionDynamicAutomationProvider.h" #include #include diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp index 9e4a2963678..859973dd75f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp @@ -8,11 +8,8 @@ namespace winrt::Microsoft::ReactNative::implementation { CompositionAnnotationProvider::CompositionAnnotationProvider( - const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView, - CompositionDynamicAutomationProvider *parentProvider) noexcept - : m_view{componentView} { - m_parentProvider.copy_from(parentProvider); -} + const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept + : m_view{componentView} {} HRESULT __stdcall CompositionAnnotationProvider::get_AnnotationTypeId(int *retVal) { if (retVal == nullptr) return E_POINTER; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h index d82af128808..4b682f59646 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -12,8 +11,7 @@ namespace winrt::Microsoft::ReactNative::implementation { class CompositionAnnotationProvider : public winrt::implements { public: CompositionAnnotationProvider( - const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView, - CompositionDynamicAutomationProvider *parentProvider) noexcept; + const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept; // inherited via IAnnotationProvider virtual HRESULT __stdcall get_AnnotationTypeId(int *retVal) override; @@ -25,7 +23,6 @@ class CompositionAnnotationProvider : public winrt::implements m_annotationProvider; - winrt::com_ptr m_parentProvider; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index 8e1f2d5dbae..20c2b2820e9 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -287,8 +287,8 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE strongView.try_as())) { if (!m_textProvider) { m_textProvider = winrt::make( - strongView.as(), this) - .try_as(); + strongView.as()) + .as(); } m_textProvider.as().copy_to(pRetVal); } @@ -297,8 +297,8 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE strongView.try_as()) { if (!m_textProvider) { m_textProvider = winrt::make( - strongView.as(), this) - .try_as(); + strongView.as()) + .as(); } m_textProvider.as().copy_to(pRetVal); } @@ -307,8 +307,8 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE accessibilityAnnotationHasValue(props->accessibilityAnnotation)) { if (!m_annotationProvider) { m_annotationProvider = winrt::make( - strongView.as(), this) - .try_as(); + strongView.as()) + .as(); } m_annotationProvider.as().copy_to(pRetVal); } @@ -952,9 +952,18 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetSelection(SAFEARRAY * std::vector selectedItems; for (size_t i = 0; i < m_selectionItems.size(); i++) { auto selectionItem = m_selectionItems.at(i); - auto provider = selectionItem.as(); + + winrt::com_ptr unkSelectionItemProvider; + auto hr = selectionItem->GetPatternProvider(UIA_SelectionItemPatternId, unkSelectionItemProvider.put()); + if (FAILED(hr)) + return hr; + + auto selectionItemProvider = unkSelectionItemProvider.try_as(); + if (!selectionItemProvider) + return E_FAIL; + BOOL selected; - auto hr = provider->get_IsSelected(&selected); + hr = selectionItemProvider->get_IsSelected(&selected); if (hr == S_OK && selected) { selectedItems.push_back(int(i)); } @@ -1013,27 +1022,28 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::get_IsSelected(BOOL *pRe return S_OK; } -IRawElementProviderSimple *findSelectionContainer(winrt::Microsoft::ReactNative::ComponentView current) { +winrt::Microsoft::ReactNative::ComponentView findSelectionContainer( + winrt::Microsoft::ReactNative::ComponentView current) noexcept { if (!current) return nullptr; - auto props = std::static_pointer_cast( - winrt::get_self(current)->props()); - if (props->accessibilityState.has_value() && props->accessibilityState->multiselectable.has_value() && - props->accessibilityState->required.has_value()) { - auto uiaProvider = - current.as()->EnsureUiaProvider(); - if (uiaProvider != nullptr) { - auto spProviderSimple = uiaProvider.try_as(); - if (spProviderSimple != nullptr) { - spProviderSimple->AddRef(); - return spProviderSimple.get(); - } + if (auto viewbase = current.try_as()) { + auto props = viewbase->viewProps(); + if (props->accessibilityState.has_value() && props->accessibilityState->multiselectable.has_value() && + props->accessibilityState->required.has_value()) { + return current; } - } else { - return findSelectionContainer(current.Parent()); } - return nullptr; + return findSelectionContainer(current.Parent()); +} + +winrt::Microsoft::ReactNative::ComponentView CompositionDynamicAutomationProvider::GetSelectionContainer() noexcept { + auto strongView = m_view.view(); + + if (!strongView) + return nullptr; + + return findSelectionContainer(strongView.Parent()); } HRESULT __stdcall CompositionDynamicAutomationProvider::get_SelectionContainer(IRawElementProviderSimple **pRetVal) { @@ -1044,7 +1054,20 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::get_SelectionContainer(I if (!strongView) return UIA_E_ELEMENTNOTAVAILABLE; - *pRetVal = findSelectionContainer(strongView.Parent()); + *pRetVal = nullptr; + + auto selectionContainerView = GetSelectionContainer(); + auto uiaProvider = + winrt::get_self(selectionContainerView) + ->EnsureUiaProvider(); + if (uiaProvider != nullptr) { + auto spProviderSimple = uiaProvider.try_as(); + if (spProviderSimple != nullptr) { + spProviderSimple->AddRef(); + *pRetVal = spProviderSimple.get(); + } + } + return S_OK; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h index 7009a6e21b8..8fc2bcdeb79 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h @@ -97,6 +97,7 @@ class CompositionDynamicAutomationProvider : public winrt::implements< void AddToSelectionItems(winrt::com_ptr &item); void RemoveFromSelectionItems(winrt::com_ptr &item); + winrt::Microsoft::ReactNative::ComponentView GetSelectionContainer() noexcept; private: ::Microsoft::ReactNative::ReactTaggedView m_view; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp index ef997fe0ae4..cdc309090c2 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp @@ -10,10 +10,8 @@ namespace winrt::Microsoft::ReactNative::implementation { CompositionTextProvider::CompositionTextProvider( - const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView, - CompositionDynamicAutomationProvider *parentProvider) noexcept + const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept : m_view{componentView} { - m_parentProvider.copy_from(parentProvider); EnsureTextRangeProvider(); } @@ -24,10 +22,9 @@ void CompositionTextProvider::EnsureTextRangeProvider() { return; if (!m_textRangeProvider) { - m_textRangeProvider = - winrt::make( - strongView.as(), m_parentProvider.get()) - .try_as(); + m_textRangeProvider = winrt::make( + strongView.as()) + .as(); } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h index cb68ad7bfe9..28195b2fbd8 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -11,9 +10,7 @@ namespace winrt::Microsoft::ReactNative::implementation { class CompositionTextProvider : public winrt::implements { public: - CompositionTextProvider( - const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView, - CompositionDynamicAutomationProvider *parentProvider) noexcept; + CompositionTextProvider(const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept; // inherited via ITextProvider virtual HRESULT __stdcall get_DocumentRange(ITextRangeProvider **pRetVal) override; @@ -35,7 +32,6 @@ class CompositionTextProvider : public winrt::implements m_textRangeProvider; - winrt::com_ptr m_parentProvider; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp index e2260913e15..eef088606e0 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp @@ -11,18 +11,14 @@ namespace winrt::Microsoft::ReactNative::implementation { CompositionTextRangeProvider::CompositionTextRangeProvider( - const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView, - CompositionDynamicAutomationProvider *parentProvider) noexcept - : m_view{componentView} { - m_parentProvider.copy_from(parentProvider); -} + const winrt::Microsoft::ReactNative::ComponentView &componentView) noexcept + : m_view{componentView} {} HRESULT __stdcall CompositionTextRangeProvider::Clone(ITextRangeProvider **pRetVal) { if (pRetVal == nullptr) return E_POINTER; - auto clone = winrt::make( - m_view.view().as(), m_parentProvider.get()); + auto clone = winrt::make(m_view.view()); *pRetVal = clone.detach(); return S_OK; } @@ -91,13 +87,13 @@ HRESULT __stdcall CompositionTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEI if (!strongView) return UIA_E_ELEMENTNOTAVAILABLE; - auto props = std::static_pointer_cast( + auto props = std::static_pointer_cast( winrt::get_self(strongView)->props()); - auto textinputProps = std::static_pointer_cast( - winrt::get_self(strongView)->props()); + auto asParagraph = + strongView.try_as(); - auto isTextInput = + auto asTextInput = strongView.try_as(); if (props == nullptr) @@ -106,15 +102,16 @@ HRESULT __stdcall CompositionTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEI if (attributeId == UIA_BackgroundColorAttributeId) { pRetVal->vt = VT_I4; pRetVal->lVal = (*props->backgroundColor).AsColorRefWithAlpha(); - } else if (attributeId == UIA_CapStyleAttributeId) { + } else if (attributeId == UIA_CapStyleAttributeId && asParagraph) { pRetVal->vt = VT_I4; auto fontVariant = facebook::react::FontVariant::Default; auto textTransform = facebook::react::TextTransform::None; - if (props->textAttributes.fontVariant.has_value()) { - fontVariant = props->textAttributes.fontVariant.value(); + + if (asParagraph->paragraphProps().textAttributes.fontVariant.has_value()) { + fontVariant = asParagraph->paragraphProps().textAttributes.fontVariant.value(); } - if (props->textAttributes.textTransform.has_value()) { - textTransform = props->textAttributes.textTransform.value(); + if (asParagraph->paragraphProps().textAttributes.textTransform.has_value()) { + textTransform = asParagraph->paragraphProps().textAttributes.textTransform.value(); } if (fontVariant == facebook::react::FontVariant::SmallCaps) { pRetVal->lVal = CapStyle_SmallCap; @@ -125,39 +122,44 @@ HRESULT __stdcall CompositionTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEI } else if (textTransform == facebook::react::TextTransform::Uppercase) { pRetVal->lVal = CapStyle_AllCap; } - } else if (attributeId == UIA_FontNameAttributeId) { + } else if (attributeId == UIA_FontNameAttributeId && asParagraph) { pRetVal->vt = VT_BSTR; - auto fontName = props->textAttributes.fontFamily; + auto fontName = asParagraph->paragraphProps().textAttributes.fontFamily; if (fontName.empty()) { fontName = "Segoe UI"; } std::wstring wfontName(fontName.begin(), fontName.end()); pRetVal->bstrVal = SysAllocString(wfontName.c_str()); - } else if (attributeId == UIA_FontSizeAttributeId) { + } else if (attributeId == UIA_FontSizeAttributeId && asParagraph) { pRetVal->vt = VT_R8; - pRetVal->dblVal = props->textAttributes.fontSize; - } else if (attributeId == UIA_FontWeightAttributeId) { - if (props->textAttributes.fontWeight.has_value()) { + pRetVal->dblVal = asParagraph->paragraphProps().textAttributes.fontSize; + } else if (attributeId == UIA_FontWeightAttributeId && asParagraph) { + if (asParagraph->paragraphProps().textAttributes.fontWeight.has_value()) { pRetVal->vt = VT_I4; - pRetVal->lVal = static_cast(props->textAttributes.fontWeight.value()); + pRetVal->lVal = static_cast(asParagraph->paragraphProps().textAttributes.fontWeight.value()); } - } else if (attributeId == UIA_ForegroundColorAttributeId) { + } else if (attributeId == UIA_ForegroundColorAttributeId && asParagraph) { pRetVal->vt = VT_I4; - pRetVal->lVal = (*props->textAttributes.foregroundColor).AsColorRefWithAlpha(); - } else if (attributeId == UIA_IsItalicAttributeId) { + pRetVal->lVal = (*asParagraph->paragraphProps().textAttributes.foregroundColor).AsColorRefWithAlpha(); + } else if (attributeId == UIA_IsItalicAttributeId && asParagraph) { pRetVal->vt = VT_BOOL; - pRetVal->boolVal = (props->textAttributes.fontStyle.has_value() && - props->textAttributes.fontStyle.value() == facebook::react::FontStyle::Italic) + pRetVal->boolVal = + (asParagraph->paragraphProps().textAttributes.fontStyle.has_value() && + asParagraph->paragraphProps().textAttributes.fontStyle.value() == facebook::react::FontStyle::Italic) ? VARIANT_TRUE : VARIANT_FALSE; } else if (attributeId == UIA_IsReadOnlyAttributeId) { pRetVal->vt = VT_BOOL; - pRetVal->boolVal = isTextInput ? textinputProps->editable ? VARIANT_FALSE : VARIANT_TRUE : VARIANT_TRUE; - } else if (attributeId == UIA_HorizontalTextAlignmentAttributeId) { + if (asTextInput) { + pRetVal->boolVal = asTextInput->windowsTextInputProps().editable ? VARIANT_FALSE : VARIANT_TRUE; + } else { + pRetVal->boolVal = VARIANT_TRUE; + } + } else if (attributeId == UIA_HorizontalTextAlignmentAttributeId && asParagraph) { pRetVal->vt = VT_I4; auto textAlign = facebook::react::TextAlignment::Center; - if (props->textAttributes.alignment.has_value()) { - textAlign = props->textAttributes.alignment.value(); + if (asParagraph->paragraphProps().textAttributes.alignment.has_value()) { + textAlign = asParagraph->paragraphProps().textAttributes.alignment.value(); } if (textAlign == facebook::react::TextAlignment::Left) { pRetVal->lVal = HorizontalTextAlignment_Left; @@ -170,40 +172,42 @@ HRESULT __stdcall CompositionTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEI } else if (textAlign == facebook::react::TextAlignment::Natural) { pRetVal->lVal = HorizontalTextAlignment_Left; } - } else if (attributeId == UIA_StrikethroughColorAttributeId) { - if (props->textAttributes.textDecorationLineType.has_value() && - (props->textAttributes.textDecorationLineType.value() == + } else if (attributeId == UIA_StrikethroughColorAttributeId && asParagraph) { + if (asParagraph->paragraphProps().textAttributes.textDecorationLineType.has_value() && + (asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::Strikethrough || - props->textAttributes.textDecorationLineType.value() == + asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::UnderlineStrikethrough)) { pRetVal->vt = VT_I4; - pRetVal->lVal = (*props->textAttributes.textDecorationColor).AsColorRefWithAlpha(); + pRetVal->lVal = (*asParagraph->paragraphProps().textAttributes.textDecorationColor).AsColorRefWithAlpha(); } - } else if (attributeId == UIA_StrikethroughStyleAttributeId) { - if (props->textAttributes.textDecorationLineType.has_value() && - (props->textAttributes.textDecorationLineType.value() == + } else if (attributeId == UIA_StrikethroughStyleAttributeId && asParagraph) { + if (asParagraph->paragraphProps().textAttributes.textDecorationLineType.has_value() && + (asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::Strikethrough || - props->textAttributes.textDecorationLineType.value() == + asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::UnderlineStrikethrough)) { pRetVal->vt = VT_I4; - auto style = props->textAttributes.textDecorationStyle.value(); + auto style = asParagraph->paragraphProps().textAttributes.textDecorationStyle.value(); pRetVal->lVal = GetTextDecorationLineStyle(style); } - } else if (attributeId == UIA_UnderlineColorAttributeId) { - if (props->textAttributes.textDecorationLineType.has_value() && - (props->textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::Underline || - props->textAttributes.textDecorationLineType.value() == + } else if (attributeId == UIA_UnderlineColorAttributeId && asParagraph) { + if (asParagraph->paragraphProps().textAttributes.textDecorationLineType.has_value() && + (asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() == + facebook::react::TextDecorationLineType::Underline || + asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::UnderlineStrikethrough)) { pRetVal->vt = VT_I4; - pRetVal->lVal = (*props->textAttributes.textDecorationColor).AsColorRefWithAlpha(); + pRetVal->lVal = (*asParagraph->paragraphProps().textAttributes.textDecorationColor).AsColorRefWithAlpha(); } - } else if (attributeId == UIA_UnderlineStyleAttributeId) { - if (props->textAttributes.textDecorationLineType.has_value() && - (props->textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::Underline || - props->textAttributes.textDecorationLineType.value() == + } else if (attributeId == UIA_UnderlineStyleAttributeId && asParagraph) { + if (asParagraph->paragraphProps().textAttributes.textDecorationLineType.has_value() && + (asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() == + facebook::react::TextDecorationLineType::Underline || + asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::UnderlineStrikethrough)) { pRetVal->vt = VT_I4; - auto style = props->textAttributes.textDecorationStyle.value(); + auto style = asParagraph->paragraphProps().textAttributes.textDecorationStyle.value(); pRetVal->lVal = GetTextDecorationLineStyle(style); } } @@ -214,7 +218,18 @@ HRESULT __stdcall CompositionTextRangeProvider::GetBoundingRectangles(SAFEARRAY if (pRetVal == nullptr) return E_POINTER; UiaRect rect; - auto hr = m_parentProvider->get_BoundingRectangle(&rect); + + auto strongView = m_view.view(); + if (!strongView) + return UIA_E_ELEMENTNOTAVAILABLE; + + auto componentView = strongView.as(); + auto provider = componentView->EnsureUiaProvider(); + auto repf = provider.try_as(); + if (!repf) + return E_FAIL; + + auto hr = repf->get_BoundingRectangle(&rect); if (FAILED(hr)) return hr; *pRetVal = SafeArrayCreateVector(VT_R8, 0, 4); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h index 18ec13688bf..8fb3308dbc4 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -12,9 +11,7 @@ namespace winrt::Microsoft::ReactNative::implementation { class CompositionTextRangeProvider : public winrt::implements { public: - CompositionTextRangeProvider( - const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView, - CompositionDynamicAutomationProvider *parentProvider) noexcept; + CompositionTextRangeProvider(const winrt::Microsoft::ReactNative::ComponentView &componentView) noexcept; // inherited via ITextRangeProvider virtual HRESULT __stdcall Clone(ITextRangeProvider **pRetVal) override; @@ -53,7 +50,6 @@ class CompositionTextRangeProvider : public winrt::implements m_parentProvider; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index b41120fe159..95a2e752c7c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -857,14 +857,13 @@ void ComponentView::updateAccessibilityProps( if ((oldViewProps.accessibilityState.has_value() && oldViewProps.accessibilityState->selected.has_value()) != ((newViewProps.accessibilityState.has_value() && newViewProps.accessibilityState->selected.has_value()))) { - auto compProvider = - EnsureUiaProvider() - .try_as(); - if (compProvider) { + EnsureUiaProvider(); + if (m_innerAutomationProvider) { if ((newViewProps.accessibilityState.has_value() && newViewProps.accessibilityState->selected.has_value())) { - winrt::Microsoft::ReactNative::implementation::AddSelectionItemsToContainer(compProvider.get()); + winrt::Microsoft::ReactNative::implementation::AddSelectionItemsToContainer(m_innerAutomationProvider.get()); } else { - winrt::Microsoft::ReactNative::implementation::RemoveSelectionItemsFromContainer(compProvider.get()); + winrt::Microsoft::ReactNative::implementation::RemoveSelectionItemsFromContainer( + m_innerAutomationProvider.get()); } } } @@ -1354,12 +1353,17 @@ std::string ViewComponentView::DefaultControlType() const noexcept { return "group"; } -winrt::IInspectable ComponentView::EnsureUiaProvider() noexcept { - if (m_uiaProvider == nullptr) { - m_uiaProvider = - winrt::make(*get_strong()); - } - return m_uiaProvider; +winrt::Windows::Foundation::IInspectable ComponentView::CreateAutomationProvider() noexcept { + Assert(!m_innerAutomationProvider); + m_innerAutomationProvider = + winrt::make_self( + *get_strong()); + return *m_innerAutomationProvider; +} + +const winrt::com_ptr + &ComponentView::InnerAutomationProvider() const noexcept { + return m_innerAutomationProvider; } bool IntersectRect(RECT *prcDst, const RECT &prcSrc1, const RECT &prcSrc2) { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index 588af7ad463..fed4797026c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -19,8 +19,11 @@ namespace Microsoft::ReactNative { struct CompContext; } // namespace Microsoft::ReactNative -namespace winrt::Microsoft::ReactNative::Composition::implementation { +namespace winrt::Microsoft::ReactNative::implementation { +class CompositionDynamicAutomationProvider; +} +namespace winrt::Microsoft::ReactNative::Composition::implementation { struct FocusPrimitive { std::shared_ptr m_focusInnerPrimitive; std::shared_ptr m_focusOuterPrimitive; @@ -100,7 +103,9 @@ struct ComponentView : public ComponentViewT< comp::CompositionPropertySet EnsureCenterPointPropertySet() noexcept; void EnsureTransformMatrixFacade() noexcept; - winrt::IInspectable EnsureUiaProvider() noexcept override; + winrt::Windows::Foundation::IInspectable CreateAutomationProvider() noexcept override; + const winrt::com_ptr + &InnerAutomationProvider() const noexcept; std::optional getAccessiblityValue() noexcept override; void setAcccessiblityValue(std::string &&value) noexcept override; bool getAcccessiblityIsReadOnly() noexcept override; @@ -130,7 +135,9 @@ struct ComponentView : public ComponentViewT< facebook::react::Point &ptContent, facebook::react::Point &localPt) const noexcept; - winrt::IInspectable m_uiaProvider{nullptr}; + // Most access should be through EnsureUIAProvider, instead of direct access to this. + winrt::com_ptr + m_innerAutomationProvider; winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext m_compContext; comp::CompositionPropertySet m_centerPropSet{nullptr}; facebook::react::SharedViewEventEmitter m_eventEmitter; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp index f2a84a4c646..11cb5cc0565 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -111,12 +111,11 @@ void ContentIslandComponentView::ParentLayoutChanged() noexcept { }); } -winrt::IInspectable ContentIslandComponentView::EnsureUiaProvider() noexcept { - if (m_uiaProvider == nullptr) { - m_uiaProvider = winrt::make( - *get_strong(), m_childSiteLink); - } - return m_uiaProvider; +winrt::Windows::Foundation::IInspectable ContentIslandComponentView::CreateAutomationProvider() noexcept { + m_innerAutomationProvider = + winrt::make_self( + *get_strong(), m_childSiteLink); + return *m_innerAutomationProvider; } bool ContentIslandComponentView::focusable() const noexcept { @@ -283,6 +282,10 @@ void ContentIslandComponentView::ConfigureChildSiteLinkAutomation() noexcept { args.AutomationProvider(nullptr); args.Handled(true); }); + + if (m_innerAutomationProvider) { + m_innerAutomationProvider->SetChildSiteLink(m_childSiteLink); + } } } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h index 013ae66f2f0..e6f5fe8808a 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h @@ -43,7 +43,7 @@ struct ContentIslandComponentView : ContentIslandComponentViewT #include #include -#include "CompositionDynamicAutomationProvider.h" #include "CompositionHelpers.h" #include "RootComponentView.h" diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp index e5cbd80f851..acd3d69fa89 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp @@ -17,7 +17,6 @@ #include #include #include -#include "CompositionDynamicAutomationProvider.h" #include "CompositionHelpers.h" #include "RootComponentView.h" #include "TextDrawing.h" diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp index 83b25e3c727..51d8ce17e2a 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp @@ -227,6 +227,14 @@ void ReactCompositionViewComponentBuilder::SetUnmountChildComponentViewHandler( m_unmountChildComponentViewHandler = impl; } +void ReactCompositionViewComponentBuilder::SetCreateAutomationPeerHandler(CreateAutomationPeerDelegate impl) noexcept { + m_createAutomationPeerHandler = impl; +} + +const CreateAutomationPeerDelegate &ReactCompositionViewComponentBuilder::CreateAutomationPeerHandler() const noexcept { + return m_createAutomationPeerHandler; +} + const UnmountChildComponentViewDelegate &ReactCompositionViewComponentBuilder::UnmountChildComponentViewHandler() const noexcept { return m_unmountChildComponentViewHandler; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h index f9d9ab9a227..5984bf94cb6 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h @@ -39,6 +39,7 @@ struct ReactCompositionViewComponentBuilder void SetUpdateEventEmitterHandler(UpdateEventEmitterDelegate impl) noexcept; void SetMountChildComponentViewHandler(MountChildComponentViewDelegate impl) noexcept; void SetUnmountChildComponentViewHandler(UnmountChildComponentViewDelegate impl) noexcept; + void SetCreateAutomationPeerHandler(CreateAutomationPeerDelegate impl) noexcept; public: // Composition::IReactCompositionViewComponentBuilder void SetViewComponentViewInitializer(const ViewComponentViewInitializer &initializer) noexcept; @@ -77,6 +78,7 @@ struct ReactCompositionViewComponentBuilder const CreateVisualDelegate &CreateVisualHandler() const noexcept; const winrt::Microsoft::ReactNative::Composition::Experimental::IVisualToMountChildrenIntoDelegate & VisualToMountChildrenIntoHandler() const noexcept; + const CreateAutomationPeerDelegate &CreateAutomationPeerHandler() const noexcept; private: void InitializeComponentView(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept; @@ -103,6 +105,7 @@ struct ReactCompositionViewComponentBuilder winrt::Microsoft::ReactNative::UpdateEventEmitterDelegate m_updateEventEmitterHandler; winrt::Microsoft::ReactNative::MountChildComponentViewDelegate m_mountChildComponentViewHandler; winrt::Microsoft::ReactNative::UnmountChildComponentViewDelegate m_unmountChildComponentViewHandler; + winrt::Microsoft::ReactNative::CreateAutomationPeerDelegate m_createAutomationPeerHandler; winrt::Microsoft::ReactNative::Composition::CreateVisualDelegate m_createVisualHandler; winrt::Microsoft::ReactNative::Composition::Experimental::IVisualToMountChildrenIntoDelegate diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp index b5ca1da68c1..0f9c2ba4832 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -9,6 +9,7 @@ #include #include #include "CompositionRootAutomationProvider.h" +#include "ContentIslandComponentView.h" #include "ParagraphComponentView.h" #include "ReactNativeIsland.h" #include "Theme.h" @@ -296,7 +297,41 @@ winrt::IInspectable RootComponentView::UiaProviderFromPoint(const POINT &ptPixel if (view == nullptr) return nullptr; - return winrt::get_self(view)->EnsureUiaProvider(); + auto uiaProvider = + winrt::get_self(view)->EnsureUiaProvider(); + + if (auto contentIsland = + view.try_as()) { + if (contentIsland->InnerAutomationProvider()) { + if (auto childProvider = contentIsland->InnerAutomationProvider()->TryGetChildSiteLinkAutomationProvider()) { + // ChildProvider is the the automation provider from the ChildSiteLink. In the case of WinUI, this + // is a pointer to WinUI's internal CUIAHostWindow object. + // It seems odd, but even though this node doesn't behave as a fragment root in our case (the real fragment root + // is the RootComponentView's UIA provider), we still use its IRawElementProviderFragmentRoot -- just so + // we can do the ElementProviderFromPoint call. (this was recommended by the team who did the initial + // architecture work). + if (auto fragmentRoot = childProvider.try_as()) { + com_ptr frag; + // WinUI then does its own hitTest inside the XAML tree. + fragmentRoot->ElementProviderFromPoint( + ptScreen + .x, // Note since we're going through IRawElementProviderFragment the coordinates are in screen space. + ptScreen.y, + frag.put()); + // We return the specific child provider(frag) when hosted XAML has an element + // under the cursor. This satisfies the UIA "element at point" contract and exposes + // the control’s patterns/properties. If the hosted tree finds nothing, we fall back + // to the RNW container’s provider (uiaProvider) to keep the island accessible. + // (A Microsoft_UI_Xaml!CUIAWrapper object) + if (frag) { + return frag.as(); + } + } + } + } + } + + return uiaProvider; } float RootComponentView::FontSizeMultiplier() const noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index 5182e4e2f74..19e31bc25f0 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -20,7 +20,6 @@ #include #include #include -#include "CompositionDynamicAutomationProvider.h" #include "JSValueReader.h" #include "RootComponentView.h" diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp index 7a79a2a7e88..6adffd46183 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp @@ -7,7 +7,6 @@ #include "SwitchComponentView.h" #include #include -#include "CompositionDynamicAutomationProvider.h" #include "RootComponentView.h" #include "UiaHelpers.h" diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index fec096fd8c2..dbb79ec1855 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -6,7 +6,6 @@ #include "WindowsTextInputComponentView.h" #include -#include #include #include #include diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp index 68551fd5f22..ebdad34024e 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -513,27 +513,45 @@ ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept { } void AddSelectionItemsToContainer(CompositionDynamicAutomationProvider *provider) noexcept { - winrt::com_ptr selectionContainer; - provider->get_SelectionContainer(selectionContainer.put()); - if (!selectionContainer) + auto selectionContainerView = provider->GetSelectionContainer(); + if (!selectionContainerView) return; - auto selectionContainerProvider = selectionContainer.as(); + + auto selectionContainerCompView = + selectionContainerView.try_as(); + if (!selectionContainerCompView) + return; + + selectionContainerCompView->EnsureUiaProvider(); + + if (!selectionContainerCompView->InnerAutomationProvider()) + return; + auto simpleProvider = static_cast(provider); winrt::com_ptr simpleProviderPtr; simpleProviderPtr.copy_from(simpleProvider); - selectionContainerProvider->AddToSelectionItems(simpleProviderPtr); + selectionContainerCompView->InnerAutomationProvider()->AddToSelectionItems(simpleProviderPtr); } void RemoveSelectionItemsFromContainer(CompositionDynamicAutomationProvider *provider) noexcept { - winrt::com_ptr selectionContainer; - provider->get_SelectionContainer(selectionContainer.put()); - if (!selectionContainer) + auto selectionContainerView = provider->GetSelectionContainer(); + if (!selectionContainerView) return; - auto selectionContainerProvider = selectionContainer.as(); + + auto selectionContainerCompView = + selectionContainerView.try_as(); + if (!selectionContainerCompView) + return; + + selectionContainerCompView->EnsureUiaProvider(); + + if (!selectionContainerCompView->InnerAutomationProvider()) + return; + auto simpleProvider = static_cast(provider); winrt::com_ptr simpleProviderPtr; simpleProviderPtr.copy_from(simpleProvider); - selectionContainerProvider->RemoveFromSelectionItems(simpleProviderPtr); + selectionContainerCompView->InnerAutomationProvider()->RemoveFromSelectionItems(simpleProviderPtr); } ToggleState GetToggleState(const std::optional &state) noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UnimplementedNativeViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UnimplementedNativeViewComponentView.cpp index 022004e2d69..bd36de24d9b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UnimplementedNativeViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UnimplementedNativeViewComponentView.cpp @@ -8,7 +8,6 @@ #include #include -#include "CompositionDynamicAutomationProvider.h" #include "Unicode.h" namespace winrt::Microsoft::ReactNative::Composition::implementation { diff --git a/vnext/Microsoft.ReactNative/IReactViewComponentBuilder.idl b/vnext/Microsoft.ReactNative/IReactViewComponentBuilder.idl index fa2197a0596..37f430acdb8 100644 --- a/vnext/Microsoft.ReactNative/IReactViewComponentBuilder.idl +++ b/vnext/Microsoft.ReactNative/IReactViewComponentBuilder.idl @@ -54,6 +54,10 @@ namespace Microsoft.ReactNative Boolean Handled; }; + runtimeclass CreateAutomationPeerArgs { + Object DefaultAutomationPeer { get; }; + }; + [experimental] DOC_STRING("A delegate that creates a @IComponentProps object for an instance of @ViewProps. See @IReactViewComponentBuilder.SetCreateProps") delegate IComponentProps ViewPropsFactory(ViewProps props, IComponentProps cloneFrom); @@ -95,6 +99,9 @@ namespace Microsoft.ReactNative [experimental] delegate void UnmountChildComponentViewDelegate(ComponentView source, UnmountChildComponentViewArgs args); + [experimental] + delegate Object CreateAutomationPeerDelegate(ComponentView source, CreateAutomationPeerArgs args); + [experimental] runtimeclass EventEmitter { void DispatchEvent(String eventName, JSValueArgWriter args); @@ -124,6 +131,7 @@ namespace Microsoft.ReactNative void SetUpdateEventEmitterHandler(UpdateEventEmitterDelegate impl); void SetMountChildComponentViewHandler(MountChildComponentViewDelegate impl); void SetUnmountChildComponentViewHandler(UnmountChildComponentViewDelegate impl); + void SetCreateAutomationPeerHandler(CreateAutomationPeerDelegate impl); }; // [exclusiveto(ShadowNode)] From 83c6bac06cf67b822fb97962dc253c0b043b0f5f Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:54:30 -0800 Subject: [PATCH 11/13] fix --- .../windows/RNTesterApp-Fabric/packages.lock.json | 2 +- .../CompositionDynamicAutomationProvider.cpp | 7 +++++++ .../CompositionDynamicAutomationProvider.h | 8 ++++++++ .../CompositionRootAutomationProvider.cpp | 3 ++- .../Composition/ContentIslandComponentView.cpp | 2 -- .../Fabric/Composition/RootComponentView.cpp | 3 ++- .../Fabric/Composition/RootComponentView.h | 2 +- .../components/rnwcore/ActivityIndicatorView.g.h | 14 ++++++++++++++ .../components/rnwcore/AndroidDrawerLayout.g.h | 14 ++++++++++++++ .../rnwcore/AndroidHorizontalScrollContentView.g.h | 14 ++++++++++++++ .../components/rnwcore/AndroidProgressBar.g.h | 14 ++++++++++++++ .../rnwcore/AndroidSwipeRefreshLayout.g.h | 14 ++++++++++++++ .../react/components/rnwcore/AndroidSwitch.g.h | 14 ++++++++++++++ .../react/components/rnwcore/DebuggingOverlay.g.h | 14 ++++++++++++++ .../react/components/rnwcore/InputAccessory.g.h | 14 ++++++++++++++ .../react/components/rnwcore/ModalHostView.g.h | 14 ++++++++++++++ .../react/components/rnwcore/PullToRefreshView.g.h | 14 ++++++++++++++ .../react/components/rnwcore/SafeAreaView.g.h | 14 ++++++++++++++ vnext/codegen/react/components/rnwcore/Switch.g.h | 14 ++++++++++++++ .../components/rnwcore/UnimplementedNativeView.g.h | 14 ++++++++++++++ .../react/components/rnwcore/VirtualView.g.h | 14 ++++++++++++++ 21 files changed, 217 insertions(+), 6 deletions(-) diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json index 078aaa3b9c3..9a384fa5f18 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json @@ -114,7 +114,7 @@ "dependencies": { "Microsoft.ReactNative": "[1.0.0, )", "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", - "Microsoft.WindowsAppSDK": "[1.8.251106002, )", + "Microsoft.WindowsAppSDK": "[1.7.250401001, )", "boost": "[1.83.0, )" } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index 20c2b2820e9..30fbc50895b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -149,6 +149,13 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::SetFocus(void) { return UiaSetFocusHelper(m_view); } +winrt::IUnknown CompositionDynamicAutomationProvider::TryGetChildSiteLinkAutomationProvider() { + if (m_childSiteLink) { + return m_childSiteLink.AutomationProvider().as(); + } + return nullptr; +} + HRESULT __stdcall CompositionDynamicAutomationProvider::get_FragmentRoot(IRawElementProviderFragmentRoot **pRetVal) { if (pRetVal == nullptr) return E_POINTER; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h index 8fc2bcdeb79..7a5b8d686ac 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h @@ -99,6 +99,14 @@ class CompositionDynamicAutomationProvider : public winrt::implements< void RemoveFromSelectionItems(winrt::com_ptr &item); winrt::Microsoft::ReactNative::ComponentView GetSelectionContainer() noexcept; + void SetChildSiteLink(winrt::Microsoft::UI::Content::ChildSiteLink childSiteLink) { + m_childSiteLink = childSiteLink; + } + + // If this object is for a ChildSiteLink, returns the ChildSiteLink's automation provider. + // This will be a provider object from the hosted framework (for example, WinUI). + winrt::IUnknown TryGetChildSiteLinkAutomationProvider(); + private: ::Microsoft::ReactNative::ReactTaggedView m_view; winrt::com_ptr m_textProvider; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp index 6ead642c857..e254eb4c367 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp @@ -219,7 +219,8 @@ HRESULT __stdcall CompositionRootAutomationProvider::ElementProviderFromPoint( auto local = rootView->ConvertScreenToLocal({static_cast(x), static_cast(y)}); auto provider = rootView->UiaProviderFromPoint( {static_cast(local.X * rootView->LayoutMetrics().PointScaleFactor), - static_cast(local.Y * rootView->LayoutMetrics().PointScaleFactor)}); + static_cast(local.Y * rootView->LayoutMetrics().PointScaleFactor)}, + {static_cast(x), static_cast(y)}); auto spFragment = provider.try_as(); if (spFragment) { *pRetVal = spFragment.detach(); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp index 11cb5cc0565..327c0028600 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -198,14 +198,12 @@ ContentIslandComponentView::~ContentIslandComponentView() noexcept { void ContentIslandComponentView::MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { - assert(false); base_type::MountChildComponentView(childComponentView, index); } void ContentIslandComponentView::UnmountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { - assert(false); base_type::UnmountChildComponentView(childComponentView, index); } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp index 0f9c2ba4832..d666530456b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -8,6 +8,7 @@ #include #include +#include "CompositionDynamicAutomationProvider.h" #include "CompositionRootAutomationProvider.h" #include "ContentIslandComponentView.h" #include "ParagraphComponentView.h" @@ -277,7 +278,7 @@ facebook::react::Point RootComponentView::getClientOffset() const noexcept { return {}; } -winrt::IInspectable RootComponentView::UiaProviderFromPoint(const POINT &ptPixels) noexcept { +winrt::IUnknown RootComponentView::UiaProviderFromPoint(const POINT &ptPixels, const POINT &ptScreen) noexcept { facebook::react::Point ptDips{ static_cast(ptPixels.x) / m_layoutMetrics.pointScaleFactor, static_cast(ptPixels.y) / m_layoutMetrics.pointScaleFactor}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h index dc8fe578692..58b6f1ae2d4 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h @@ -65,7 +65,7 @@ struct RootComponentView : RootComponentViewT& EventEmitter() const { return m_eventEmitter; } @@ -185,6 +191,14 @@ void RegisterActivityIndicatorViewNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseActivityIndicatorView::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseActivityIndicatorView::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/AndroidDrawerLayout.g.h b/vnext/codegen/react/components/rnwcore/AndroidDrawerLayout.g.h index 60f6327eaa7..f026661ef9e 100644 --- a/vnext/codegen/react/components/rnwcore/AndroidDrawerLayout.g.h +++ b/vnext/codegen/react/components/rnwcore/AndroidDrawerLayout.g.h @@ -167,6 +167,12 @@ struct BaseAndroidDrawerLayout { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + // You must provide an implementation of this method to handle the "openDrawer" command virtual void HandleOpenDrawerCommand() noexcept = 0; @@ -268,6 +274,14 @@ void RegisterAndroidDrawerLayoutNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseAndroidDrawerLayout::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseAndroidDrawerLayout::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/AndroidHorizontalScrollContentView.g.h b/vnext/codegen/react/components/rnwcore/AndroidHorizontalScrollContentView.g.h index 2162299f622..555f14ed489 100644 --- a/vnext/codegen/react/components/rnwcore/AndroidHorizontalScrollContentView.g.h +++ b/vnext/codegen/react/components/rnwcore/AndroidHorizontalScrollContentView.g.h @@ -98,6 +98,12 @@ struct BaseAndroidHorizontalScrollContentView { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -173,6 +179,14 @@ void RegisterAndroidHorizontalScrollContentViewNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseAndroidHorizontalScrollContentView::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseAndroidHorizontalScrollContentView::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/AndroidProgressBar.g.h b/vnext/codegen/react/components/rnwcore/AndroidProgressBar.g.h index 98c0b14197f..9d4336ad69b 100644 --- a/vnext/codegen/react/components/rnwcore/AndroidProgressBar.g.h +++ b/vnext/codegen/react/components/rnwcore/AndroidProgressBar.g.h @@ -122,6 +122,12 @@ struct BaseAndroidProgressBar { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -197,6 +203,14 @@ void RegisterAndroidProgressBarNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseAndroidProgressBar::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseAndroidProgressBar::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/AndroidSwipeRefreshLayout.g.h b/vnext/codegen/react/components/rnwcore/AndroidSwipeRefreshLayout.g.h index 6d149d8fd67..f80f3cd12e5 100644 --- a/vnext/codegen/react/components/rnwcore/AndroidSwipeRefreshLayout.g.h +++ b/vnext/codegen/react/components/rnwcore/AndroidSwipeRefreshLayout.g.h @@ -130,6 +130,12 @@ struct BaseAndroidSwipeRefreshLayout { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + // You must provide an implementation of this method to handle the "setNativeRefreshing" command virtual void HandleSetNativeRefreshingCommand(bool value) noexcept = 0; @@ -223,6 +229,14 @@ void RegisterAndroidSwipeRefreshLayoutNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseAndroidSwipeRefreshLayout::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseAndroidSwipeRefreshLayout::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/AndroidSwitch.g.h b/vnext/codegen/react/components/rnwcore/AndroidSwitch.g.h index 7aa785222ee..3c773a2054e 100644 --- a/vnext/codegen/react/components/rnwcore/AndroidSwitch.g.h +++ b/vnext/codegen/react/components/rnwcore/AndroidSwitch.g.h @@ -147,6 +147,12 @@ struct BaseAndroidSwitch { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + // You must provide an implementation of this method to handle the "setNativeValue" command virtual void HandleSetNativeValueCommand(bool value) noexcept = 0; @@ -240,6 +246,14 @@ void RegisterAndroidSwitchNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseAndroidSwitch::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseAndroidSwitch::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/DebuggingOverlay.g.h b/vnext/codegen/react/components/rnwcore/DebuggingOverlay.g.h index 23ece9b6f09..3d521856930 100644 --- a/vnext/codegen/react/components/rnwcore/DebuggingOverlay.g.h +++ b/vnext/codegen/react/components/rnwcore/DebuggingOverlay.g.h @@ -95,6 +95,12 @@ struct BaseDebuggingOverlay { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + // You must provide an implementation of this method to handle the "highlightTraceUpdates" command virtual void HandleHighlightTraceUpdatesCommand(std::vector updates) noexcept = 0; @@ -207,6 +213,14 @@ void RegisterDebuggingOverlayNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseDebuggingOverlay::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseDebuggingOverlay::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/InputAccessory.g.h b/vnext/codegen/react/components/rnwcore/InputAccessory.g.h index 2b490f988ef..e5edf7004e0 100644 --- a/vnext/codegen/react/components/rnwcore/InputAccessory.g.h +++ b/vnext/codegen/react/components/rnwcore/InputAccessory.g.h @@ -98,6 +98,12 @@ struct BaseInputAccessory { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -173,6 +179,14 @@ void RegisterInputAccessoryNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseInputAccessory::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseInputAccessory::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/ModalHostView.g.h b/vnext/codegen/react/components/rnwcore/ModalHostView.g.h index 1aa595d258c..02c1c70bc45 100644 --- a/vnext/codegen/react/components/rnwcore/ModalHostView.g.h +++ b/vnext/codegen/react/components/rnwcore/ModalHostView.g.h @@ -189,6 +189,12 @@ struct BaseModalHostView { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -264,6 +270,14 @@ void RegisterModalHostViewNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseModalHostView::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseModalHostView::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/PullToRefreshView.g.h b/vnext/codegen/react/components/rnwcore/PullToRefreshView.g.h index bcb972aae78..313d15406c7 100644 --- a/vnext/codegen/react/components/rnwcore/PullToRefreshView.g.h +++ b/vnext/codegen/react/components/rnwcore/PullToRefreshView.g.h @@ -126,6 +126,12 @@ struct BasePullToRefreshView { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + // You must provide an implementation of this method to handle the "setNativeRefreshing" command virtual void HandleSetNativeRefreshingCommand(bool refreshing) noexcept = 0; @@ -219,6 +225,14 @@ void RegisterPullToRefreshViewNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BasePullToRefreshView::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BasePullToRefreshView::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/SafeAreaView.g.h b/vnext/codegen/react/components/rnwcore/SafeAreaView.g.h index 6f37466ee69..758523eded0 100644 --- a/vnext/codegen/react/components/rnwcore/SafeAreaView.g.h +++ b/vnext/codegen/react/components/rnwcore/SafeAreaView.g.h @@ -95,6 +95,12 @@ struct BaseSafeAreaView { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -170,6 +176,14 @@ void RegisterSafeAreaViewNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseSafeAreaView::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseSafeAreaView::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/Switch.g.h b/vnext/codegen/react/components/rnwcore/Switch.g.h index 7c452873596..a02dbeac1a1 100644 --- a/vnext/codegen/react/components/rnwcore/Switch.g.h +++ b/vnext/codegen/react/components/rnwcore/Switch.g.h @@ -143,6 +143,12 @@ struct BaseSwitch { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + // You must provide an implementation of this method to handle the "setValue" command virtual void HandleSetValueCommand(bool value) noexcept = 0; @@ -236,6 +242,14 @@ void RegisterSwitchNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseSwitch::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseSwitch::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/UnimplementedNativeView.g.h b/vnext/codegen/react/components/rnwcore/UnimplementedNativeView.g.h index 3577f22bc08..e4afa1bfea8 100644 --- a/vnext/codegen/react/components/rnwcore/UnimplementedNativeView.g.h +++ b/vnext/codegen/react/components/rnwcore/UnimplementedNativeView.g.h @@ -98,6 +98,12 @@ struct BaseUnimplementedNativeView { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -173,6 +179,14 @@ void RegisterUnimplementedNativeViewNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseUnimplementedNativeView::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseUnimplementedNativeView::Initialize) { diff --git a/vnext/codegen/react/components/rnwcore/VirtualView.g.h b/vnext/codegen/react/components/rnwcore/VirtualView.g.h index d0c7364971c..b314def2022 100644 --- a/vnext/codegen/react/components/rnwcore/VirtualView.g.h +++ b/vnext/codegen/react/components/rnwcore/VirtualView.g.h @@ -124,6 +124,12 @@ struct BaseVirtualView { winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept { } + // CreateAutomationPeer will only be called if this method is overridden + virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept { + return nullptr; + } + const std::shared_ptr& EventEmitter() const { return m_eventEmitter; } @@ -199,6 +205,14 @@ void RegisterVirtualViewNativeComponent( }); } + if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseVirtualView::CreateAutomationPeer) { + builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept { + auto userData = view.UserData().as(); + return userData->CreateAutomationPeer(view, args); + }); + } + compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { auto userData = winrt::make_self(); if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseVirtualView::Initialize) { From cdb9cc399ca1befd29d7ebac90d50a8fcc91b8b5 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:17:25 -0800 Subject: [PATCH 12/13] fix --- ...-78e59fbb-fe4f-4921-b941-78c82219d869.json | 6 +- ...-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json | 4 +- ...-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json | 4 +- ...-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json | 4 +- ...-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json | 4 +- ...-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json | 2 +- ...-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json | 4 +- ...-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json | 4 +- ...-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json | 4 +- ...-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json | 4 +- ...-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json | 4 +- ...-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json | 4 +- .../automation-commands/src/dumpVisualTree.ts | 65 ++++++++++++++++++- .../e2e-test-app/test/visitAllPages.test.ts | 1 + vnext/fmt/packages.lock.json | 13 ++++ 15 files changed, 100 insertions(+), 27 deletions(-) create mode 100644 vnext/fmt/packages.lock.json diff --git a/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json b/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json index 38ef36233e3..5ec0f7ca9c3 100644 --- a/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json +++ b/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json @@ -1,7 +1,7 @@ { - "type": "prerelease", - "comment": "Update to no longer include paper", + "type": "patch", + "comment": "Add name to accessibility type", "packageName": "@react-native-windows/automation-commands", "email": "30809111+acoates-ms@users.noreply.github.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json b/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json index 0a0681435b3..59206a25e87 100644 --- a/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json +++ b/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "Add ability to customize native accessibility of custom native components", "packageName": "@react-native-windows/codegen", "email": "30809111+acoates-ms@users.noreply.github.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json b/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json index 35ffe563a9d..58f6cda353a 100644 --- a/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json +++ b/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "Add TSF support to TextInput", "packageName": "react-native-windows", "email": "30809111+acoates-ms@users.noreply.github.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json b/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json index ba2817c6e9c..d049d2a1923 100644 --- a/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json +++ b/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "adding accessibility and UIA support for XAML fabric", "packageName": "react-native-windows", "email": "protikbiswas100@microsoft.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json b/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json index 5ef35d7d587..ad992b9e57e 100644 --- a/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json +++ b/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "Fix DPI scaling for debugging overlay highlights", "packageName": "react-native-windows", "email": "74712637+iamAbhi-916@users.noreply.github.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json b/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json index bfa96624efb..fbcbd5f7167 100644 --- a/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json +++ b/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json @@ -1,5 +1,5 @@ { - "type": "prerelease", + "type": "patch", "comment": "Defer UIA accessibility provider initialization until requested", "packageName": "react-native-windows", "email": "198982749+Copilot@users.noreply.github.com", diff --git a/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json b/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json index c59a5341ba5..88785808361 100644 --- a/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json +++ b/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "[Fabric] Fix UIA_LiveSettingPropertyId to use VT_I4 datatype instead of VT_BSTR", "packageName": "react-native-windows", "email": "ankudutt101@gmail.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json b/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json index f93fe3ceb8d..df62838c386 100644 --- a/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json +++ b/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "Fix stackoverflow in StructInfo", "packageName": "react-native-windows", "email": "vmorozov@microsoft.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json b/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json index ffa6b780976..63f1230472f 100644 --- a/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json +++ b/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "Add ability to customize native accessibility of custom native components", "packageName": "react-native-windows", "email": "30809111+acoates-ms@users.noreply.github.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json b/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json index 0b29256a101..159a080ba5e 100644 --- a/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json +++ b/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "Tooltip positioned incorrectly on non 100% scale factor", "packageName": "react-native-windows", "email": "30809111+acoates-ms@users.noreply.github.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json b/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json index b9a7adab163..30267e46d0e 100644 --- a/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json +++ b/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "Pressables should take focus on press", "packageName": "react-native-windows", "email": "30809111+acoates-ms@users.noreply.github.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json b/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json index 60476319eb0..c4fe7ef10b5 100644 --- a/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json +++ b/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json @@ -1,7 +1,7 @@ { - "type": "prerelease", + "type": "patch", "comment": "Implements selectable for ", "packageName": "react-native-windows", "email": "74712637+iamAbhi-916@users.noreply.github.com", "dependentChangeType": "patch" -} +} \ No newline at end of file diff --git a/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts b/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts index 890ac8dbae7..735d3c65a0b 100644 --- a/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts +++ b/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts @@ -80,7 +80,7 @@ export default async function dumpVisualTree( removeGuidsFromImageSources?: boolean; additionalProperties?: string[]; }, -): Promise { +): Promise { if (!automationClient) { throw new Error('RPC client is not enabled'); } @@ -94,9 +94,21 @@ export default async function dumpVisualTree( throw new Error(dumpResponse.message); } - const element: VisualTree = dumpResponse.result; + const element: UIElement | VisualTree = dumpResponse.result; - if (opts?.removeGuidsFromImageSources !== false) { + if ('XamlType' in element && opts?.pruneCollapsed !== false) { + pruneCollapsedElements(element); + } + + if ('XamlType' in element && opts?.deterministicOnly !== false) { + removeNonDeterministicProps(element); + } + + if ('XamlType' in element && opts?.removeDefaultProps !== false) { + removeDefaultProps(element); + } + + if (!('XamlType' in element) && opts?.removeGuidsFromImageSources !== false) { removeGuidsFromImageSources(element); } @@ -172,3 +184,50 @@ function removeGuidsFromImageSourcesHelper(node: ComponentNode) { function removeGuidsFromImageSources(visualTree: VisualTree) { removeGuidsFromImageSourcesHelper(visualTree['Component Tree']); } + +/** + * Removes trees of XAML that are not visible. + */ +function pruneCollapsedElements(element: UIElement) { + if (!element.children) { + return; + } + + element.children = element.children.filter( + child => child.Visibility !== 'Collapsed', + ); + + element.children.forEach(pruneCollapsedElements); +} + +/** + * Removes trees of properties that are not deterministic + */ +function removeNonDeterministicProps(element: UIElement) { + if (element.RenderSize) { + // RenderSize is subject to rounding, etc and should mostly be derived from + // other deterministic properties in the tree. + delete element.RenderSize; + } + + if (element.children) { + element.children.forEach(removeNonDeterministicProps); + } +} + +/** + * Removes noise from snapshot by removing properties with the default value + */ +function removeDefaultProps(element: UIElement) { + const defaultValues: [string, unknown][] = [['Tooltip', null]]; + + defaultValues.forEach(([propname, defaultValue]) => { + if (element[propname] === defaultValue) { + delete element[propname]; + } + }); + + if (element.children) { + element.children.forEach(removeDefaultProps); + } +} diff --git a/packages/e2e-test-app/test/visitAllPages.test.ts b/packages/e2e-test-app/test/visitAllPages.test.ts index 8e92597bfe2..baa4787e434 100644 --- a/packages/e2e-test-app/test/visitAllPages.test.ts +++ b/packages/e2e-test-app/test/visitAllPages.test.ts @@ -35,6 +35,7 @@ const componentExamples = testerList.Components.map(e => e.module.title); describe('visitAllPages', () => { for (const component of componentExamples) { if ( + component === 'Custom Native Accessibility Example' || component === 'Moving Light Example' || component === 'Drawing Island Example' || component === 'Fabric Native Component' || diff --git a/vnext/fmt/packages.lock.json b/vnext/fmt/packages.lock.json new file mode 100644 index 00000000000..a31237b580e --- /dev/null +++ b/vnext/fmt/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "native,Version=v0.0": {}, + "native,Version=v0.0/win10-arm": {}, + "native,Version=v0.0/win10-arm-aot": {}, + "native,Version=v0.0/win10-arm64-aot": {}, + "native,Version=v0.0/win10-x64": {}, + "native,Version=v0.0/win10-x64-aot": {}, + "native,Version=v0.0/win10-x86": {}, + "native,Version=v0.0/win10-x86-aot": {} + } +} \ No newline at end of file From 9d10b23e4cf6e7a78b939b34e0cd9ecaed1b1253 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:22:54 -0800 Subject: [PATCH 13/13] Update snapshot --- .../test/__snapshots__/HomeUIADump.test.ts.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap index a71b4f9f784..d637dcadfd6 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap @@ -6560,12 +6560,12 @@ exports[`Home UIA Tree Dump XAML 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "47, 24", + "Size": "47, 25", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "47, 24", + "Size": "47, 25", "Visual Type": "SpriteVisual", }, ],