diff --git a/change/react-native-windows-56b491f9-8c86-4189-9baf-209370171b9d.json b/change/react-native-windows-56b491f9-8c86-4189-9baf-209370171b9d.json new file mode 100644 index 00000000000..57d0d72595e --- /dev/null +++ b/change/react-native-windows-56b491f9-8c86-4189-9baf-209370171b9d.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Add UIA HeadingLevel behaviour for Fabric architecture", + "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 0848d282dc3..026e5ea4aae 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 @@ -24,6 +24,7 @@ class AccessibilityBaseExample extends React.Component { style={{width: 50, height: 50, backgroundColor: 'blue'}} accessible={true} accessibilityLabel="A blue box" + role="heading" accessibilityHint="A hint for the blue box." accessibilityLevel={1} accessibilityItemType="comment" @@ -42,6 +43,7 @@ class AccessibilityBaseExample extends React.Component { style={{width: 50, height: 50, backgroundColor: 'red'}} accessible={true} accessibilityLabel="A hint for the red box." + accessibilityRole="header" accessibilityLevel={2} testID="accessibility-base-view-2" /> 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 b2f31b2b4a6..7001368444f 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 @@ -4,9 +4,10 @@ exports[`Accessibility Tests Accessibility data for Label and Level 1`] = ` { "Automation Tree": { "AutomationId": "accessibility-base-view-2", - "ControlType": 50026, + "ControlType": 50020, + "HeadingLevel": 80052, "Level": 2, - "LocalizedControlType": "group", + "LocalizedControlType": "text", }, "Component Tree": { "Type": "Microsoft.ReactNative.Composition.ViewComponentView", @@ -36,12 +37,13 @@ exports[`Accessibility Tests Accessibility data for Label,Level and Hint 1`] = ` "AnnotationPattern.TypeId": 60003, "AnnotationPattern.TypeName": "Check Comment", "AutomationId": "accessibility-base-view-1", - "ControlType": 50026, + "ControlType": 50020, "Description": "Sample Description", + "HeadingLevel": 80051, "HelpText": "A hint for the blue box.", "ItemType": "comment", "Level": 1, - "LocalizedControlType": "group", + "LocalizedControlType": "text", }, "Component Tree": { "Type": "Microsoft.ReactNative.Composition.ViewComponentView", 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 007260bbc1c..aa8047b3207 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 @@ -23,6 +23,7 @@ exports[`snapshotAllPages Accessibility Windows 1`] = ` accessibilityLabel="A blue box" accessibilityLevel={1} accessible={true} + role="heading" style={ { "backgroundColor": "blue", @@ -38,6 +39,7 @@ exports[`snapshotAllPages Accessibility Windows 1`] = ` get_CurrentFullDescription(&description); } + IUIAutomationElement8 *pTarget8; + hr = pTarget->QueryInterface(__uuidof(IUIAutomationElement8), reinterpret_cast(&pTarget8)); + if (SUCCEEDED(hr) && pTarget8) { + pTarget8->get_CurrentHeadingLevel(&headingLevel); + } result.Insert(L"AutomationId", winrt::Windows::Data::Json::JsonValue::CreateStringValue(automationId)); result.Insert(L"ControlType", winrt::Windows::Data::Json::JsonValue::CreateNumberValue(controlType)); InsertStringValueIfNotEmpty(result, L"HelpText", helpText); @@ -560,6 +566,7 @@ winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse( L"LocalizedControlType", winrt::Windows::Data::Json::JsonValue::CreateStringValue(localizedControlType)); InsertStringValueIfNotEmpty(result, L"Name", name); InsertIntValueIfNotDefault(result, L"PositionInSet", positionInSet); + InsertIntValueIfNotDefault(result, L"HeadingLevel", headingLevel, HeadingLevel_None); InsertIntValueIfNotDefault(result, L"SizeofSet", sizeOfSet); InsertIntValueIfNotDefault(result, L"Level", level); InsertLiveSettingValueIfNotDefault(result, L"LiveSetting", liveSetting); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index 5e34404bd31..6ce1074a63c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -313,161 +313,6 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE return S_OK; } -long GetControlTypeFromString(const std::string &role) noexcept { - if (role == "adjustable") { - return UIA_SliderControlTypeId; - } else if (role == "group" || role == "search" || role == "radiogroup" || role == "timer" || role.empty()) { - return UIA_GroupControlTypeId; - } else if (role == "button" || role == "imagebutton" || role == "switch" || role == "togglebutton") { - return UIA_ButtonControlTypeId; - } else if (role == "checkbox") { - return UIA_CheckBoxControlTypeId; - } else if (role == "combobox") { - return UIA_ComboBoxControlTypeId; - } else if (role == "alert" || role == "header" || role == "summary" || role == "text") { - return UIA_TextControlTypeId; - } else if (role == "image") { - return UIA_ImageControlTypeId; - } else if (role == "keyboardkey") { - return UIA_CustomControlTypeId; - } else if (role == "link") { - return UIA_HyperlinkControlTypeId; - } - // list and listitem were added by RNW to better support UIA Control Types - else if (role == "list") { - return UIA_ListControlTypeId; - } else if (role == "listitem") { - return UIA_ListItemControlTypeId; - } else if (role == "menu") { - return UIA_MenuControlTypeId; - } else if (role == "menubar") { - return UIA_MenuBarControlTypeId; - } else if (role == "menuitem") { - return UIA_MenuItemControlTypeId; - } - // If role is "none", remove the element from the control tree - // and expose it as a plain element would in the raw tree. - else if (role == "none") { - return UIA_GroupControlTypeId; - } else if (role == "progressbar") { - return UIA_ProgressBarControlTypeId; - } else if (role == "radio") { - return UIA_RadioButtonControlTypeId; - } else if (role == "scrollbar") { - return UIA_ScrollBarControlTypeId; - } else if (role == "spinbutton") { - return UIA_SpinnerControlTypeId; - } else if (role == "splitbutton") { - return UIA_SplitButtonControlTypeId; - } else if (role == "tab") { - return UIA_TabItemControlTypeId; - } else if (role == "tablist") { - return UIA_TabControlTypeId; - } else if (role == "textinput" || role == "searchbox") { - return UIA_EditControlTypeId; - } else if (role == "toolbar") { - return UIA_ToolBarControlTypeId; - } else if (role == "tree") { - return UIA_TreeControlTypeId; - } else if (role == "treeitem") { - return UIA_TreeItemControlTypeId; - } else if (role == "pane") { - return UIA_PaneControlTypeId; - } - assert(false); - return UIA_GroupControlTypeId; -} - -long GetControlTypeFromRole(const facebook::react::Role &role) noexcept { - switch (role) { - case facebook::react::Role::Alert: - return UIA_TextControlTypeId; - case facebook::react::Role::Application: - return UIA_WindowControlTypeId; - case facebook::react::Role::Button: - return UIA_ButtonControlTypeId; - case facebook::react::Role::Checkbox: - return UIA_CheckBoxControlTypeId; - case facebook::react::Role::Columnheader: - return UIA_HeaderControlTypeId; - case facebook::react::Role::Combobox: - return UIA_ComboBoxControlTypeId; - case facebook::react::Role::Document: - return UIA_DocumentControlTypeId; - case facebook::react::Role::Grid: - return UIA_GroupControlTypeId; - case facebook::react::Role::Group: - return UIA_GroupControlTypeId; - case facebook::react::Role::Heading: - return UIA_TextControlTypeId; - case facebook::react::Role::Img: - return UIA_ImageControlTypeId; - case facebook::react::Role::Link: - return UIA_HyperlinkControlTypeId; - case facebook::react::Role::List: - return UIA_ListControlTypeId; - case facebook::react::Role::Listitem: - return UIA_ListItemControlTypeId; - case facebook::react::Role::Menu: - return UIA_MenuControlTypeId; - case facebook::react::Role::Menubar: - return UIA_MenuBarControlTypeId; - case facebook::react::Role::Menuitem: - return UIA_MenuItemControlTypeId; - case facebook::react::Role::None: - return UIA_GroupControlTypeId; - case facebook::react::Role::Presentation: - return UIA_GroupControlTypeId; - case facebook::react::Role::Progressbar: - return UIA_ProgressBarControlTypeId; - case facebook::react::Role::Radio: - return UIA_RadioButtonControlTypeId; - case facebook::react::Role::Radiogroup: - return UIA_GroupControlTypeId; - case facebook::react::Role::Rowgroup: - return UIA_GroupControlTypeId; - case facebook::react::Role::Rowheader: - return UIA_HeaderControlTypeId; - case facebook::react::Role::Scrollbar: - return UIA_ScrollBarControlTypeId; - case facebook::react::Role::Searchbox: - return UIA_EditControlTypeId; - case facebook::react::Role::Separator: - return UIA_SeparatorControlTypeId; - case facebook::react::Role::Slider: - return UIA_SliderControlTypeId; - case facebook::react::Role::Spinbutton: - return UIA_SpinnerControlTypeId; - case facebook::react::Role::Status: - return UIA_StatusBarControlTypeId; - case facebook::react::Role::Summary: - return UIA_GroupControlTypeId; - case facebook::react::Role::Switch: - return UIA_ButtonControlTypeId; - case facebook::react::Role::Tab: - return UIA_TabItemControlTypeId; - case facebook::react::Role::Table: - return UIA_TableControlTypeId; - case facebook::react::Role::Tablist: - return UIA_TabControlTypeId; - case facebook::react::Role::Tabpanel: - return UIA_TabControlTypeId; - case facebook::react::Role::Timer: - return UIA_ButtonControlTypeId; - case facebook::react::Role::Toolbar: - return UIA_ToolBarControlTypeId; - case facebook::react::Role::Tooltip: - return UIA_ToolTipControlTypeId; - case facebook::react::Role::Tree: - return UIA_TreeControlTypeId; - case facebook::react::Role::Treegrid: - return UIA_TreeControlTypeId; - case facebook::react::Role::Treeitem: - return UIA_TreeItemControlTypeId; - } - return UIA_GroupControlTypeId; -} - HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) { if (pRetVal == nullptr) return E_POINTER; @@ -640,6 +485,11 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERT pRetVal->bstrVal = SysAllocString(desc.c_str()); break; } + case UIA_HeadingLevelPropertyId: { + pRetVal->vt = VT_I4; + pRetVal->lVal = GetHeadingLevel(props->accessibilityLevel, props->accessibilityRole, props->role); + break; + } } return hr; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp index f2f12530ed1..68551fd5f22 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -291,6 +291,190 @@ long GetAnnotationTypeId(const std::string &annotationType) noexcept { return AnnotationType_Unknown; } +long GetControlTypeFromString(const std::string &role) noexcept { + if (role == "adjustable") { + return UIA_SliderControlTypeId; + } else if (role == "group" || role == "search" || role == "radiogroup" || role == "timer" || role.empty()) { + return UIA_GroupControlTypeId; + } else if (role == "button" || role == "imagebutton" || role == "switch" || role == "togglebutton") { + return UIA_ButtonControlTypeId; + } else if (role == "checkbox") { + return UIA_CheckBoxControlTypeId; + } else if (role == "combobox") { + return UIA_ComboBoxControlTypeId; + } else if (role == "alert" || role == "header" || role == "summary" || role == "text") { + return UIA_TextControlTypeId; + } else if (role == "image") { + return UIA_ImageControlTypeId; + } else if (role == "keyboardkey") { + return UIA_CustomControlTypeId; + } else if (role == "link") { + return UIA_HyperlinkControlTypeId; + } + // list and listitem were added by RNW to better support UIA Control Types + else if (role == "list") { + return UIA_ListControlTypeId; + } else if (role == "listitem") { + return UIA_ListItemControlTypeId; + } else if (role == "menu") { + return UIA_MenuControlTypeId; + } else if (role == "menubar") { + return UIA_MenuBarControlTypeId; + } else if (role == "menuitem") { + return UIA_MenuItemControlTypeId; + } + // If role is "none", remove the element from the control tree + // and expose it as a plain element would in the raw tree. + else if (role == "none") { + return UIA_GroupControlTypeId; + } else if (role == "progressbar") { + return UIA_ProgressBarControlTypeId; + } else if (role == "radio") { + return UIA_RadioButtonControlTypeId; + } else if (role == "scrollbar") { + return UIA_ScrollBarControlTypeId; + } else if (role == "spinbutton") { + return UIA_SpinnerControlTypeId; + } else if (role == "splitbutton") { + return UIA_SplitButtonControlTypeId; + } else if (role == "tab") { + return UIA_TabItemControlTypeId; + } else if (role == "tablist") { + return UIA_TabControlTypeId; + } else if (role == "textinput" || role == "searchbox") { + return UIA_EditControlTypeId; + } else if (role == "toolbar") { + return UIA_ToolBarControlTypeId; + } else if (role == "tree") { + return UIA_TreeControlTypeId; + } else if (role == "treeitem") { + return UIA_TreeItemControlTypeId; + } else if (role == "pane") { + return UIA_PaneControlTypeId; + } + assert(false); + return UIA_GroupControlTypeId; +} + +long GetControlTypeFromRole(const facebook::react::Role &role) noexcept { + switch (role) { + case facebook::react::Role::Alert: + return UIA_TextControlTypeId; + case facebook::react::Role::Application: + return UIA_WindowControlTypeId; + case facebook::react::Role::Button: + return UIA_ButtonControlTypeId; + case facebook::react::Role::Checkbox: + return UIA_CheckBoxControlTypeId; + case facebook::react::Role::Columnheader: + return UIA_HeaderControlTypeId; + case facebook::react::Role::Combobox: + return UIA_ComboBoxControlTypeId; + case facebook::react::Role::Document: + return UIA_DocumentControlTypeId; + case facebook::react::Role::Grid: + return UIA_GroupControlTypeId; + case facebook::react::Role::Group: + return UIA_GroupControlTypeId; + case facebook::react::Role::Heading: + return UIA_TextControlTypeId; + case facebook::react::Role::Img: + return UIA_ImageControlTypeId; + case facebook::react::Role::Link: + return UIA_HyperlinkControlTypeId; + case facebook::react::Role::List: + return UIA_ListControlTypeId; + case facebook::react::Role::Listitem: + return UIA_ListItemControlTypeId; + case facebook::react::Role::Menu: + return UIA_MenuControlTypeId; + case facebook::react::Role::Menubar: + return UIA_MenuBarControlTypeId; + case facebook::react::Role::Menuitem: + return UIA_MenuItemControlTypeId; + case facebook::react::Role::None: + return UIA_GroupControlTypeId; + case facebook::react::Role::Presentation: + return UIA_GroupControlTypeId; + case facebook::react::Role::Progressbar: + return UIA_ProgressBarControlTypeId; + case facebook::react::Role::Radio: + return UIA_RadioButtonControlTypeId; + case facebook::react::Role::Radiogroup: + return UIA_GroupControlTypeId; + case facebook::react::Role::Rowgroup: + return UIA_GroupControlTypeId; + case facebook::react::Role::Rowheader: + return UIA_HeaderControlTypeId; + case facebook::react::Role::Scrollbar: + return UIA_ScrollBarControlTypeId; + case facebook::react::Role::Searchbox: + return UIA_EditControlTypeId; + case facebook::react::Role::Separator: + return UIA_SeparatorControlTypeId; + case facebook::react::Role::Slider: + return UIA_SliderControlTypeId; + case facebook::react::Role::Spinbutton: + return UIA_SpinnerControlTypeId; + case facebook::react::Role::Status: + return UIA_StatusBarControlTypeId; + case facebook::react::Role::Summary: + return UIA_GroupControlTypeId; + case facebook::react::Role::Switch: + return UIA_ButtonControlTypeId; + case facebook::react::Role::Tab: + return UIA_TabItemControlTypeId; + case facebook::react::Role::Table: + return UIA_TableControlTypeId; + case facebook::react::Role::Tablist: + return UIA_TabControlTypeId; + case facebook::react::Role::Tabpanel: + return UIA_TabControlTypeId; + case facebook::react::Role::Timer: + return UIA_ButtonControlTypeId; + case facebook::react::Role::Toolbar: + return UIA_ToolBarControlTypeId; + case facebook::react::Role::Tooltip: + return UIA_ToolTipControlTypeId; + case facebook::react::Role::Tree: + return UIA_TreeControlTypeId; + case facebook::react::Role::Treegrid: + return UIA_TreeControlTypeId; + case facebook::react::Role::Treeitem: + return UIA_TreeItemControlTypeId; + } + return UIA_GroupControlTypeId; +} + +long GetHeadingLevel(int headingLevel, const std::string &strRole, const facebook::react::Role &role) noexcept { + if (strRole != "header" && role != facebook::react::Role::Heading) { + return HeadingLevel_None; + } + + switch (headingLevel) { + case 1: + return HeadingLevel1; + case 2: + return HeadingLevel2; + case 3: + return HeadingLevel3; + case 4: + return HeadingLevel4; + case 5: + return HeadingLevel5; + case 6: + return HeadingLevel6; + case 7: + return HeadingLevel7; + case 8: + return HeadingLevel8; + case 9: + return HeadingLevel9; + default: + return HeadingLevel_None; + } +} + bool accessibilityAnnotationHasValue( const std::optional &annotation) noexcept { return annotation.has_value() && diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h index 3d63c0c3e2b..3efd94da399 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h @@ -62,6 +62,12 @@ long GetLiveSetting(const std::string &liveRegion) noexcept; long GetAnnotationTypeId(const std::string &annotationType) noexcept; +long GetControlTypeFromRole(const facebook::react::Role &role) noexcept; + +long GetControlTypeFromString(const std::string &role) noexcept; + +long GetHeadingLevel(int headingLevel, const std::string &strRole, const facebook::react::Role &role) noexcept; + bool accessibilityAnnotationHasValue( const std::optional &annotation) noexcept;