Skip to content

Commit

Permalink
Adds ARIA role support to Paper UIManager (#12792)
Browse files Browse the repository at this point in the history
* Adds ARIA role support to Paper UIManager

In react-native v0.73, the `role` prop was switched from setting its value to `accessibilityRole` to a standalone native prop.

This wires up the native prop behavior in the Paper UIManager for react-native-windows.

Here is the table of ARIA role to UIA role in this commit:
|**ARIA role**|**UIA role**|
|alert|Text|
|alertdialog|Window|
|application|Group|
|article|Document|
|banner|Text|
|button|Button|
|cell|DataItem|
|checkbox|CheckBox|
|columnheader|HeaderItem|
|combobox|ComboBox|
|complementary|Text|
|contentinfo|Text|
|definition|Text|
|dialog|Window|
|directory||
|document|Document|
|feed||
|figure|Image|
|form||
|grid|DataGrid|
|group|Group
|heading|Text|
|img|Image|
|link|Hyperlink|
|list|List|
|listitem|ListItem|
|log||
|main||
|marquee||
|math||
|menu|Menu|
|menubar|MenuBar|
|menuitem|MenuItem|
|meter||
|navigation||
|none|Group|
|note|Text|
|option||
|presentation||
|progressbar|PrgressBar|
|radio|RadioButton|
|radiogroup|Group|
|region|Group|
|row|DataGrid|
|rowgroup|Group|
|rowheader|HeaderItem|
|scrollbar|ScrollBar|
|searchbox|Edit|
|separator|Separator|
|slider|Slider|
|spinbutton|Spinner|
|status|Text|
|summary|Text|
|switch|Group|
|tab|TabItem|
|table|Table|
|tablist|Tab|
|tabpanel|Group|
|term|Text|
|timer|Group|
|toolbar|ToolBar|
|tooltip|ToolTip|
|tree|Tree|
|treegrid|Tree|
|treeitem|TreeItem|

* Change files

* Improves ARIA role to UIA role mappings

Switches from ad hoc mappings to mappings defined here:
https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-ariaspecification#w3c-aria-role-mapped-to-microsoft-active-accessibility-and-ui-automation

Comments left inline for how the missing mappings were interpolated.

* Fixes a few issues with previous mappings

* Re-map ARIA -> UIA roles based on https://www.w3.org/TR/core-aam-1.2
  • Loading branch information
rozele committed Mar 11, 2024
1 parent e6464a7 commit bc70cea
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 2 deletions.
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Adds ARIA role support to Paper UIManager",
"packageName": "react-native-windows",
"email": "erozell@outlook.com",
"dependentChangeType": "patch"
}
157 changes: 157 additions & 0 deletions vnext/Microsoft.ReactNative/Views/DynamicAutomationPeer.cpp
Expand Up @@ -44,6 +44,154 @@ winrt::hstring DynamicAutomationPeer::GetNameCore() const {
}

winrt::AutomationControlType DynamicAutomationPeer::GetAutomationControlTypeCore() const {
const auto automationControlType = GetAutomationControlTypeFromAriaRole();
return automationControlType ? automationControlType.value() : GetAutomationControlTypeFromAccessibilityRole();
}

