Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type AutomationNode = {
AutomationId?: string;
ControlType?: number;
LocalizedControlType?: string;
Name?: string;
__Children?: [AutomationNode];
};

Expand Down Expand Up @@ -79,7 +80,7 @@ export default async function dumpVisualTree(
removeGuidsFromImageSources?: boolean;
additionalProperties?: string[];
},
): Promise<UIElement | VisualTree> {
): Promise<VisualTree> {
if (!automationClient) {
throw new Error('RPC client is not enabled');
}
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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_::
Expand Down Expand Up @@ -222,6 +228,14 @@ void Register::_COMPONENT_NAME_::NativeComponent(
});
}

if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &Base::_COMPONENT_NAME_::<TUserData>::CreateAutomationPeer) {
builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept {
auto userData = view.UserData().as<TUserData>();
return userData->CreateAutomationPeer(view, args);
});
}

compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
auto userData = winrt::make_self<TUserData>();
if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &Base::_COMPONENT_NAME_::<TUserData>::Initialize) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<View testID="custom-accessibility-root-1" accessible accessibilityLabel='example root'>
<RNTesterText>The below view should have custom accessibility</RNTesterText>
<CustomAccessibility style={{width: 500, height: 500, backgroundColor:'green'}} accessible accessibilityLabel='accessibility should not show this, as native overrides it' testID="custom-accessibility-1"/>
</View>
);
}

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 (
<CustomAccessibilityExample />
);
},
}
];
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ const Components: Array<RNTesterModuleInfo> = [
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'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*
* @format
*/

import {dumpVisualTree} from '@react-native-windows/automation-commands';
import {goToComponentExample} from './RNTesterNavigation';
import {app} from '@react-native-windows/automation';
import {verifyNoErrorLogs} from './Helpers';

beforeAll(async () => {
// If window is partially offscreen, tests will fail to click on certain elements
await app.setWindowPosition(0, 0);
await app.setWindowSize(1000, 1250);
await goToComponentExample('Custom Native Accessibility Example');
});

afterEach(async () => {
await verifyNoErrorLogs();
});

describe('Custom Accessibility Tests', () => {
test('Verify custom native component has UIA label from native', async () => {
const nativeComponent = await dumpVisualTree('custom-accessibility-1');

// Verify that the native component reports its accessiblity label from the native code
expect(nativeComponent['Automation Tree'].Name).toBe(
'accessiblity label from native',
);

const dump = await dumpVisualTree('custom-accessibility-root-1');
expect(dump).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -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",
},
],
},
],
},
}
`;
Loading
Loading