Skip to content

Commit 190a33d

Browse files
authored
Add ability to customize native accessibility of custom native components (#15532)
* Add ability to customize native accessibility of custom native components * format * Change files * fix * fix * Update test * Change files * pacakgelock * snapshots
1 parent 7ed51a3 commit 190a33d

File tree

52 files changed

+1040
-258
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1040
-258
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Update to no longer include paper",
4+
"packageName": "@react-native-windows/automation-commands",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Add ability to customize native accessibility of custom native components",
4+
"packageName": "@react-native-windows/codegen",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Add ability to customize native accessibility of custom native components",
4+
"packageName": "react-native-windows",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts

Lines changed: 4 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type AutomationNode = {
3737
AutomationId?: string;
3838
ControlType?: number;
3939
LocalizedControlType?: string;
40+
Name?: string;
4041
__Children?: [AutomationNode];
4142
};
4243

@@ -79,7 +80,7 @@ export default async function dumpVisualTree(
7980
removeGuidsFromImageSources?: boolean;
8081
additionalProperties?: string[];
8182
},
82-
): Promise<UIElement | VisualTree> {
83+
): Promise<VisualTree> {
8384
if (!automationClient) {
8485
throw new Error('RPC client is not enabled');
8586
}
@@ -93,21 +94,9 @@ export default async function dumpVisualTree(
9394
throw new Error(dumpResponse.message);
9495
}
9596

96-
const element: UIElement | VisualTree = dumpResponse.result;
97+
const element: VisualTree = dumpResponse.result;
9798

98-
if ('XamlType' in element && opts?.pruneCollapsed !== false) {
99-
pruneCollapsedElements(element);
100-
}
101-
102-
if ('XamlType' in element && opts?.deterministicOnly !== false) {
103-
removeNonDeterministicProps(element);
104-
}
105-
106-
if ('XamlType' in element && opts?.removeDefaultProps !== false) {
107-
removeDefaultProps(element);
108-
}
109-
110-
if (!('XamlType' in element) && opts?.removeGuidsFromImageSources !== false) {
99+
if (opts?.removeGuidsFromImageSources !== false) {
111100
removeGuidsFromImageSources(element);
112101
}
113102

@@ -183,50 +172,3 @@ function removeGuidsFromImageSourcesHelper(node: ComponentNode) {
183172
function removeGuidsFromImageSources(visualTree: VisualTree) {
184173
removeGuidsFromImageSourcesHelper(visualTree['Component Tree']);
185174
}
186-
187-
/**
188-
* Removes trees of XAML that are not visible.
189-
*/
190-
function pruneCollapsedElements(element: UIElement) {
191-
if (!element.children) {
192-
return;
193-
}
194-
195-
element.children = element.children.filter(
196-
child => child.Visibility !== 'Collapsed',
197-
);
198-
199-
element.children.forEach(pruneCollapsedElements);
200-
}
201-
202-
/**
203-
* Removes trees of properties that are not deterministic
204-
*/
205-
function removeNonDeterministicProps(element: UIElement) {
206-
if (element.RenderSize) {
207-
// RenderSize is subject to rounding, etc and should mostly be derived from
208-
// other deterministic properties in the tree.
209-
delete element.RenderSize;
210-
}
211-
212-
if (element.children) {
213-
element.children.forEach(removeNonDeterministicProps);
214-
}
215-
}
216-
217-
/**
218-
* Removes noise from snapshot by removing properties with the default value
219-
*/
220-
function removeDefaultProps(element: UIElement) {
221-
const defaultValues: [string, unknown][] = [['Tooltip', null]];
222-
223-
defaultValues.forEach(([propname, defaultValue]) => {
224-
if (element[propname] === defaultValue) {
225-
delete element[propname];
226-
}
227-
});
228-
229-
if (element.children) {
230-
element.children.forEach(removeDefaultProps);
231-
}
232-
}

packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ struct Base::_COMPONENT_NAME_:: {
141141
winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept {
142142
}
143143
144+
// CreateAutomationPeer will only be called if this method is overridden
145+
virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
146+
const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept {
147+
return nullptr;
148+
}
149+
144150
::_COMPONENT_VIEW_COMMAND_HANDLERS_::
145151
146152
::_COMPONENT_VIEW_COMMAND_HANDLER_::
@@ -222,6 +228,14 @@ void Register::_COMPONENT_NAME_::NativeComponent(
222228
});
223229
}
224230
231+
if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &Base::_COMPONENT_NAME_::<TUserData>::CreateAutomationPeer) {
232+
builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
233+
const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept {
234+
auto userData = view.UserData().as<TUserData>();
235+
return userData->CreateAutomationPeer(view, args);
236+
});
237+
}
238+
225239
compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
226240
auto userData = winrt::make_self<TUserData>();
227241
if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &Base::_COMPONENT_NAME_::<TUserData>::Initialize) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
import React from 'react';
4+
import {View} from 'react-native';
5+
import {CustomAccessibility} from 'sample-custom-component';
6+
import RNTesterText from '../../components/RNTesterText';
7+
8+
const CustomAccessibilityExample = () => {
9+
return (
10+
<View testID="custom-accessibility-root-1" accessible accessibilityLabel='example root'>
11+
<RNTesterText>The below view should have custom accessibility</RNTesterText>
12+
<CustomAccessibility style={{width: 500, height: 500, backgroundColor:'green'}} accessible accessibilityLabel='accessibility should not show this, as native overrides it' testID="custom-accessibility-1"/>
13+
</View>
14+
);
15+
}
16+
17+
exports.displayName = 'CustomAccessibilityExample';
18+
exports.framework = 'React';
19+
exports.category = 'UI';
20+
exports.title = 'Custom Native Accessibility Example';
21+
exports.description =
22+
'Sample of a Custom Native Component overriding default accessibility';
23+
24+
exports.examples = [
25+
{
26+
title: 'Custom Native Accessibility',
27+
render: function (): React.Node {
28+
return (
29+
<CustomAccessibilityExample />
30+
);
31+
},
32+
}
33+
];

packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ const Components: Array<RNTesterModuleInfo> = [
7979
key: 'Moving Light',
8080
module: require('../examples-win/NativeComponents/MovingLight'),
8181
},
82+
{
83+
key: 'Custom Native Accessibility',
84+
module: require('../examples-win/NativeComponents/CustomAccessibility'),
85+
},
8286
{
8387
key: 'Native Component',
8488
module: require('../examples-win/NativeComponents/NativeComponent'),
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
* Licensed under the MIT License.
4+
*
5+
* @format
6+
*/
7+
8+
import {dumpVisualTree} from '@react-native-windows/automation-commands';
9+
import {goToComponentExample} from './RNTesterNavigation';
10+
import {app} from '@react-native-windows/automation';
11+
import {verifyNoErrorLogs} from './Helpers';
12+
13+
beforeAll(async () => {
14+
// If window is partially offscreen, tests will fail to click on certain elements
15+
await app.setWindowPosition(0, 0);
16+
await app.setWindowSize(1000, 1250);
17+
await goToComponentExample('Custom Native Accessibility Example');
18+
});
19+
20+
afterEach(async () => {
21+
await verifyNoErrorLogs();
22+
});
23+
24+
describe('Custom Accessibility Tests', () => {
25+
test('Verify custom native component has UIA label from native', async () => {
26+
const nativeComponent = await dumpVisualTree('custom-accessibility-1');
27+
28+
// Verify that the native component reports its accessiblity label from the native code
29+
expect(nativeComponent['Automation Tree'].Name).toBe(
30+
'accessiblity label from native',
31+
);
32+
33+
const dump = await dumpVisualTree('custom-accessibility-root-1');
34+
expect(dump).toMatchSnapshot();
35+
});
36+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Custom Accessibility Tests Verify custom native component has UIA label from native 1`] = `
4+
{
5+
"Automation Tree": {
6+
"AutomationId": "custom-accessibility-root-1",
7+
"ControlType": 50026,
8+
"LocalizedControlType": "group",
9+
"Name": "example root",
10+
"__Children": [
11+
{
12+
"AutomationId": "",
13+
"ControlType": 50020,
14+
"LocalizedControlType": "text",
15+
"Name": "The below view should have custom accessibility",
16+
"TextRangePattern.GetText": "The below view should have custom accessibility",
17+
},
18+
{
19+
"AutomationId": "custom-accessibility-1",
20+
"ControlType": 50026,
21+
"LocalizedControlType": "group",
22+
"Name": "accessiblity label from native",
23+
},
24+
],
25+
},
26+
"Component Tree": {
27+
"Type": "Microsoft.ReactNative.Composition.ViewComponentView",
28+
"_Props": {
29+
"AccessibilityLabel": "example root",
30+
"TestId": "custom-accessibility-root-1",
31+
},
32+
"__Children": [
33+
{
34+
"Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
35+
"_Props": {},
36+
},
37+
{
38+
"Type": "Microsoft.ReactNative.Composition.ViewComponentView",
39+
"_Props": {
40+
"AccessibilityLabel": "accessibility should not show this, as native overrides it",
41+
"TestId": "custom-accessibility-1",
42+
},
43+
},
44+
],
45+
},
46+
"Visual Tree": {
47+
"Comment": "custom-accessibility-root-1",
48+
"Offset": "0, 0, 0",
49+
"Size": "998, 519",
50+
"Visual Type": "SpriteVisual",
51+
"__Children": [
52+
{
53+
"Offset": "0, 0, 0",
54+
"Size": "998, 19",
55+
"Visual Type": "SpriteVisual",
56+
"__Children": [
57+
{
58+
"Offset": "0, 0, 0",
59+
"Size": "998, 19",
60+
"Visual Type": "SpriteVisual",
61+
},
62+
],
63+
},
64+
{
65+
"Offset": "0, 19, 0",
66+
"Size": "500, 500",
67+
"Visual Type": "SpriteVisual",
68+
"__Children": [
69+
{
70+
"Brush": {
71+
"Brush Type": "ColorBrush",
72+
"Color": "rgba(0, 128, 0, 255)",
73+
},
74+
"Comment": "custom-accessibility-1",
75+
"Offset": "0, 0, 0",
76+
"Size": "500, 500",
77+
"Visual Type": "SpriteVisual",
78+
},
79+
],
80+
},
81+
],
82+
},
83+
}
84+
`;

0 commit comments

Comments
 (0)