std::optional<winrt::AutomationControlType> DynamicAutomationPeer::GetAutomationControlTypeFromAriaRole() const {
const auto ariaRole = GetAriaRole();
// Sourced from: https://www.w3.org/TR/core-aam-1.2
// Remaining unmapped roles are:
// - "none"
// - "presentation"
switch (ariaRole) {
case winrt::Microsoft::ReactNative::AriaRole::Alert:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::AlertDialog:
return winrt::AutomationControlType::Pane;
case winrt::Microsoft::ReactNative::AriaRole::Application:
return winrt::AutomationControlType::Pane;
case winrt::Microsoft::ReactNative::AriaRole::Article:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Banner:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Button:
return winrt::AutomationControlType::Button;
case winrt::Microsoft::ReactNative::AriaRole::Cell:
return winrt::AutomationControlType::DataItem;
case winrt::Microsoft::ReactNative::AriaRole::CheckBox:
return winrt::AutomationControlType::CheckBox;
case winrt::Microsoft::ReactNative::AriaRole::ColumnHeader:
return winrt::AutomationControlType::DataItem;
case winrt::Microsoft::ReactNative::AriaRole::ComboBox:
return winrt::AutomationControlType::ComboBox;
case winrt::Microsoft::ReactNative::AriaRole::Complementary:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::ContentInfo:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Definition:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Dialog:
return winrt::AutomationControlType::Pane;
case winrt::Microsoft::ReactNative::AriaRole::Directory:
return winrt::AutomationControlType::List;
case winrt::Microsoft::ReactNative::AriaRole::Document:
return winrt::AutomationControlType::Document;
case winrt::Microsoft::ReactNative::AriaRole::Feed:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Figure:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Form:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Grid:
return winrt::AutomationControlType::DataGrid;
case winrt::Microsoft::ReactNative::AriaRole::Group:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Heading:
return winrt::AutomationControlType::Text;
case winrt::Microsoft::ReactNative::AriaRole::Img:
return winrt::AutomationControlType::Image;
case winrt::Microsoft::ReactNative::AriaRole::Link:
return winrt::AutomationControlType::Hyperlink;
case winrt::Microsoft::ReactNative::AriaRole::List:
return winrt::AutomationControlType::List;
case winrt::Microsoft::ReactNative::AriaRole::ListItem:
return winrt::AutomationControlType::ListItem;
case winrt::Microsoft::ReactNative::AriaRole::Log:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Main:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Marquee:
return winrt::AutomationControlType::Text;
case winrt::Microsoft::ReactNative::AriaRole::Math:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Menu:
return winrt::AutomationControlType::Menu;
case winrt::Microsoft::ReactNative::AriaRole::MenuBar:
return winrt::AutomationControlType::MenuBar;
case winrt::Microsoft::ReactNative::AriaRole::MenuItem:
return winrt::AutomationControlType::MenuItem;
case winrt::Microsoft::ReactNative::AriaRole::Meter:
return winrt::AutomationControlType::ProgressBar;
case winrt::Microsoft::ReactNative::AriaRole::Navigation:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Note:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Option:
return winrt::AutomationControlType::ListItem;
case winrt::Microsoft::ReactNative::AriaRole::ProgressBar:
return winrt::AutomationControlType::ProgressBar;
case winrt::Microsoft::ReactNative::AriaRole::Radio:
return winrt::AutomationControlType::RadioButton;
case winrt::Microsoft::ReactNative::AriaRole::RadioGroup:
return winrt::AutomationControlType::List;
case winrt::Microsoft::ReactNative::AriaRole::Region:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Row:
return winrt::AutomationControlType::DataItem;
case winrt::Microsoft::ReactNative::AriaRole::RowGroup:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::RowHeader:
return winrt::AutomationControlType::HeaderItem;
case winrt::Microsoft::ReactNative::AriaRole::ScrollBar:
return winrt::AutomationControlType::ScrollBar;
case winrt::Microsoft::ReactNative::AriaRole::SearchBox:
return winrt::AutomationControlType::Edit;
case winrt::Microsoft::ReactNative::AriaRole::Separator:
if (auto const &viewControl = Owner().try_as<winrt::Microsoft::ReactNative::ViewControl>()) {
if (viewControl.IsTabStop()) {
return winrt::AutomationControlType::Thumb;
}
}
return winrt::AutomationControlType::Separator;
case winrt::Microsoft::ReactNative::AriaRole::Slider:
return winrt::AutomationControlType::Slider;
case winrt::Microsoft::ReactNative::AriaRole::SpinButton:
return winrt::AutomationControlType::Spinner;
case winrt::Microsoft::ReactNative::AriaRole::Status:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Switch:
return winrt::AutomationControlType::Button;
case winrt::Microsoft::ReactNative::AriaRole::Tab:
return winrt::AutomationControlType::TabItem;
case winrt::Microsoft::ReactNative::AriaRole::Table:
return winrt::AutomationControlType::DataGrid;
case winrt::Microsoft::ReactNative::AriaRole::TabList:
return winrt::AutomationControlType::Tab;
case winrt::Microsoft::ReactNative::AriaRole::TabPanel:
return winrt::AutomationControlType::Pane;
case winrt::Microsoft::ReactNative::AriaRole::Term:
return winrt::AutomationControlType::Text;
case winrt::Microsoft::ReactNative::AriaRole::Timer:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::ToolBar:
return winrt::AutomationControlType::ToolBar;
case winrt::Microsoft::ReactNative::AriaRole::ToolTip:
return winrt::AutomationControlType::ToolTip;
case winrt::Microsoft::ReactNative::AriaRole::Tree:
return winrt::AutomationControlType::Tree;
case winrt::Microsoft::ReactNative::AriaRole::TreeGrid:
return winrt::AutomationControlType::DataGrid;
case winrt::Microsoft::ReactNative::AriaRole::TreeItem:
return winrt::AutomationControlType::TreeItem;
case winrt::Microsoft::ReactNative::AriaRole::None:
case winrt::Microsoft::ReactNative::AriaRole::Presentation:
default:
return std::nullopt;
}
}

