From b3141a4d0b3ebcbf617e1863ff848c828dec7010 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:19:03 -0800 Subject: [PATCH 1/9] Add ability to customize native accessibility of custom native components --- .../generators/GenerateComponentWindows.ts | 155 +++++++------ .../CustomAccessibility.windows.js | 33 +++ .../src/js/utils/RNTesterList.windows.js | 4 + .../src/CustomAccessibilityNativeComponent.ts | 7 + packages/sample-custom-component/src/index.ts | 5 +- .../CustomAccessibility.cpp | 129 +++++++++++ .../CustomAccessibility.h | 7 + .../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 | 17 +- .../Composition/ContentIslandComponentView.h | 2 +- .../Fabric/Composition/ImageComponentView.cpp | 1 - .../Composition/ParagraphComponentView.cpp | 1 - .../ReactCompositionViewComponentBuilder.cpp | 8 + .../ReactCompositionViewComponentBuilder.h | 3 + .../Fabric/Composition/RootComponentView.cpp | 53 ++--- .../Composition/ScrollViewComponentView.cpp | 1 - .../Composition/SwitchComponentView.cpp | 1 - .../WindowsTextInputComponentView.cpp | 1 - .../Fabric/Composition/UiaHelpers.cpp | 38 +++- .../UnimplementedNativeViewComponentView.cpp | 1 - .../IReactViewComponentBuilder.idl | 9 + 39 files changed, 785 insertions(+), 248 deletions(-) create mode 100644 packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js 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/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts index 9a449abde03..7eccea1eaa9 100644 --- a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts +++ b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts @@ -13,16 +13,16 @@ import type { ObjectTypeAnnotation, CommandParamTypeAnnotation, } from '@react-native/codegen/lib/CodegenSchema'; -import {getAliasCppName, setPreferredModuleName} from './AliasManaging'; +import { getAliasCppName, setPreferredModuleName } from './AliasManaging'; import { translateComponentPropsFieldType, translateComponentEventType, translateCommandParamType, } from './PropObjectTypes'; -import type {CppStringTypes} from './ObjectTypes'; -import type {AliasMap} from './AliasManaging'; +import type { CppStringTypes } from './ObjectTypes'; +import type { AliasMap } from './AliasManaging'; -export type {CppStringTypes} from './ObjectTypes'; +export type { CppStringTypes } from './ObjectTypes'; type FilesOutput = Map; @@ -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) { @@ -283,7 +297,7 @@ export function createComponentGenerator({ ): FilesOutput => { const files = new Map(); - const cppCodegenOptions = {cppStringType}; + const cppCodegenOptions = { cppStringType }; for (const componentName of Object.keys(schema.modules)) { const component = schema.modules[componentName]; @@ -310,7 +324,7 @@ export function createComponentGenerator({ // Props const propObjectAliases: AliasMap< ObjectTypeAnnotation - > = {types: {}, jobs: []}; + > = { types: {}, jobs: [] }; const propsName = `${componentName}Props`; const propsFields = componentShape.props .map(prop => { @@ -320,11 +334,10 @@ export function createComponentGenerator({ `${propsName}_${prop.name}`, cppCodegenOptions, ); - return ` REACT_FIELD(${prop.name})\n ${ - prop.optional && !propType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${propType.type}>` - : propType.type - } ${prop.name}${propType.initializer};\n`; + return ` REACT_FIELD(${prop.name})\n ${prop.optional && !propType.alreadySupportsOptionalOrHasDefault + ? `std::optional<${propType.type}>` + : propType.type + } ${prop.name}${propType.initializer};\n`; }) .join('\n'); @@ -345,12 +358,11 @@ export function createComponentGenerator({ `${propsName}_${property.name}`, cppCodegenOptions, ); - return ` REACT_FIELD(${property.name})\n ${ - property.optional && + return ` REACT_FIELD(${property.name})\n ${property.optional && !propType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${propType.type}>` - : propType.type - } ${property.name}${propType.initializer};\n`; + ? `std::optional<${propType.type}>` + : propType.type + } ${property.name}${propType.initializer};\n`; }) .join('\n'); @@ -366,7 +378,7 @@ export function createComponentGenerator({ // Events const eventObjectAliases: AliasMap< ObjectTypeAnnotation - > = {types: {}, jobs: []}; + > = { types: {}, jobs: [] }; const eventEmitterName = `${componentName}EventEmitter`; const eventEmitterMethods = componentShape.events .filter(event => event.typeAnnotation.argument) @@ -412,12 +424,11 @@ export function createComponentGenerator({ eventObjectTypeName, cppCodegenOptions, ); - return ` REACT_FIELD(${property.name})\n ${ - property.optional && + return ` REACT_FIELD(${property.name})\n ${property.optional && !eventPropType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${eventPropType.type}>` - : eventPropType.type - } ${property.name}${eventPropType.initializer};\n`; + ? `std::optional<${eventPropType.type}>` + : eventPropType.type + } ${property.name}${eventPropType.initializer};\n`; }) .join('\n'); return eventsObjectTemplate @@ -446,36 +457,34 @@ export function createComponentGenerator({ // Commands const commandAliases: AliasMap< ObjectTypeAnnotation - > = {types: {}, jobs: []}; + > = { types: {}, jobs: [] }; const hasAnyCommands = componentShape.commands.length !== 0; const commandHandlers = hasAnyCommands ? componentShape.commands - .map(command => { - const commandArgs = command.typeAnnotation.params - .map(param => { - const commandArgType = translateCommandParamType( - param.typeAnnotation, - commandAliases, - `${componentName}_${command.name}`, - cppCodegenOptions, - ); - return `${ - param.optional && - !commandArgType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${commandArgType.type}>` - : commandArgType.type + .map(command => { + const commandArgs = command.typeAnnotation.params + .map(param => { + const commandArgType = translateCommandParamType( + param.typeAnnotation, + commandAliases, + `${componentName}_${command.name}`, + cppCodegenOptions, + ); + return `${param.optional && + !commandArgType.alreadySupportsOptionalOrHasDefault + ? `std::optional<${commandArgType.type}>` + : commandArgType.type } ${param.name}`; - }) - .join(', '); + }) + .join(', '); - return ` // You must provide an implementation of this method to handle the "${ - command.name + return ` // You must provide an implementation of this method to handle the "${command.name }" command virtual void Handle${capitalizeFirstLetter( - command.name, - )}Command(${commandArgs}) noexcept = 0;`; - }) - .join('\n\n') + command.name, + )}Command(${commandArgs}) noexcept = 0;`; + }) + .join('\n\n') : ''; const commandHandler = hasAnyCommands @@ -483,39 +492,37 @@ export function createComponentGenerator({ auto userData = view.UserData().as(); auto commandName = args.CommandName(); ${componentShape.commands - .map(command => { - const commaSeparatedCommandArgs = command.typeAnnotation.params - .map(param => param.name) - .join(', '); - return ` if (commandName == L"${command.name}") { -${ - command.typeAnnotation.params.length !== 0 - ? ` ${command.typeAnnotation.params - .map(param => { - const commandArgType = translateCommandParamType( - param.typeAnnotation, - commandAliases, - `${componentName}_${command.name}`, - cppCodegenOptions, - ); - return `${ - param.optional && - !commandArgType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${commandArgType.type}>` - : commandArgType.type - } ${param.name};`; - }) - .join('\n')} + .map(command => { + const commaSeparatedCommandArgs = command.typeAnnotation.params + .map(param => param.name) + .join(', '); + return ` if (commandName == L"${command.name}") { +${command.typeAnnotation.params.length !== 0 + ? ` ${command.typeAnnotation.params + .map(param => { + const commandArgType = translateCommandParamType( + param.typeAnnotation, + commandAliases, + `${componentName}_${command.name}`, + cppCodegenOptions, + ); + return `${param.optional && + !commandArgType.alreadySupportsOptionalOrHasDefault + ? `std::optional<${commandArgType.type}>` + : commandArgType.type + } ${param.name};`; + }) + .join('\n')} winrt::Microsoft::ReactNative::ReadArgs(args.CommandArgs(), ${commaSeparatedCommandArgs});` - : '' -} + : '' + } userData->Handle${capitalizeFirstLetter( - command.name, - )}Command(${commaSeparatedCommandArgs}); + command.name, + )}Command(${commaSeparatedCommandArgs}); return; }`; - }) - .join('\n\n')} + }) + .join('\n\n')} }` : ''; 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..bde53ba7e3f --- /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 e0022d8ca5d..6095f578d71 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 @@ -79,6 +79,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/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..b360ed163ea --- /dev/null +++ b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp @@ -0,0 +1,129 @@ +// 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(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..87bc0d7e78e --- /dev/null +++ b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h @@ -0,0 +1,7 @@ +#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 7f679b2b4a6..2a9dae1e5fe 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj +++ b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj @@ -101,6 +101,7 @@ + DrawingIsland.idl @@ -113,6 +114,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..639f37ebd53 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::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 186cf014ed7..30fbc50895b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -294,8 +294,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); } @@ -304,8 +304,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); } @@ -314,8 +314,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); } @@ -959,9 +959,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)); } @@ -1020,27 +1029,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) { @@ -1051,7 +1061,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 5049a52c4b3..7a5b8d686ac 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; void SetChildSiteLink(winrt::Microsoft::UI::Content::ChildSiteLink childSiteLink) { m_childSiteLink = childSiteLink; 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 00f5ea73b21..5f50d14da18 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -855,14 +855,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()); } } } @@ -1352,12 +1351,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 8e71d242d13..41215fea0e2 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -110,12 +110,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 { @@ -281,10 +280,8 @@ void ContentIslandComponentView::ConfigureChildSiteLinkAutomation() noexcept { args.Handled(true); }); - if (m_uiaProvider) { - auto providerImpl = - m_uiaProvider.as(); - providerImpl->SetChildSiteLink(m_childSiteLink); + if (m_innerAutomationProvider) { + m_innerAutomationProvider->SetChildSiteLink(m_childSiteLink); } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h index 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 52c262423b5..c9f250f1e06 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; +} + bool ReactCompositionViewComponentBuilder::XamlSupport() const noexcept { return m_xamlSupport; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h index fa5ec618baf..c015b33a5fb 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; bool XamlSupport() const noexcept; void XamlSupport(bool isRequired) noexcept; @@ -79,6 +80,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; @@ -105,6 +107,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 c0d7903d4af..d666530456b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -10,6 +10,7 @@ #include #include "CompositionDynamicAutomationProvider.h" #include "CompositionRootAutomationProvider.h" +#include "ContentIslandComponentView.h" #include "ParagraphComponentView.h" #include "ReactNativeIsland.h" #include "Theme.h" @@ -300,32 +301,32 @@ winrt::IUnknown RootComponentView::UiaProviderFromPoint(const POINT &ptPixels, c auto uiaProvider = winrt::get_self(view)->EnsureUiaProvider(); - // TODO: Avoid exposing CompositionDynamicAutomationProvider in RootComponentView - auto dynamicProvider = - uiaProvider.try_as(); - if (dynamicProvider) { - if (auto childProvider = dynamicProvider->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(); + 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(); + } } } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index fb0916cd78d..ca5b83decdc 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -19,7 +19,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 25fc9437cf4..425f146a140 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 2de775887a3..6cbb13c4443 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -517,27 +517,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 2b60aa7e6cd..369f771ae38 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,8 @@ namespace Microsoft.ReactNative void SetUpdateEventEmitterHandler(UpdateEventEmitterDelegate impl); void SetMountChildComponentViewHandler(MountChildComponentViewDelegate impl); void SetUnmountChildComponentViewHandler(UnmountChildComponentViewDelegate impl); + void SetCreateAutomationPeerHandler(CreateAutomationPeerDelegate impl); + Boolean XamlSupport { get; set; }; }; From 91abecaee6e135a33259e0997f31917a502fa059 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:19:36 -0800 Subject: [PATCH 2/9] format --- .../CustomAccessibility.cpp | 36 ++++++++----------- .../CustomAccessibility.h | 3 +- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp index b360ed163ea..3c8d96c1773 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp +++ b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp @@ -5,36 +5,30 @@ #include "codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h" #ifdef RNW_NEW_ARCH +#include +#include #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()) {} -struct CustomAccessibilityAutomationPeer : public winrt::implements -{ - - CustomAccessibilityAutomationPeer(const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) - : m_inner(args.DefaultAutomationPeer()) - { - } - - virtual HRESULT __stdcall Navigate(NavigateDirection direction, IRawElementProviderFragment **pRetVal) override - { + 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 - { + virtual HRESULT __stdcall GetRuntimeId(SAFEARRAY **pRetVal) override { winrt::com_ptr innerAsREPF = m_inner.try_as(); if (!innerAsREPF) return E_FAIL; @@ -105,15 +99,15 @@ struct CustomAccessibilityAutomationPeer : public winrt::implementsget_HostRawElementProvider(pRetVal); } -private: + private: winrt::Windows::Foundation::IInspectable m_inner; }; -struct CustomAccessibility : public winrt::implements, Codegen::BaseCustomAccessibility { - +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 { + const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs &args) noexcept override { return winrt::make(args); } }; diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h index 87bc0d7e78e..99c15405141 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h @@ -2,6 +2,7 @@ #if defined(RNW_NEW_ARCH) -void RegisterCustomAccessibilityComponentView(winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder); +void RegisterCustomAccessibilityComponentView( + winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder); #endif // defined(RNW_NEW_ARCH) From fc307e34bdfe39e57bbf0fe8efefcbb62310fdf6 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:19:44 -0800 Subject: [PATCH 3/9] Change files --- ...ndows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json | 7 +++++++ ...ative-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json | 7 +++++++ 2 files changed, 14 insertions(+) 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 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" +} From bec33ad48c8bc5dd125403a780b00d46ee2c6920 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:20:50 -0800 Subject: [PATCH 4/9] fix --- .../generators/GenerateComponentWindows.ts | 141 +++++++++--------- .../CustomAccessibility.windows.js | 4 +- .../test/CustomAccessibilityTest.test.ts | 31 ++++ .../Fabric/ComponentView.h | 2 +- 4 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts diff --git a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts index 7eccea1eaa9..b08d0e51f16 100644 --- a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts +++ b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts @@ -13,16 +13,16 @@ import type { ObjectTypeAnnotation, CommandParamTypeAnnotation, } from '@react-native/codegen/lib/CodegenSchema'; -import { getAliasCppName, setPreferredModuleName } from './AliasManaging'; +import {getAliasCppName, setPreferredModuleName} from './AliasManaging'; import { translateComponentPropsFieldType, translateComponentEventType, translateCommandParamType, } from './PropObjectTypes'; -import type { CppStringTypes } from './ObjectTypes'; -import type { AliasMap } from './AliasManaging'; +import type {CppStringTypes} from './ObjectTypes'; +import type {AliasMap} from './AliasManaging'; -export type { CppStringTypes } from './ObjectTypes'; +export type {CppStringTypes} from './ObjectTypes'; type FilesOutput = Map; @@ -297,7 +297,7 @@ export function createComponentGenerator({ ): FilesOutput => { const files = new Map(); - const cppCodegenOptions = { cppStringType }; + const cppCodegenOptions = {cppStringType}; for (const componentName of Object.keys(schema.modules)) { const component = schema.modules[componentName]; @@ -324,7 +324,7 @@ export function createComponentGenerator({ // Props const propObjectAliases: AliasMap< ObjectTypeAnnotation - > = { types: {}, jobs: [] }; + > = {types: {}, jobs: []}; const propsName = `${componentName}Props`; const propsFields = componentShape.props .map(prop => { @@ -334,10 +334,11 @@ export function createComponentGenerator({ `${propsName}_${prop.name}`, cppCodegenOptions, ); - return ` REACT_FIELD(${prop.name})\n ${prop.optional && !propType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${propType.type}>` - : propType.type - } ${prop.name}${propType.initializer};\n`; + return ` REACT_FIELD(${prop.name})\n ${ + prop.optional && !propType.alreadySupportsOptionalOrHasDefault + ? `std::optional<${propType.type}>` + : propType.type + } ${prop.name}${propType.initializer};\n`; }) .join('\n'); @@ -358,11 +359,12 @@ export function createComponentGenerator({ `${propsName}_${property.name}`, cppCodegenOptions, ); - return ` REACT_FIELD(${property.name})\n ${property.optional && + return ` REACT_FIELD(${property.name})\n ${ + property.optional && !propType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${propType.type}>` - : propType.type - } ${property.name}${propType.initializer};\n`; + ? `std::optional<${propType.type}>` + : propType.type + } ${property.name}${propType.initializer};\n`; }) .join('\n'); @@ -378,7 +380,7 @@ export function createComponentGenerator({ // Events const eventObjectAliases: AliasMap< ObjectTypeAnnotation - > = { types: {}, jobs: [] }; + > = {types: {}, jobs: []}; const eventEmitterName = `${componentName}EventEmitter`; const eventEmitterMethods = componentShape.events .filter(event => event.typeAnnotation.argument) @@ -424,11 +426,12 @@ export function createComponentGenerator({ eventObjectTypeName, cppCodegenOptions, ); - return ` REACT_FIELD(${property.name})\n ${property.optional && + return ` REACT_FIELD(${property.name})\n ${ + property.optional && !eventPropType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${eventPropType.type}>` - : eventPropType.type - } ${property.name}${eventPropType.initializer};\n`; + ? `std::optional<${eventPropType.type}>` + : eventPropType.type + } ${property.name}${eventPropType.initializer};\n`; }) .join('\n'); return eventsObjectTemplate @@ -457,34 +460,36 @@ export function createComponentGenerator({ // Commands const commandAliases: AliasMap< ObjectTypeAnnotation - > = { types: {}, jobs: [] }; + > = {types: {}, jobs: []}; const hasAnyCommands = componentShape.commands.length !== 0; const commandHandlers = hasAnyCommands ? componentShape.commands - .map(command => { - const commandArgs = command.typeAnnotation.params - .map(param => { - const commandArgType = translateCommandParamType( - param.typeAnnotation, - commandAliases, - `${componentName}_${command.name}`, - cppCodegenOptions, - ); - return `${param.optional && - !commandArgType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${commandArgType.type}>` - : commandArgType.type + .map(command => { + const commandArgs = command.typeAnnotation.params + .map(param => { + const commandArgType = translateCommandParamType( + param.typeAnnotation, + commandAliases, + `${componentName}_${command.name}`, + cppCodegenOptions, + ); + return `${ + param.optional && + !commandArgType.alreadySupportsOptionalOrHasDefault + ? `std::optional<${commandArgType.type}>` + : commandArgType.type } ${param.name}`; - }) - .join(', '); + }) + .join(', '); - return ` // You must provide an implementation of this method to handle the "${command.name + return ` // You must provide an implementation of this method to handle the "${ + command.name }" command virtual void Handle${capitalizeFirstLetter( - command.name, - )}Command(${commandArgs}) noexcept = 0;`; - }) - .join('\n\n') + command.name, + )}Command(${commandArgs}) noexcept = 0;`; + }) + .join('\n\n') : ''; const commandHandler = hasAnyCommands @@ -492,37 +497,39 @@ export function createComponentGenerator({ auto userData = view.UserData().as(); auto commandName = args.CommandName(); ${componentShape.commands - .map(command => { - const commaSeparatedCommandArgs = command.typeAnnotation.params - .map(param => param.name) - .join(', '); - return ` if (commandName == L"${command.name}") { -${command.typeAnnotation.params.length !== 0 - ? ` ${command.typeAnnotation.params - .map(param => { - const commandArgType = translateCommandParamType( - param.typeAnnotation, - commandAliases, - `${componentName}_${command.name}`, - cppCodegenOptions, - ); - return `${param.optional && - !commandArgType.alreadySupportsOptionalOrHasDefault - ? `std::optional<${commandArgType.type}>` - : commandArgType.type - } ${param.name};`; - }) - .join('\n')} + .map(command => { + const commaSeparatedCommandArgs = command.typeAnnotation.params + .map(param => param.name) + .join(', '); + return ` if (commandName == L"${command.name}") { +${ + command.typeAnnotation.params.length !== 0 + ? ` ${command.typeAnnotation.params + .map(param => { + const commandArgType = translateCommandParamType( + param.typeAnnotation, + commandAliases, + `${componentName}_${command.name}`, + cppCodegenOptions, + ); + return `${ + param.optional && + !commandArgType.alreadySupportsOptionalOrHasDefault + ? `std::optional<${commandArgType.type}>` + : commandArgType.type + } ${param.name};`; + }) + .join('\n')} winrt::Microsoft::ReactNative::ReadArgs(args.CommandArgs(), ${commaSeparatedCommandArgs});` - : '' - } + : '' +} userData->Handle${capitalizeFirstLetter( - command.name, - )}Command(${commaSeparatedCommandArgs}); + command.name, + )}Command(${commaSeparatedCommandArgs}); return; }`; - }) - .join('\n\n')} + }) + .join('\n\n')} }` : ''; 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 index bde53ba7e3f..770f452965b 100644 --- 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 @@ -7,9 +7,9 @@ import RNTesterText from '../../components/RNTesterText'; const CustomAccessibilityExample = () => { return ( - + The below view should have custom accessibility - + ); } 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..86714682113 --- /dev/null +++ b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import {dumpVisualTree} from '@react-native-windows/automation-commands'; +import {goToApiExample} 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 goToApiExample('CustomAccessibilityExample'); +}); + +afterEach(async () => { + await verifyNoErrorLogs(); +}); + +describe('Custom Accessibility Tests', () => { + test('Verify custom native component has UIA label from native', async () => { + //const root = await app.findElementByTestID('custom-accessibility-root-1'); + const dump = await dumpVisualTree('custom-accessibility-root-1'); + + expect(dump).toMatchSnapshot(); + }); +}); diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h index 639f37ebd53..7700ac9e3fa 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h @@ -266,7 +266,7 @@ struct ComponentView facebook::react::LayoutMetrics m_layoutMetrics; winrt::Windows::Foundation::Collections::IVector m_children{ winrt::single_threaded_vector()}; - winrt::IInspectable m_uiaProvider{nullptr}; + winrt::Windows::Foundation::IInspectable m_uiaProvider{nullptr}; winrt::event< winrt::Windows::Foundation::EventHandler> From cf704936189ad2680d69506c36223fe6ce8e6f01 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:13:03 -0800 Subject: [PATCH 5/9] fix --- .../e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts index 86714682113..242c9acf6bc 100644 --- a/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts +++ b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts @@ -14,7 +14,7 @@ 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 goToApiExample('CustomAccessibilityExample'); + await goToApiExample('Custom Native Accessibility Example'); }); afterEach(async () => { @@ -25,7 +25,6 @@ describe('Custom Accessibility Tests', () => { test('Verify custom native component has UIA label from native', async () => { //const root = await app.findElementByTestID('custom-accessibility-root-1'); const dump = await dumpVisualTree('custom-accessibility-root-1'); - expect(dump).toMatchSnapshot(); }); }); From 4f8a01bb7160e6029f2c8dcc0c99425137259a4a Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:52:50 -0800 Subject: [PATCH 6/9] Update test --- .../automation-commands/src/dumpVisualTree.ts | 66 +-------------- .../CustomAccessibility.windows.js | 2 +- .../test/CustomAccessibilityTest.test.ts | 12 ++- .../CustomAccessibilityTest.test.ts.snap | 84 +++++++++++++++++++ .../windows/RNTesterApp-Fabric.sln | 18 ++++ .../RNTesterApp-Fabric/RNTesterApp-Fabric.cpp | 4 + .../RNTesterApp-Fabric.vcxproj | 5 ++ .../RNTesterApp-Fabric/packages.lock.json | 9 ++ 8 files changed, 134 insertions(+), 66 deletions(-) create mode 100644 packages/e2e-test-app-fabric/test/__snapshots__/CustomAccessibilityTest.test.ts.snap 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/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js b/packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js index 770f452965b..591861b8fa6 100644 --- 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 @@ -7,7 +7,7 @@ import RNTesterText from '../../components/RNTesterText'; const CustomAccessibilityExample = () => { return ( - + The below view should have custom accessibility diff --git a/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts index 242c9acf6bc..31010001cae 100644 --- a/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts +++ b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts @@ -6,7 +6,7 @@ */ import {dumpVisualTree} from '@react-native-windows/automation-commands'; -import {goToApiExample} from './RNTesterNavigation'; +import {goToComponentExample} from './RNTesterNavigation'; import {app} from '@react-native-windows/automation'; import {verifyNoErrorLogs} from './Helpers'; @@ -14,7 +14,7 @@ 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 goToApiExample('Custom Native Accessibility Example'); + await goToComponentExample('Custom Native Accessibility Example'); }); afterEach(async () => { @@ -23,7 +23,13 @@ afterEach(async () => { describe('Custom Accessibility Tests', () => { test('Verify custom native component has UIA label from native', async () => { - //const root = await app.findElementByTestID('custom-accessibility-root-1'); + 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/windows/RNTesterApp-Fabric.sln b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.sln index 67e1e8fc238..d6d1107a020 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.sln +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.sln @@ -35,6 +35,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Include", "..\..\..\vnext\i EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AutomationChannel", "..\..\..\node_modules\@react-native-windows\automation-channel\windows\AutomationChannel\AutomationChannel.vcxproj", "{C0A69310-6119-46DC-A6D6-0BAB7826DC92}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SampleCustomComponent", "..\..\sample-custom-component\windows\SampleCustomComponent\SampleCustomComponent.vcxproj", "{A8DA218C-4CB5-48CB-A9EE-9E6337165D07}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\..\..\vnext\Shared\Shared.vcxitems*{2049dbe9-8d13-42c9-ae4b-413ae38fffd0}*SharedItemsImports = 9 @@ -164,6 +166,22 @@ Global {C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x86.Build.0 = Release|Win32 {C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|ARM64.ActiveCfg = Release|ARM64 {C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|ARM64.Build.0 = Release|ARM64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|ARM64.Build.0 = Debug|ARM64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x64.ActiveCfg = Debug|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x64.Build.0 = Debug|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x64.Deploy.0 = Debug|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x86.ActiveCfg = Debug|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x86.Build.0 = Debug|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Debug|x86.Deploy.0 = Debug|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|ARM64.ActiveCfg = Release|ARM64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|ARM64.Build.0 = Release|ARM64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x64.ActiveCfg = Release|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x64.Build.0 = Release|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x64.Deploy.0 = Release|x64 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x86.ActiveCfg = Release|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x86.Build.0 = Release|Win32 + {A8DA218C-4CB5-48CB-A9EE-9E6337165D07}.Release|x86.Deploy.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp index 4c9127ccb69..0ded5f28259 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp @@ -8,6 +8,9 @@ #include #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 1058e9206aa..7cab1c4a590 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 @@ -199,6 +199,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": { From 835ab9352e0a372d456056e694c57d4cb436135e Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:53:14 -0800 Subject: [PATCH 7/9] Change files --- ...tion-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.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 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" +} From c35b52690ca894c79461893382e675fc1071b220 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:35:34 -0800 Subject: [PATCH 8/9] pacakgelock --- .../RNTesterApp-Fabric.Package/packages.lock.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.Package/packages.lock.json b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.Package/packages.lock.json index fbcc7632c30..c5f54d21d19 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.Package/packages.lock.json +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.Package/packages.lock.json @@ -195,6 +195,16 @@ "dependencies": { "AutomationChannel": "[1.0.0, )", "Microsoft.JavaScript.Hermes": "[0.0.0-2512.22001-bc3d0ed7, )", + "Microsoft.ReactNative": "[1.0.0, )", + "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", + "Microsoft.WindowsAppSDK": "[1.8.251106002, )", + "SampleCustomComponent": "[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, )", From e617b341bd3c4aa2f6fcb9a018e5a364b937a85a Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:38:09 -0800 Subject: [PATCH 9/9] snapshots --- .../__snapshots__/HomeUIADump.test.ts.snap | 125 +++++++++++++++--- .../__snapshots__/snapshotPages.test.js.snap | 33 +++++ 2 files changed, 136 insertions(+), 22 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 c14f739a40c..84585152942 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", }, ], @@ -6479,12 +6560,12 @@ exports[`Home UIA Tree Dump XMLHttpRequest 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "135, 24", + "Size": "135, 25", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "135, 24", + "Size": "135, 25", "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 e35f435b3d9..10585f227af 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`] = `