diff --git a/change/react-native-windows-5aa167ea-3ec8-4b11-87ed-14f728f2eec6.json b/change/react-native-windows-5aa167ea-3ec8-4b11-87ed-14f728f2eec6.json new file mode 100644 index 00000000000..baa9fd7b9bb --- /dev/null +++ b/change/react-native-windows-5aa167ea-3ec8-4b11-87ed-14f728f2eec6.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement accessibilityLevel for Fabric", + "packageName": "react-native-windows", + "email": "kvineeth@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/@react-native-windows/tester/src/js/examples-win/Accessibility/AccessibilityExampleWindows.tsx b/packages/@react-native-windows/tester/src/js/examples-win/Accessibility/AccessibilityExampleWindows.tsx index 2c3ffef60bb..7ef5860479c 100644 --- a/packages/@react-native-windows/tester/src/js/examples-win/Accessibility/AccessibilityExampleWindows.tsx +++ b/packages/@react-native-windows/tester/src/js/examples-win/Accessibility/AccessibilityExampleWindows.tsx @@ -22,14 +22,19 @@ class AccessibilityBaseExample extends React.Component { <Text>The following has accessibilityLabel and accessibilityHint:</Text> <View style={{width: 50, height: 50, backgroundColor: 'blue'}} + accessible={true} accessibilityLabel="A blue box" accessibilityHint="A hint for the blue box." + accessibilityLevel={1} + testID="accessibility-base-view-1" /> <Text>The following has accessible and accessibilityLabel:</Text> <View style={{width: 50, height: 50, backgroundColor: 'red'}} accessible={true} accessibilityLabel="A hint for the red box." + accessibilityLevel={2} + testID="accessibility-base-view-2" /> <Text> The following has accessibilitySetSize, accessibilityPosInSet and @@ -37,13 +42,15 @@ class AccessibilityBaseExample extends React.Component { </Text> <View style={{width: 50, height: 50, backgroundColor: 'red'}} + testID="accessibility-base-view-3" accessible={true} accessibilityRole="listitem" accessibilityLabel="This label should not be used" aria-label="Aria label takes precedence" accessibilitySetSize={5} accessibilityPosInSet={2} - accessibilityLevel={4} + aria-level={9} //aria-level takes precedence over accessibilityLevel + accessibilityLevel={5} /> </View> ); diff --git a/packages/e2e-test-app-fabric/test/AccessibilityTest.test.ts b/packages/e2e-test-app-fabric/test/AccessibilityTest.test.ts index 68e4df380c1..d27af11844f 100644 --- a/packages/e2e-test-app-fabric/test/AccessibilityTest.test.ts +++ b/packages/e2e-test-app-fabric/test/AccessibilityTest.test.ts @@ -77,4 +77,31 @@ describe('Accessibility Tests', () => { const dump = await dumpVisualTree('accessibilityValue-text'); expect(dump).toMatchSnapshot(); }); + test('Accessibility data for Label,Level and Hint', async () => { + await searchBox('Lab'); + const componentsTab = await app.findElementByTestID( + 'accessibility-base-view-1', + ); + await componentsTab.waitForDisplayed({timeout: 5000}); + const dump = await dumpVisualTree('accessibility-base-view-1'); + expect(dump).toMatchSnapshot(); + }); + test('Accessibility data for Label and Level', async () => { + await searchBox('Lab'); + const componentsTab = await app.findElementByTestID( + 'accessibility-base-view-2', + ); + await componentsTab.waitForDisplayed({timeout: 5000}); + const dump = await dumpVisualTree('accessibility-base-view-2'); + expect(dump).toMatchSnapshot(); + }); + test('Accessibility data for Role, Setsize etc.', async () => { + await searchBox('Lab'); + const componentsTab = await app.findElementByTestID( + 'accessibility-base-view-3', + ); + await componentsTab.waitForDisplayed({timeout: 5000}); + const dump = await dumpVisualTree('accessibility-base-view-3'); + expect(dump).toMatchSnapshot(); + }); }); diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/AccessibilityTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/AccessibilityTest.test.ts.snap index cf7f392168f..1662d978eda 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/AccessibilityTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/AccessibilityTest.test.ts.snap @@ -1,5 +1,95 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Accessibility Tests Accessibility data for Label and Level 1`] = ` +{ + "Automation Tree": { + "AutomationId": "accessibility-base-view-2", + "ControlType": 50026, + "Level": 2, + "LocalizedControlType": "group", + "Name": "A hint for the red box.", + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "A hint for the red box.", + "TestId": "accessibility-base-view-2", + }, + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 0, 0, 255)", + }, + "Comment": "accessibility-base-view-2", + "Offset": "0, 0, 0", + "Size": "50, 50", + "Visual Type": "SpriteVisual", + }, +} +`; + +exports[`Accessibility Tests Accessibility data for Label,Level and Hint 1`] = ` +{ + "Automation Tree": { + "AutomationId": "accessibility-base-view-1", + "ControlType": 50026, + "HelpText": "A hint for the blue box.", + "Level": 1, + "LocalizedControlType": "group", + "Name": "A blue box", + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "A blue box", + "TestId": "accessibility-base-view-1", + }, + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(0, 0, 255, 255)", + }, + "Comment": "accessibility-base-view-1", + "Offset": "0, 0, 0", + "Size": "50, 50", + "Visual Type": "SpriteVisual", + }, +} +`; + +exports[`Accessibility Tests Accessibility data for Role, Setsize etc. 1`] = ` +{ + "Automation Tree": { + "AutomationId": "accessibility-base-view-3", + "ControlType": 50007, + "Level": 9, + "LocalizedControlType": "list item", + "Name": "Aria label takes precedence", + "PositionInSet": 2, + "SizeofSet": 5, + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "Aria label takes precedence", + "TestId": "accessibility-base-view-3", + }, + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 0, 0, 255)", + }, + "Comment": "accessibility-base-view-3", + "Offset": "0, 0, 0", + "Size": "50, 50", + "Visual Type": "SpriteVisual", + }, +} +`; + exports[`Accessibility Tests Components can store range data by setting the min, max, and now of accessibilityValue 1`] = ` { "Automation Tree": { 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 c2bdfc40a12..fdbd40728a8 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 @@ -10,6 +10,8 @@ exports[`snapshotAllPages Accessibility Windows 1`] = ` <View accessibilityHint="A hint for the blue box." accessibilityLabel="A blue box" + accessibilityLevel={1} + accessible={true} style={ { "backgroundColor": "blue", @@ -17,12 +19,14 @@ exports[`snapshotAllPages Accessibility Windows 1`] = ` "width": 50, } } + testID="accessibility-base-view-1" /> <Text> The following has accessible and accessibilityLabel: </Text> <View accessibilityLabel="A hint for the red box." + accessibilityLevel={2} accessible={true} style={ { @@ -31,18 +35,20 @@ exports[`snapshotAllPages Accessibility Windows 1`] = ` "width": 50, } } + testID="accessibility-base-view-2" /> <Text> The following has accessibilitySetSize, accessibilityPosInSet and accessibilityLabel: </Text> <View accessibilityLabel="This label should not be used" - accessibilityLevel={4} + accessibilityLevel={5} accessibilityPosInSet={2} accessibilityRole="listitem" accessibilitySetSize={5} accessible={true} aria-label="Aria label takes precedence" + aria-level={9} style={ { "backgroundColor": "red", @@ -50,6 +56,7 @@ exports[`snapshotAllPages Accessibility Windows 1`] = ` "width": 50, } } + testID="accessibility-base-view-3" /> </View> `; 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 7cff87dadb2..f0a935e1260 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 @@ -494,6 +494,7 @@ winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse( BSTR name; int positionInSet = 0; int sizeOfSet = 0; + int level = 0; LiveSetting liveSetting = LiveSetting::Off; BSTR itemStatus; @@ -511,6 +512,7 @@ winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse( pTarget4->get_CurrentPositionInSet(&positionInSet); pTarget4->get_CurrentSizeOfSet(&sizeOfSet); pTarget4->get_CurrentLiveSetting(&liveSetting); + pTarget4->get_CurrentLevel(&level); pTarget4->Release(); } result.Insert(L"AutomationId", winrt::Windows::Data::Json::JsonValue::CreateStringValue(automationId)); @@ -523,6 +525,7 @@ winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse( InsertStringValueIfNotEmpty(result, L"Name", name); InsertIntValueIfNotDefault(result, L"PositionInSet", positionInSet); InsertIntValueIfNotDefault(result, L"SizeofSet", sizeOfSet); + InsertIntValueIfNotDefault(result, L"Level", level); InsertLiveSettingValueIfNotDefault(result, L"LiveSetting", liveSetting); InsertStringValueIfNotEmpty(result, L"ItemStatus", itemStatus); DumpUIAPatternInfo(pTarget, result); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index ad5e31cff02..3bedab40f08 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -559,6 +559,11 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERT : SysAllocString(L""); break; } + case UIA_LevelPropertyId: { + pRetVal->vt = VT_I4; + pRetVal->lVal = props->accessibilityLevel; + break; + } } return hr; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 38af8995408..c23df8a9d06 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -804,6 +804,9 @@ void ComponentView::updateAccessibilityProps( oldViewProps.accessibilityLiveRegion, newViewProps.accessibilityLiveRegion); + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + EnsureUiaProvider(), UIA_LevelPropertyId, oldViewProps.accessibilityLevel, newViewProps.accessibilityLevel); + if ((oldViewProps.accessibilityState.has_value() && oldViewProps.accessibilityState->selected.has_value()) != ((newViewProps.accessibilityState.has_value() && newViewProps.accessibilityState->selected.has_value()))) { auto compProvider = diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp index 3b8666ab7b9..4c3aeb8e37a 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp @@ -40,6 +40,10 @@ HostPlatformViewProps::HostPlatformViewProps( ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.accessibilitySetSize : convertRawProp(context, rawProps, "accessibilitySetSize", sourceProps.accessibilitySetSize, 0)), + accessibilityLevel( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.accessibilityLevel + : convertRawProp(context, rawProps, "accessibilityLevel", sourceProps.accessibilityLevel, 0)), accessibilityLiveRegion( ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.accessibilityLiveRegion : convertRawProp( @@ -84,6 +88,7 @@ void HostPlatformViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(focusable); RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityPosInSet); RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilitySetSize); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLevel); RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLiveRegion); RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents); RAW_SET_PROP_SWITCH_CASE_BASIC(keyUpEvents); diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.h index 885dd8cbf18..0a48bc1e787 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.h +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.h @@ -27,6 +27,7 @@ class HostPlatformViewProps : public BaseViewProps { int accessibilityPosInSet{0}; int accessibilitySetSize{0}; std::string accessibilityLiveRegion{"none"}; + int accessibilityLevel{0}; // std::optional<std::string> overflowAnchor{}; std::optional<std::string> tooltip{};