Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ARIA role support to Paper UIManager #12792

Merged
merged 5 commits into from Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Adds ARIA role support to Paper UIManager",
"packageName": "react-native-windows",
"email": "erozell@outlook.com",
"dependentChangeType": "patch"
}
167 changes: 167 additions & 0 deletions vnext/Microsoft.ReactNative/Views/DynamicAutomationPeer.cpp
Expand Up @@ -44,6 +44,164 @@ 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();
// Unless otherwise specified, mappings sourced from:
// https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-ariaspecification#w3c-aria-role-mapped-to-microsoft-active-accessibility-and-ui-automation
// Remaining mappings are:
// "cell": DataItem (based on "gridcell" mapping)
// "feed": List (based on "directory" mapping)
// "figure": Image (based on "img" mapping)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth using the ARIA Core Accessibility API Mapping as a reference since that's the source of truth for how ARIA should map to each platform's native a11y API in browsers. It should agree with the MSDN page and gives mappings for some of the ones you're missing here (for example, I noticed it says feed, figure should map to group, but I would check the other roles, too)

// "math": Group (based on "definition" mapping)
// "meter": Pane (based on "timer" mapping)
// "none": Group (based on "presentation")
// "rowgroup": Group (based on "group" mapping)
// "searchbox": Group (based on "group" mapping)
// "summary": N/A (based on missing ARIA documentation)
// "switch": CheckBox (based on "checkbox" mapping)
// "table": Grid (based on "grid" mapping)
// "term": Group (based on "definition" mapping)
switch (ariaRole) {
case winrt::Microsoft::ReactNative::AriaRole::Alert:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this implementation persists the problem I described in #11432

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference would be to fix Alert and RadioGroup to use the correct core-aam mapping, at the very least in this new 'role' prop if we can't fix accessibilityRole for backward compatibility reasons. I'd like the new ARIA inspired prop to be more rigorous in it's implementation of the aria spec.

return winrt::AutomationControlType::Text;
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::Document;
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::List;
case winrt::Microsoft::ReactNative::AriaRole::Figure:
return winrt::AutomationControlType::Image;
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::Group;
case winrt::Microsoft::ReactNative::AriaRole::Navigation:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::None:
return winrt::AutomationControlType::Pane;
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::Presentation:
return winrt::AutomationControlType::Pane;
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::Group;
case winrt::Microsoft::ReactNative::AriaRole::Region:
return winrt::AutomationControlType::Pane;
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::DataItem;
case winrt::Microsoft::ReactNative::AriaRole::ScrollBar:
return winrt::AutomationControlType::ScrollBar;
case winrt::Microsoft::ReactNative::AriaRole::SearchBox:
return winrt::AutomationControlType::Group;
case winrt::Microsoft::ReactNative::AriaRole::Separator:
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::StatusBar;
case winrt::Microsoft::ReactNative::AriaRole::Switch:
return winrt::AutomationControlType::CheckBox;
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::Group;
case winrt::Microsoft::ReactNative::AriaRole::Timer:
return winrt::AutomationControlType::Pane;
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::Summary:
case winrt::Microsoft::ReactNative::AriaRole::Unknown:
default:
return std::nullopt;
}
}

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

switch (accessibilityRole) {
Expand Down Expand Up @@ -342,6 +500,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
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