winrt::AutomationControlType DynamicAutomationPeer::GetAutomationControlTypeFromAccessibilityRole() const {
auto accessibilityRole = GetAccessibilityRole();

switch (accessibilityRole) {
Expand Down Expand Up @@ -342,6 +490,15 @@ winrt::Microsoft::ReactNative::AccessibilityRoles DynamicAutomationPeer::GetAcce
return winrt::Microsoft::ReactNative::AccessibilityRoles::None;
}

winrt::Microsoft::ReactNative::AriaRole DynamicAutomationPeer::GetAriaRole() const {
try {
return DynamicAutomationProperties::GetAriaRole(Owner());
} catch (...) {
}

return winrt::Microsoft::ReactNative::AriaRole::Unknown;
}

bool DynamicAutomationPeer::HasAccessibilityState(winrt::Microsoft::ReactNative::AccessibilityStates state) const {
try {
auto const &owner = Owner();
Expand Down
5 changes: 5 additions & 0 deletions vnext/Microsoft.ReactNative/Views/DynamicAutomationPeer.h
Expand Up @@ -72,6 +72,11 @@ struct DynamicAutomationPeer : DynamicAutomationPeerT<DynamicAutomationPeer> {
private:
winrt::hstring GetContentName() const;
winrt::Microsoft::ReactNative::AccessibilityRoles GetAccessibilityRole() const;
winrt::Microsoft::ReactNative::AriaRole GetAriaRole() const;

std::optional<xaml::Automation::Peers::AutomationControlType> GetAutomationControlTypeFromAriaRole() const;
xaml::Automation::Peers::AutomationControlType GetAutomationControlTypeFromAccessibilityRole() const;

bool HasAccessibilityState(winrt::Microsoft::ReactNative::AccessibilityStates state) const;
bool HasAccessibilityValue(winrt::Microsoft::ReactNative::AccessibilityValue value) const;
double GetAccessibilityValueRange(winrt::Microsoft::ReactNative::AccessibilityValue value) const;
Expand Down
20 changes: 20 additions & 0 deletions vnext/Microsoft.ReactNative/Views/DynamicAutomationProperties.cpp
Expand Up @@ -48,6 +48,26 @@ winrt::Microsoft::ReactNative::AccessibilityRoles DynamicAutomationProperties::G
element.GetValue(AccessibilityRoleProperty()));
}

xaml::DependencyProperty DynamicAutomationProperties::AriaRoleProperty() {
static xaml::DependencyProperty s_ariaRoleProperty = xaml::DependencyProperty::RegisterAttached(
L"AriaRole",
winrt::xaml_typename<winrt::Microsoft::ReactNative::AriaRole>(),
dynamicAutomationTypeName,
winrt::PropertyMetadata(winrt::box_value(winrt::Microsoft::ReactNative::AriaRole::Unknown)));

return s_ariaRoleProperty;
}

void DynamicAutomationProperties::SetAriaRole(
xaml::UIElement const &element,
winrt::Microsoft::ReactNative::AriaRole const &value) {
element.SetValue(AriaRoleProperty(), winrt::box_value<Microsoft::ReactNative::AriaRole>(value));
}

winrt::Microsoft::ReactNative::AriaRole DynamicAutomationProperties::GetAriaRole(xaml::UIElement const &element) {
return winrt::unbox_value<winrt::Microsoft::ReactNative::AriaRole>(element.GetValue(AriaRoleProperty()));
}

xaml::DependencyProperty DynamicAutomationProperties::AccessibilityStateSelectedProperty() {
static xaml::DependencyProperty s_AccessibilityStateSelectedProperty = xaml::DependencyProperty::RegisterAttached(
L"AccessibilityStateSelected",
Expand Down
Expand Up @@ -26,6 +26,10 @@ struct DynamicAutomationProperties : DynamicAutomationPropertiesT<DynamicAutomat
winrt::Microsoft::ReactNative::AccessibilityRoles const &value);
static AccessibilityRoles GetAccessibilityRole(xaml::UIElement const &element);

static xaml::DependencyProperty AriaRoleProperty();
static void SetAriaRole(xaml::UIElement const &element, winrt::Microsoft::ReactNative::AriaRole const &value);
static AriaRole GetAriaRole(xaml::UIElement const &element);

static xaml::DependencyProperty AccessibilityStateSelectedProperty();
static void SetAccessibilityStateSelected(xaml::UIElement const &element, bool value);
static bool GetAccessibilityStateSelected(xaml::UIElement const &element);
Expand Down

0 comments on commit bc70cea

Please sign in to comment.