From 786f81d567423aa531d404fd09fc5c74b1541c82 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 8 May 2023 15:07:03 -0700 Subject: [PATCH 1/4] Native ARIA Roles: Shared code Summary: ### Stack ARIA roles in React Native are implemented on top of `accessibilityRole`. This is lossy because there are many more ARIA roles than `accessibilityRole`. This is especially true for RN on desktop where `accessibilityRole` was designed around accessibility APIs only available on mobile. This series of changes aims to change this implementation to instead pass the ARIA role to native, alongside any existing `accessibilityRole`. This gives the platform more control in exactly how to map an ARIA role to native behavior. As an example, this would allow mapping any ARIA role to [`AutomationControlType`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.automation.peers.automationcontroltype?view=windowsdesktop-7.0&viewFallbackFrom=dotnet-uwp-10.0) on Windows without needing to fork to add new options to `accessibilityRole`. It also allows greater implementation flexibility for other platforms down the line, but for now, iOS and Android behave the same as before (though with their implementation living in native). ### Diff This syncs the Fabric representations of Roles to the current state of the world in JS, and adds usage to the view configs. 1. `Role` enum for the View `role` prop (ARIA role) 2. Sync enums and conversions to JS `AccessibilityRole` 1. This parsing is done for `TextAttributes` only. `View` uses the string directly 2. Add ARIA roles, and parse those to their enum form. 3. Move enums from attributedstring primitves to accessibility primitives 3. Add to viewconfig to allow it to be passed Changelog: [Internal] Differential Revision: https://www.internalfb.com/diff/D45431372?entry_point=27 fbshipit-source-id: 7a6547e1932079555a4ab7254297e5c42fbb0e7f --- .../View/ReactNativeViewAttributes.js | 1 + .../NativeComponent/BaseViewConfig.android.js | 1 + .../NativeComponent/BaseViewConfig.ios.js | 1 + .../renderer/attributedstring/CMakeLists.txt | 1 + .../attributedstring/TextAttributes.cpp | 4 + .../attributedstring/TextAttributes.h | 5 +- .../renderer/attributedstring/conversions.h | 145 +----- .../renderer/attributedstring/primitives.h | 38 -- .../components/text/BaseTextProps.cpp | 8 + .../components/view/AccessibilityPrimitives.h | 127 +++++ .../components/view/AccessibilityProps.cpp | 9 + .../components/view/AccessibilityProps.h | 1 + .../view/accessibilityPropsConversions.h | 482 ++++++++++++++++++ .../RCTAttributedTextUtils.mm | 48 +- 14 files changed, 682 insertions(+), 189 deletions(-) diff --git a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js index f4ea0d2c1f52..e27df43205f5 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js @@ -35,6 +35,7 @@ const UIView = { collapsable: true, needsOffscreenAlphaCompositing: true, style: ReactNativeStyleAttributes, + role: true, }; const RCTView = { diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index 5cd505439ea7..fef57a1aea9a 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -179,6 +179,7 @@ const validAttributesForNonEventProps = { accessibilityActions: true, accessibilityValue: true, importantForAccessibility: true, + role: true, rotation: true, scaleX: true, scaleY: true, diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js index 9c1e72fa1a1a..bb13443bf44a 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -198,6 +198,7 @@ const validAttributesForNonEventProps = { nativeID: true, pointerEvents: true, removeClippedSubviews: true, + role: true, borderRadius: true, borderColor: {process: require('../StyleSheet/processColor').default}, borderCurve: true, diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/attributedstring/CMakeLists.txt index b5399e44e48d..cc34a3fe959a 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/CMakeLists.txt @@ -26,6 +26,7 @@ target_link_libraries(react_render_attributedstring glog glog_init react_debug + rrc_view react_render_core react_render_debug react_render_graphics diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp index 2112a70a3d20..dfeaafef0d2c 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp @@ -101,6 +101,7 @@ void TextAttributes::apply(TextAttributes textAttributes) { accessibilityRole = textAttributes.accessibilityRole.has_value() ? textAttributes.accessibilityRole : accessibilityRole; + role = textAttributes.role.has_value() ? textAttributes.role : role; } #pragma mark - Operators @@ -126,6 +127,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const { isHighlighted, layoutDirection, accessibilityRole, + role, textTransform) == std::tie( rhs.foregroundColor, @@ -147,6 +149,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const { rhs.isHighlighted, rhs.layoutDirection, rhs.accessibilityRole, + rhs.role, rhs.textTransform) && floatEquality(opacity, rhs.opacity) && floatEquality(fontSize, rhs.fontSize) && @@ -215,6 +218,7 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const { debugStringConvertibleItem("isHighlighted", isHighlighted), debugStringConvertibleItem("layoutDirection", layoutDirection), debugStringConvertibleItem("accessibilityRole", accessibilityRole), + debugStringConvertibleItem("role", role), }; } #endif diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h index fd552ca78178..a681e9b48371 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -80,6 +81,7 @@ class TextAttributes : public DebugStringConvertible { // construction. std::optional layoutDirection{}; std::optional accessibilityRole{}; + std::optional role{}; #pragma mark - Operations @@ -131,7 +133,8 @@ struct hash { textAttributes.textShadowColor, textAttributes.isHighlighted, textAttributes.layoutDirection, - textAttributes.accessibilityRole); + textAttributes.accessibilityRole, + textAttributes.role); } }; } // namespace std diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 20383dd113ba..b2ae66a5b95f 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -641,150 +642,6 @@ inline std::string toString(const TextDecorationStyle &textDecorationStyle) { return "solid"; } -inline std::string toString(const AccessibilityRole &accessibilityRole) { - switch (accessibilityRole) { - case AccessibilityRole::None: - return "none"; - case AccessibilityRole::Button: - return "button"; - case AccessibilityRole::Link: - return "link"; - case AccessibilityRole::Search: - return "search"; - case AccessibilityRole::Image: - return "image"; - case AccessibilityRole::Imagebutton: - return "imagebutton"; - case AccessibilityRole::Keyboardkey: - return "keyboardkey"; - case AccessibilityRole::Text: - return "text"; - case AccessibilityRole::Adjustable: - return "adjustable"; - case AccessibilityRole::Summary: - return "summary"; - case AccessibilityRole::Header: - return "header"; - case AccessibilityRole::Alert: - return "alert"; - case AccessibilityRole::Checkbox: - return "checkbox"; - case AccessibilityRole::Combobox: - return "combobox"; - case AccessibilityRole::Menu: - return "menu"; - case AccessibilityRole::Menubar: - return "menubar"; - case AccessibilityRole::Menuitem: - return "menuitem"; - case AccessibilityRole::Progressbar: - return "progressbar"; - case AccessibilityRole::Radio: - return "radio"; - case AccessibilityRole::Radiogroup: - return "radiogroup"; - case AccessibilityRole::Scrollbar: - return "scrollbar"; - case AccessibilityRole::Spinbutton: - return "spinbutton"; - case AccessibilityRole::Switch: - return "switch"; - case AccessibilityRole::Tab: - return "tab"; - case AccessibilityRole::TabBar: - return "tabbar"; - case AccessibilityRole::Tablist: - return "tablist"; - case AccessibilityRole::Timer: - return "timer"; - case AccessibilityRole::Toolbar: - return "toolbar"; - } - - LOG(ERROR) << "Unsupported AccessibilityRole value"; - react_native_expect(false); - // sane default for prod - return "none"; -} - -inline void fromRawValue( - const PropsParserContext &context, - const RawValue &value, - AccessibilityRole &result) { - react_native_expect(value.hasType()); - if (value.hasType()) { - auto string = (std::string)value; - if (string == "none") { - result = AccessibilityRole::None; - } else if (string == "button" || string == "togglebutton") { - result = AccessibilityRole::Button; - } else if (string == "link") { - result = AccessibilityRole::Link; - } else if (string == "search") { - result = AccessibilityRole::Search; - } else if (string == "image") { - result = AccessibilityRole::Image; - } else if (string == "imagebutton") { - result = AccessibilityRole::Imagebutton; - } else if (string == "keyboardkey") { - result = AccessibilityRole::Keyboardkey; - } else if (string == "text") { - result = AccessibilityRole::Text; - } else if (string == "adjustable") { - result = AccessibilityRole::Adjustable; - } else if (string == "summary") { - result = AccessibilityRole::Summary; - } else if (string == "header") { - result = AccessibilityRole::Header; - } else if (string == "alert") { - result = AccessibilityRole::Alert; - } else if (string == "checkbox") { - result = AccessibilityRole::Checkbox; - } else if (string == "combobox") { - result = AccessibilityRole::Combobox; - } else if (string == "menu") { - result = AccessibilityRole::Menu; - } else if (string == "menubar") { - result = AccessibilityRole::Menubar; - } else if (string == "menuitem") { - result = AccessibilityRole::Menuitem; - } else if (string == "progressbar") { - result = AccessibilityRole::Progressbar; - } else if (string == "radio") { - result = AccessibilityRole::Radio; - } else if (string == "radiogroup") { - result = AccessibilityRole::Radiogroup; - } else if (string == "scrollbar") { - result = AccessibilityRole::Scrollbar; - } else if (string == "spinbutton") { - result = AccessibilityRole::Spinbutton; - } else if (string == "switch") { - result = AccessibilityRole::Switch; - } else if (string == "tab") { - result = AccessibilityRole::Tab; - } else if (string == "tabbar") { - result = AccessibilityRole::TabBar; - } else if (string == "tablist") { - result = AccessibilityRole::Tablist; - } else if (string == "timer") { - result = AccessibilityRole::Timer; - } else if (string == "toolbar") { - result = AccessibilityRole::Toolbar; - } else { - LOG(ERROR) << "Unsupported AccessibilityRole value: " << string; - react_native_expect(false); - // sane default for prod - result = AccessibilityRole::None; - } - return; - } - - LOG(ERROR) << "Unsupported AccessibilityRole type"; - react_native_expect(false); - // sane default for prod - result = AccessibilityRole::None; -} - inline std::string toString(const HyphenationFrequency &hyphenationFrequency) { switch (hyphenationFrequency) { case HyphenationFrequency::None: diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h index 75c5ce77f0d9..30a6e002dafd 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h @@ -105,37 +105,6 @@ enum class TextDecorationLineType { enum class TextDecorationStyle { Solid, Double, Dotted, Dashed }; -enum class AccessibilityRole { - None, - Button, - Link, - Search, - Image, - Imagebutton, - Keyboardkey, - Text, - Adjustable, - Summary, - Header, - Alert, - Checkbox, - Combobox, - Menu, - Menubar, - Menuitem, - Progressbar, - Radio, - Radiogroup, - Scrollbar, - Spinbutton, - Switch, - Tab, - TabBar, - Tablist, - Timer, - Toolbar, -}; - enum class TextTransform { None, Uppercase, @@ -223,13 +192,6 @@ struct hash { } }; -template <> -struct hash { - size_t operator()(const facebook::react::AccessibilityRole &v) const { - return hash()(static_cast(v)); - } -}; - template <> struct hash { size_t operator()(const facebook::react::TextTransform &v) const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp index d83b9f73188d..1d54aaf6829d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp @@ -183,6 +183,13 @@ static TextAttributes convertRawProp( sourceTextAttributes.accessibilityRole, defaultTextAttributes.accessibilityRole); + textAttributes.role = convertRawProp( + context, + rawProps, + "role", + sourceTextAttributes.role, + defaultTextAttributes.role); + // Color (accessed in this order by ViewProps) textAttributes.opacity = convertRawProp( context, @@ -293,6 +300,7 @@ void BaseTextProps::setProp( textAttributes, accessibilityRole, "accessibilityRole"); + REBUILD_FIELD_SWITCH_CASE(defaults, value, textAttributes, role, "role"); REBUILD_FIELD_SWITCH_CASE( defaults, value, textAttributes, opacity, "opacity"); REBUILD_FIELD_SWITCH_CASE( diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h index 6e934df39f51..108216ae6521 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h @@ -136,4 +136,131 @@ enum class AccessibilityLiveRegion : uint8_t { Assertive, }; +enum class AccessibilityRole { + None, + Button, + Dropdownlist, + Togglebutton, + Link, + Search, + Image, + Keyboardkey, + Text, + Adjustable, + Imagebutton, + Header, + Summary, + Alert, + Checkbox, + Combobox, + Menu, + Menubar, + Menuitem, + Progressbar, + Radio, + Radiogroup, + Scrollbar, + Spinbutton, + Switch, + Tab, + Tabbar, + Tablist, + Timer, + List, + Toolbar, + Grid, + Pager, + Scrollview, + Horizontalscrollview, + Viewgroup, + Webview, + Drawerlayout, + Slidingdrawer, + Iconmenu, +}; + +enum class Role { + Alert, + Alertdialog, + Application, + Article, + Banner, + Button, + Cell, + Checkbox, + Columnheader, + Combobox, + Complementary, + Contentinfo, + Definition, + Dialog, + Directory, + Document, + Feed, + Figure, + Form, + Grid, + Group, + Heading, + Img, + Link, + List, + Listitem, + Log, + Main, + Marquee, + Math, + Menu, + Menubar, + Menuitem, + Meter, + Navigation, + None, + Note, + Option, + Presentation, + Progressbar, + Radio, + Radiogroup, + Region, + Row, + Rowgroup, + Rowheader, + Scrollbar, + Searchbox, + Separator, + Slider, + Spinbutton, + Status, + Summary, + Switch, + Tab, + Table, + Tablist, + Tabpanel, + Term, + Timer, + Toolbar, + Tooltip, + Tree, + Treegrid, + Treeitem, +}; + } // namespace facebook::react + +namespace std { +template <> +struct hash { + size_t operator()(const facebook::react::AccessibilityRole &v) const { + return hash()(static_cast(v)); + } +}; + +template <> +struct hash { + size_t operator()(const facebook::react::Role &v) const { + return hash()(static_cast(v)); + } +}; +} // namespace std diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp index 8861f69c95a3..ad6cf1ce83d8 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp @@ -171,6 +171,14 @@ AccessibilityProps::AccessibilityProps( "importantForAccessibility", sourceProps.importantForAccessibility, ImportantForAccessibility::Auto)), + role( + CoreFeatures::enablePropIteratorSetter ? sourceProps.role + : convertRawProp( + context, + rawProps, + "role", + sourceProps.role, + {})), testId( CoreFeatures::enablePropIteratorSetter ? sourceProps.testId : convertRawProp( @@ -227,6 +235,7 @@ void AccessibilityProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityEscape); RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityAction); RAW_SET_PROP_SWITCH_CASE_BASIC(importantForAccessibility); + RAW_SET_PROP_SWITCH_CASE_BASIC(role); RAW_SET_PROP_SWITCH_CASE(testId, "testID"); case CONSTEXPR_RAW_PROPS_KEY_HASH("accessibilityRole"): { AccessibilityTraits traits = AccessibilityTraits::None; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h index 87f60e7b14ce..4b6cc6cf6bef 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h @@ -57,6 +57,7 @@ class AccessibilityProps { bool onAccessibilityAction{}; ImportantForAccessibility importantForAccessibility{ ImportantForAccessibility::Auto}; + Role role{Role::None}; std::string testId{""}; #pragma mark - DebugStringConvertible diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h index cc918cb9e2c8..9ee16674181e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h @@ -298,4 +298,486 @@ inline void fromRawValue( } } +inline std::string toString(const AccessibilityRole &accessibilityRole) { + switch (accessibilityRole) { + case AccessibilityRole::None: + return "none"; + case AccessibilityRole::Button: + return "button"; + case AccessibilityRole::Dropdownlist: + return "dropdownlist"; + case AccessibilityRole::Togglebutton: + return "togglebutton"; + case AccessibilityRole::Link: + return "link"; + case AccessibilityRole::Search: + return "search"; + case AccessibilityRole::Image: + return "image"; + case AccessibilityRole::Keyboardkey: + return "keyboardkey"; + case AccessibilityRole::Text: + return "text"; + case AccessibilityRole::Adjustable: + return "adjustable"; + case AccessibilityRole::Imagebutton: + return "imagebutton"; + case AccessibilityRole::Header: + return "header"; + case AccessibilityRole::Summary: + return "summary"; + case AccessibilityRole::Alert: + return "alert"; + case AccessibilityRole::Checkbox: + return "checkbox"; + case AccessibilityRole::Combobox: + return "combobox"; + case AccessibilityRole::Menu: + return "menu"; + case AccessibilityRole::Menubar: + return "menubar"; + case AccessibilityRole::Menuitem: + return "menuitem"; + case AccessibilityRole::Progressbar: + return "progressbar"; + case AccessibilityRole::Radio: + return "radio"; + case AccessibilityRole::Radiogroup: + return "radiogroup"; + case AccessibilityRole::Scrollbar: + return "scrollbar"; + case AccessibilityRole::Spinbutton: + return "spinbutton"; + case AccessibilityRole::Switch: + return "switch"; + case AccessibilityRole::Tab: + return "tab"; + case AccessibilityRole::Tabbar: + return "tabbar"; + case AccessibilityRole::Tablist: + return "tablist"; + case AccessibilityRole::Timer: + return "timer"; + case AccessibilityRole::List: + return "timer"; + case AccessibilityRole::Toolbar: + return "toolbar"; + case AccessibilityRole::Grid: + return "grid"; + case AccessibilityRole::Pager: + return "pager"; + case AccessibilityRole::Scrollview: + return "scrollview"; + case AccessibilityRole::Horizontalscrollview: + return "horizontalscrollview"; + case AccessibilityRole::Viewgroup: + return "viewgroup"; + case AccessibilityRole::Webview: + return "webview"; + case AccessibilityRole::Drawerlayout: + return "drawerlayout"; + case AccessibilityRole::Slidingdrawer: + return "slidingdrawer"; + case AccessibilityRole::Iconmenu: + return "iconmenu"; + } + + LOG(ERROR) << "Unsupported AccessibilityRole value"; + react_native_expect(false); + // sane default for prod + return "none"; +} + +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + AccessibilityRole &result) { + react_native_expect(value.hasType()); + if (value.hasType()) { + auto string = (std::string)value; + if (string == "none") { + result = AccessibilityRole::None; + } else if (string == "button") { + result = AccessibilityRole::Button; + } else if (string == "dropdownlist") { + result = AccessibilityRole::Dropdownlist; + } else if (string == "togglebutton") { + result = AccessibilityRole::Togglebutton; + } else if (string == "link") { + result = AccessibilityRole::Link; + } else if (string == "search") { + result = AccessibilityRole::Search; + } else if (string == "image") { + result = AccessibilityRole::Image; + } else if (string == "keyboardkey") { + result = AccessibilityRole::Keyboardkey; + } else if (string == "text") { + result = AccessibilityRole::Text; + } else if (string == "adjustable") { + result = AccessibilityRole::Adjustable; + } else if (string == "imagebutton") { + result = AccessibilityRole::Imagebutton; + } else if (string == "header") { + result = AccessibilityRole::Header; + } else if (string == "summary") { + result = AccessibilityRole::Summary; + } else if (string == "alert") { + result = AccessibilityRole::Alert; + } else if (string == "checkbox") { + result = AccessibilityRole::Checkbox; + } else if (string == "combobox") { + result = AccessibilityRole::Combobox; + } else if (string == "menu") { + result = AccessibilityRole::Menu; + } else if (string == "menubar") { + result = AccessibilityRole::Menubar; + } else if (string == "menuitem") { + result = AccessibilityRole::Menuitem; + } else if (string == "progressbar") { + result = AccessibilityRole::Progressbar; + } else if (string == "radio") { + result = AccessibilityRole::Radio; + } else if (string == "radiogroup") { + result = AccessibilityRole::Radiogroup; + } else if (string == "scrollbar") { + result = AccessibilityRole::Scrollbar; + } else if (string == "spinbutton") { + result = AccessibilityRole::Spinbutton; + } else if (string == "switch") { + result = AccessibilityRole::Switch; + } else if (string == "tab") { + result = AccessibilityRole::Tab; + } else if (string == "tabbar") { + result = AccessibilityRole::Tabbar; + } else if (string == "tablist") { + result = AccessibilityRole::Tablist; + } else if (string == "timer") { + result = AccessibilityRole::Timer; + } else if (string == "toolbar") { + result = AccessibilityRole::Toolbar; + } else if (string == "grid") { + result = AccessibilityRole::Grid; + } else if (string == "pager") { + result = AccessibilityRole::Pager; + } else if (string == "scrollview") { + result = AccessibilityRole::Scrollview; + } else if (string == "horizontalscrollview") { + result = AccessibilityRole::Horizontalscrollview; + } else if (string == "viewgroup") { + result = AccessibilityRole::Viewgroup; + } else if (string == "webview") { + result = AccessibilityRole::Webview; + } else if (string == "drawerlayout") { + result = AccessibilityRole::Drawerlayout; + } else if (string == "slidingdrawer") { + result = AccessibilityRole::Slidingdrawer; + } else if (string == "iconmenu") { + result = AccessibilityRole::Iconmenu; + } else { + LOG(ERROR) << "Unsupported AccessibilityRole value: " << string; + react_native_expect(false); + // sane default for prod + result = AccessibilityRole::None; + } + return; + } + + LOG(ERROR) << "Unsupported AccessibilityRole type"; + react_native_expect(false); + // sane default for prod + result = AccessibilityRole::None; +} + +inline std::string toString(const Role &role) { + switch (role) { + case Role::Alert: + return "alert"; + case Role::Alertdialog: + return "alertdialog"; + case Role::Application: + return "application"; + case Role::Article: + return "article"; + case Role::Banner: + return "banner"; + case Role::Button: + return "button"; + case Role::Cell: + return "cell"; + case Role::Checkbox: + return "checkbox"; + case Role::Columnheader: + return "columnheader"; + case Role::Combobox: + return "combobox"; + case Role::Complementary: + return "complementary"; + case Role::Contentinfo: + return "contentinfo"; + case Role::Definition: + return "definition"; + case Role::Dialog: + return "dialog"; + case Role::Directory: + return "directory"; + case Role::Document: + return "document"; + case Role::Feed: + return "feed"; + case Role::Figure: + return "figure"; + case Role::Form: + return "form"; + case Role::Grid: + return "grid"; + case Role::Group: + return "group"; + case Role::Heading: + return "heading"; + case Role::Img: + return "img"; + case Role::Link: + return "link"; + case Role::List: + return "list"; + case Role::Listitem: + return "listitem"; + case Role::Log: + return "log"; + case Role::Main: + return "main"; + case Role::Marquee: + return "marquee"; + case Role::Math: + return "math"; + case Role::Menu: + return "menu"; + case Role::Menubar: + return "menubar"; + case Role::Menuitem: + return "menuitem"; + case Role::Meter: + return "meter"; + case Role::Navigation: + return "navigation"; + case Role::None: + return "none"; + case Role::Note: + return "note"; + case Role::Option: + return "option"; + case Role::Presentation: + return "presentation"; + case Role::Progressbar: + return "progressbar"; + case Role::Radio: + return "radio"; + case Role::Radiogroup: + return "radiogroup"; + case Role::Region: + return "region"; + case Role::Row: + return "row"; + case Role::Rowgroup: + return "rowgroup"; + case Role::Rowheader: + return "rowheader"; + case Role::Scrollbar: + return "scrollbar"; + case Role::Searchbox: + return "searchbox"; + case Role::Separator: + return "separator"; + case Role::Slider: + return "slider"; + case Role::Spinbutton: + return "spinbutton"; + case Role::Status: + return "status"; + case Role::Summary: + return "summary"; + case Role::Switch: + return "switch"; + case Role::Tab: + return "tab"; + case Role::Table: + return "table"; + case Role::Tablist: + return "tablist"; + case Role::Tabpanel: + return "tabpanel"; + case Role::Term: + return "term"; + case Role::Timer: + return "timer"; + case Role::Toolbar: + return "toolbar"; + case Role::Tooltip: + return "tooltip"; + case Role::Tree: + return "tree"; + case Role::Treegrid: + return "treegrid"; + case Role::Treeitem: + return "treeitem"; + } + + LOG(ERROR) << "Unsupported Role value"; + react_native_expect(false); + // sane default for prod + return "none"; +} + +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + Role &result) { + react_native_expect(value.hasType()); + if (value.hasType()) { + auto string = (std::string)value; + if (string == "alert") { + result = Role::Alert; + } else if (string == "alertdialog") { + result = Role::Alertdialog; + } else if (string == "application") { + result = Role::Application; + } else if (string == "article") { + result = Role::Article; + } else if (string == "banner") { + result = Role::Banner; + } else if (string == "button") { + result = Role::Button; + } else if (string == "cell") { + result = Role::Cell; + } else if (string == "checkbox") { + result = Role::Checkbox; + } else if (string == "columnheader") { + result = Role::Columnheader; + } else if (string == "combobox") { + result = Role::Combobox; + } else if (string == "complementary") { + result = Role::Complementary; + } else if (string == "contentinfo") { + result = Role::Contentinfo; + } else if (string == "definition") { + result = Role::Definition; + } else if (string == "dialog") { + result = Role::Dialog; + } else if (string == "directory") { + result = Role::Directory; + } else if (string == "document") { + result = Role::Document; + } else if (string == "feed") { + result = Role::Feed; + } else if (string == "figure") { + result = Role::Figure; + } else if (string == "form") { + result = Role::Form; + } else if (string == "grid") { + result = Role::Grid; + } else if (string == "group") { + result = Role::Group; + } else if (string == "heading") { + result = Role::Heading; + } else if (string == "img") { + result = Role::Img; + } else if (string == "link") { + result = Role::Link; + } else if (string == "list") { + result = Role::List; + } else if (string == "listitem") { + result = Role::Listitem; + } else if (string == "log") { + result = Role::Log; + } else if (string == "main") { + result = Role::Main; + } else if (string == "marquee") { + result = Role::Marquee; + } else if (string == "math") { + result = Role::Math; + } else if (string == "menu") { + result = Role::Menu; + } else if (string == "menubar") { + result = Role::Menubar; + } else if (string == "menuitem") { + result = Role::Menuitem; + } else if (string == "meter") { + result = Role::Meter; + } else if (string == "navigation") { + result = Role::Navigation; + } else if (string == "none") { + result = Role::None; + } else if (string == "note") { + result = Role::Note; + } else if (string == "option") { + result = Role::Option; + } else if (string == "presentation") { + result = Role::Presentation; + } else if (string == "progressbar") { + result = Role::Progressbar; + } else if (string == "radio") { + result = Role::Radio; + } else if (string == "radiogroup") { + result = Role::Radiogroup; + } else if (string == "region") { + result = Role::Region; + } else if (string == "row") { + result = Role::Row; + } else if (string == "rowgroup") { + result = Role::Rowgroup; + } else if (string == "rowheader") { + result = Role::Rowheader; + } else if (string == "scrollbar") { + result = Role::Scrollbar; + } else if (string == "searchbox") { + result = Role::Searchbox; + } else if (string == "separator") { + result = Role::Separator; + } else if (string == "slider") { + result = Role::Slider; + } else if (string == "spinbutton") { + result = Role::Spinbutton; + } else if (string == "status") { + result = Role::Status; + } else if (string == "summary") { + result = Role::Summary; + } else if (string == "switch") { + result = Role::Switch; + } else if (string == "tab") { + result = Role::Tab; + } else if (string == "table") { + result = Role::Table; + } else if (string == "tablist") { + result = Role::Tablist; + } else if (string == "tabpanel") { + result = Role::Tabpanel; + } else if (string == "term") { + result = Role::Term; + } else if (string == "timer") { + result = Role::Timer; + } else if (string == "toolbar") { + result = Role::Toolbar; + } else if (string == "tooltip") { + result = Role::Tooltip; + } else if (string == "tree") { + result = Role::Tree; + } else if (string == "treegrid") { + result = Role::Treegrid; + } else if (string == "treeitem") { + result = Role::Treeitem; + } else { + LOG(ERROR) << "Unsupported Role value: " << string; + react_native_expect(false); + // sane default for prod + result = Role::None; + } + return; + } + + LOG(ERROR) << "Unsupported Role type"; + react_native_expect(false); + // sane default for prod + result = Role::None; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index dca3ab892c8d..68388d1c8398 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -297,6 +297,12 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex case AccessibilityRole::Button: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("button"); break; + case AccessibilityRole::Dropdownlist: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("dropdownlist"); + break; + case AccessibilityRole::Togglebutton: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("togglebutton"); + break; case AccessibilityRole::Link: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("link"); break; @@ -306,9 +312,6 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex case AccessibilityRole::Image: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("image"); break; - case AccessibilityRole::Imagebutton: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("imagebutton"); - break; case AccessibilityRole::Keyboardkey: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("keyboardkey"); break; @@ -318,12 +321,15 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex case AccessibilityRole::Adjustable: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("adjustable"); break; - case AccessibilityRole::Summary: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("summary"); + case AccessibilityRole::Imagebutton: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("imagebutton"); break; case AccessibilityRole::Header: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("header"); break; + case AccessibilityRole::Summary: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("summary"); + break; case AccessibilityRole::Alert: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("alert"); break; @@ -363,7 +369,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex case AccessibilityRole::Tab: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tab"); break; - case AccessibilityRole::TabBar: + case AccessibilityRole::Tabbar: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tabbar"); break; case AccessibilityRole::Tablist: @@ -372,9 +378,39 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex case AccessibilityRole::Timer: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("timer"); break; + case AccessibilityRole::List: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("list"); + break; case AccessibilityRole::Toolbar: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("toolbar"); break; + case AccessibilityRole::Grid: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("grid"); + break; + case AccessibilityRole::Pager: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("pager"); + break; + case AccessibilityRole::Scrollview: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("scrollview"); + break; + case AccessibilityRole::Horizontalscrollview: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("horizontalscrollview"); + break; + case AccessibilityRole::Viewgroup: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("viewgroup"); + break; + case AccessibilityRole::Webview: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("webview"); + break; + case AccessibilityRole::Drawerlayout: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("drawerlayout"); + break; + case AccessibilityRole::Slidingdrawer: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("slidingdrawer"); + break; + case AccessibilityRole::Iconmenu: + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("iconmenu"); + break; }; } From d902660fcc777635374dc2bb95780d9c31cff3c7 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 8 May 2023 15:07:03 -0700 Subject: [PATCH 2/4] Native ARIA Roles: Android Paper + Fabric, Commit 1 Summary: Add separate "Role" enum, string parsing functions, and view tag for Role (with precedence over the existing AccessibilityRole) on the Java side. Differential Revision: https://internalfb.com/D45431381 fbshipit-source-id: 370e967ce4cf6624c5502c32bf6eba541049f4f7 --- .../react/uimanager/BaseViewManager.java | 16 +- .../uimanager/BaseViewManagerAdapter.java | 3 + .../uimanager/BaseViewManagerDelegate.java | 3 + .../uimanager/BaseViewManagerInterface.java | 2 + .../uimanager/ReactAccessibilityDelegate.java | 156 +++++++++++++++++- .../facebook/react/uimanager/ViewProps.java | 1 + .../react/views/drawer/ReactDrawerLayout.java | 4 +- .../ReactScrollViewAccessibilityDelegate.java | 5 +- .../views/text/ReactBaseTextShadowNode.java | 25 ++- .../react/views/text/TextAttributeProps.java | 36 ++-- .../react/views/text/TextLayoutManager.java | 8 +- .../text/TextLayoutManagerMapBuffer.java | 8 +- .../views/view/ReactMapBufferPropSetter.kt | 10 ++ .../main/res/views/uimanager/values/ids.xml | 3 + .../react/uimanager/BaseViewManagerTest.java | 7 + .../renderer/attributedstring/conversions.h | 4 + .../view/AccessibilityPropsMapBuffer.cpp | 4 + .../view/AccessibilityPropsMapBuffer.h | 5 + 18 files changed, 274 insertions(+), 26 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index b15a2984f20c..b8f10af4da9e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -26,6 +26,7 @@ import com.facebook.react.common.MapBuilder; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.events.PointerEventHelper; import com.facebook.react.uimanager.util.ReactFindViewUtil; @@ -234,9 +235,10 @@ public void setAccessibilityHint(@NonNull T view, @Nullable String accessibility @ReactProp(name = ViewProps.ACCESSIBILITY_ROLE) public void setAccessibilityRole(@NonNull T view, @Nullable String accessibilityRole) { if (accessibilityRole == null) { - return; + view.setTag(R.id.accessibility_role, null); + } else { + view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole)); } - view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole)); } @Override @@ -380,6 +382,16 @@ public void setImportantForAccessibility( } } + @Override + @ReactProp(name = ViewProps.ROLE) + public void setRole(@NonNull T view, @Nullable String role) { + if (role == null) { + view.setTag(R.id.role, null); + } else { + view.setTag(R.id.role, Role.fromValue(role)); + } + } + @Override @Deprecated @ReactProp(name = ViewProps.ROTATION) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java index afd33e22eb1b..3e3d7c8bdc33 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java @@ -70,6 +70,9 @@ public void setShadowColor(@NonNull T view, int shadowColor) {} public void setImportantForAccessibility( @NonNull T view, @Nullable String importantForAccessibility) {} + @Override + public void setRole(@NonNull T view, @Nullable String role) {} + @Override public void setNativeId(@NonNull T view, String nativeId) {} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java index bb809381494e..e59a1c4da3b8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java @@ -89,6 +89,9 @@ public void setProperty(T view, String propName, @Nullable Object value) { case ViewProps.IMPORTANT_FOR_ACCESSIBILITY: mViewManager.setImportantForAccessibility(view, (String) value); break; + case ViewProps.ROLE: + mViewManager.setRole(view, (String) value); + break; case ViewProps.NATIVE_ID: mViewManager.setNativeId(view, (String) value); break; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java index 0a6a9a561d5c..5887ff5ba315 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java @@ -52,6 +52,8 @@ public interface BaseViewManagerInterface { void setImportantForAccessibility(T view, @Nullable String importantForAccessibility); + void setRole(T view, @Nullable String role); + void setNativeId(T view, @Nullable String nativeId); void setAccessibilityLabelledBy(T view, @Nullable Dynamic nativeId); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java index 626232ed0f39..531e4591ba6a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java @@ -96,6 +96,87 @@ private void scheduleAccessibilityEventSender(View host) { mHandler.sendMessageDelayed(msg, TIMEOUT_SEND_ACCESSIBILITY_EVENT); } + /** + * An ARIA Role representable by View's `role` prop. Ordinals should be kept in sync with + * `facebook::react::Role`. + */ + public enum Role { + ALERT, + ALERTDIALOG, + APPLICATION, + ARTICLE, + BANNER, + BUTTON, + CELL, + CHECKBOX, + COLUMNHEADER, + COMBOBOX, + COMPLEMENTARY, + CONTENTINFO, + DEFINITION, + DIALOG, + DIRECTORY, + DOCUMENT, + FEED, + FIGURE, + FORM, + GRID, + GROUP, + HEADING, + IMG, + LINK, + LIST, + LISTITEM, + LOG, + MAIN, + MARQUEE, + MATH, + MENU, + MENUBAR, + MENUITEM, + METER, + NAVIGATION, + NONE, + NOTE, + OPTION, + PRESENTATION, + PROGRESSBAR, + RADIO, + RADIOGROUP, + REGION, + ROW, + ROWGROUP, + ROWHEADER, + SCROLLBAR, + SEARCHBOX, + SEPARATOR, + SLIDER, + SPINBUTTON, + STATUS, + SUMMARY, + SWITCH, + TAB, + TABLE, + TABLIST, + TABPANEL, + TERM, + TIMER, + TOOLBAR, + TOOLTIP, + TREE, + TREEGRID, + TREEITEM; + + public static @Nullable Role fromValue(@Nullable String value) { + for (Role role : Role.values()) { + if (role.name().equalsIgnoreCase(value)) { + return role; + } + } + return null; + } + } + /** * These roles are defined by Google's TalkBack screen reader, and this list should be kept up to * date with their implementation. Details can be seen in their source code here: @@ -221,6 +302,75 @@ public static AccessibilityRole fromValue(@Nullable String value) { } throw new IllegalArgumentException("Invalid accessibility role value: " + value); } + + public static @Nullable AccessibilityRole fromRole(Role role) { + switch (role) { + case ALERT: + return AccessibilityRole.ALERT; + case BUTTON: + return AccessibilityRole.BUTTON; + case CHECKBOX: + return AccessibilityRole.CHECKBOX; + case COMBOBOX: + return AccessibilityRole.COMBOBOX; + case GRID: + return AccessibilityRole.GRID; + case HEADING: + return AccessibilityRole.HEADER; + case IMG: + return AccessibilityRole.IMAGE; + case LINK: + return AccessibilityRole.LINK; + case LIST: + return AccessibilityRole.LIST; + case MENU: + return AccessibilityRole.MENU; + case MENUBAR: + return AccessibilityRole.MENUBAR; + case MENUITEM: + return AccessibilityRole.MENUITEM; + case NONE: + return AccessibilityRole.NONE; + case PROGRESSBAR: + return AccessibilityRole.PROGRESSBAR; + case RADIO: + return AccessibilityRole.RADIO; + case RADIOGROUP: + return AccessibilityRole.RADIOGROUP; + case SCROLLBAR: + return AccessibilityRole.SCROLLBAR; + case SEARCHBOX: + return AccessibilityRole.SEARCH; + case SLIDER: + return AccessibilityRole.ADJUSTABLE; + case SPINBUTTON: + return AccessibilityRole.SPINBUTTON; + case SUMMARY: + return AccessibilityRole.SUMMARY; + case SWITCH: + return AccessibilityRole.SWITCH; + case TAB: + return AccessibilityRole.TAB; + case TABLIST: + return AccessibilityRole.TABLIST; + case TIMER: + return AccessibilityRole.TIMER; + case TOOLBAR: + return AccessibilityRole.TOOLBAR; + default: + // No mapping from ARIA role to AccessibilityRole + return null; + } + } + + public static @Nullable AccessibilityRole fromViewTag(View view) { + Role role = (Role) view.getTag(R.id.role); + if (role != null) { + return AccessibilityRole.fromRole(role); + } else { + return (AccessibilityRole) view.getTag(R.id.accessibility_role); + } + } } private final HashMap mAccessibilityActionsMap; @@ -267,8 +417,7 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo ? AccessibilityNodeInfoCompat.ACTION_COLLAPSE : AccessibilityNodeInfoCompat.ACTION_EXPAND); } - final AccessibilityRole accessibilityRole = - (AccessibilityRole) host.getTag(R.id.accessibility_role); + final AccessibilityRole accessibilityRole = AccessibilityRole.fromViewTag(host); final String accessibilityHint = (String) host.getTag(R.id.accessibility_hint); if (accessibilityRole != null) { setRole(info, accessibilityRole, host.getContext()); @@ -551,7 +700,8 @@ public static void setDelegate( || view.getTag(R.id.accessibility_actions) != null || view.getTag(R.id.react_test_id) != null || view.getTag(R.id.accessibility_collection_item) != null - || view.getTag(R.id.accessibility_links) != null)) { + || view.getTag(R.id.accessibility_links) != null + || view.getTag(R.id.role) != null)) { ViewCompat.setAccessibilityDelegate( view, new ReactAccessibilityDelegate(view, originalFocus, originalImportantForAccessibility)); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index 3f76fa7dd3d9..cc9f7178e659 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -166,6 +166,7 @@ public class ViewProps { public static final String ACCESSIBILITY_VALUE = "accessibilityValue"; public static final String ACCESSIBILITY_LABELLED_BY = "accessibilityLabelledBy"; public static final String IMPORTANT_FOR_ACCESSIBILITY = "importantForAccessibility"; + public static final String ROLE = "role"; // DEPRECATED public static final String ROTATION = "rotation"; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java index 40392887a1c7..d5d3931b00b2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java @@ -42,8 +42,8 @@ public ReactDrawerLayout(ReactContext reactContext) { public void onInitializeAccessibilityNodeInfo( View host, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); - final AccessibilityRole accessibilityRole = - (AccessibilityRole) host.getTag(R.id.accessibility_role); + + final AccessibilityRole accessibilityRole = AccessibilityRole.fromViewTag(host); if (accessibilityRole != null) { info.setClassName(AccessibilityRole.getValue(accessibilityRole)); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewAccessibilityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewAccessibilityDelegate.java index 6cf22db90c04..745b85fceb7a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewAccessibilityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewAccessibilityDelegate.java @@ -17,6 +17,7 @@ import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.ReactAccessibilityDelegate; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; public class ReactScrollViewAccessibilityDelegate extends AccessibilityDelegateCompat { private final String TAG = ReactScrollViewAccessibilityDelegate.class.getSimpleName(); @@ -122,8 +123,8 @@ private void onInitializeAccessibilityEventInternal(View view, AccessibilityEven private void onInitializeAccessibilityNodeInfoInternal( View view, AccessibilityNodeInfoCompat info) { - final ReactAccessibilityDelegate.AccessibilityRole accessibilityRole = - (ReactAccessibilityDelegate.AccessibilityRole) view.getTag(R.id.accessibility_role); + + final AccessibilityRole accessibilityRole = AccessibilityRole.fromViewTag(view); if (accessibilityRole != null) { ReactAccessibilityDelegate.setRole(info, accessibilityRole, view.getContext()); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index 12c1c5e05728..4fd3a8165f42 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -26,6 +26,8 @@ import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.uimanager.ReactShadowNode; import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.annotations.ReactProp; @@ -36,7 +38,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; /** * {@link ReactShadowNode} abstract class for spannable text nodes. @@ -180,7 +181,11 @@ private static void buildSpannedFromShadowNode( new SetSpanOperation( start, end, new ReactBackgroundColorSpan(textShadowNode.mBackgroundColor))); } - if (textShadowNode.mIsAccessibilityLink) { + boolean roleIsLink = + textShadowNode.mRole != null + ? textShadowNode.mRole == Role.LINK + : textShadowNode.mAccessibilityRole == AccessibilityRole.LINK; + if (roleIsLink) { ops.add( new SetSpanOperation(start, end, new ReactClickableSpan(textShadowNode.getReactTag()))); } @@ -325,7 +330,9 @@ protected Spannable spannedFromShadowNode( protected int mColor; protected boolean mIsBackgroundColorSet = false; protected int mBackgroundColor; - protected boolean mIsAccessibilityLink = false; + + protected @Nullable AccessibilityRole mAccessibilityRole = null; + protected @Nullable Role mRole = null; protected int mNumberOfLines = UNSET; protected int mTextAlign = Gravity.NO_GRAVITY; @@ -499,9 +506,17 @@ public void setBackgroundColor(@Nullable Integer color) { } @ReactProp(name = ViewProps.ACCESSIBILITY_ROLE) - public void setIsAccessibilityLink(@Nullable String accessibilityRole) { + public void setAccessibilityRole(@Nullable String accessibilityRole) { + if (isVirtual()) { + mAccessibilityRole = AccessibilityRole.fromValue(accessibilityRole); + markUpdated(); + } + } + + @ReactProp(name = ViewProps.ROLE) + public void setRole(@Nullable String role) { if (isVirtual()) { - mIsAccessibilityLink = Objects.equals(accessibilityRole, "link"); + mRole = Role.fromValue(role); markUpdated(); } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index bc53523c8154..eb03a50743f0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -19,7 +19,8 @@ import com.facebook.react.common.ReactConstants; import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.uimanager.PixelUtil; -import com.facebook.react.uimanager.ReactAccessibilityDelegate; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; import java.util.ArrayList; @@ -56,6 +57,8 @@ public class TextAttributeProps { public static final short TA_KEY_IS_HIGHLIGHTED = 22; public static final short TA_KEY_LAYOUT_DIRECTION = 23; public static final short TA_KEY_ACCESSIBILITY_ROLE = 24; + public static final short TA_KEY_LINE_BREAK_STRATEGY = 25; + public static final short TA_KEY_ROLE = 26; public static final int UNSET = -1; @@ -103,9 +106,8 @@ public class TextAttributeProps { protected boolean mIsLineThroughTextDecorationSet = false; protected boolean mIncludeFontPadding = true; - protected @Nullable ReactAccessibilityDelegate.AccessibilityRole mAccessibilityRole = null; - protected boolean mIsAccessibilityRoleSet = false; - protected boolean mIsAccessibilityLink = false; + protected @Nullable AccessibilityRole mAccessibilityRole = null; + protected @Nullable Role mRole = null; protected int mFontStyle = UNSET; protected int mFontWeight = UNSET; @@ -214,6 +216,9 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) { case TA_KEY_ACCESSIBILITY_ROLE: result.setAccessibilityRole(entry.getStringValue()); break; + case TA_KEY_ROLE: + result.setRole(Role.values()[entry.getIntValue()]); + break; } } @@ -254,6 +259,7 @@ public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { result.setTextTransform(getStringProp(props, PROP_TEXT_TRANSFORM)); result.setLayoutDirection(getStringProp(props, ViewProps.LAYOUT_DIRECTION)); result.setAccessibilityRole(getStringProp(props, ViewProps.ACCESSIBILITY_ROLE)); + result.setRole(getStringProp(props, ViewProps.ROLE)); return result; } @@ -618,15 +624,25 @@ private void setTextTransform(@Nullable String textTransform) { } private void setAccessibilityRole(@Nullable String accessibilityRole) { - if (accessibilityRole != null) { - mIsAccessibilityRoleSet = true; - mAccessibilityRole = - ReactAccessibilityDelegate.AccessibilityRole.fromValue(accessibilityRole); - mIsAccessibilityLink = - mAccessibilityRole.equals(ReactAccessibilityDelegate.AccessibilityRole.LINK); + if (accessibilityRole == null) { + mAccessibilityRole = null; + } else { + mAccessibilityRole = AccessibilityRole.fromValue(accessibilityRole); + } + } + + private void setRole(@Nullable String role) { + if (role == null) { + mRole = null; + } else { + mRole = Role.fromValue(role); } } + private void setRole(Role role) { + mRole = role; + } + public static int getTextBreakStrategy(@Nullable String textBreakStrategy) { int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY; if (textBreakStrategy != null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index 44195221646b..040171ed0082 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -33,6 +33,8 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; import com.facebook.yoga.YogaConstants; @@ -126,7 +128,11 @@ private static void buildSpannableFromFragment( sb.length(), new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); } else if (end >= start) { - if (textAttributes.mIsAccessibilityLink) { + boolean roleIsLink = + textAttributes.mRole != null + ? textAttributes.mRole == Role.LINK + : textAttributes.mAccessibilityRole == AccessibilityRole.LINK; + if (roleIsLink) { ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); } if (textAttributes.mIsColorSet) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java index bc0a56384b92..921670310882 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java @@ -33,6 +33,8 @@ import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.yoga.YogaConstants; import com.facebook.yoga.YogaMeasureMode; import com.facebook.yoga.YogaMeasureOutput; @@ -146,7 +148,11 @@ private static void buildSpannableFromFragment( sb.length(), new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); } else if (end >= start) { - if (textAttributes.mIsAccessibilityLink) { + boolean roleIsLink = + textAttributes.mRole != null + ? textAttributes.mRole == Role.LINK + : textAttributes.mAccessibilityRole == AccessibilityRole.LINK; + if (roleIsLink) { ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); } if (textAttributes.mIsColorSet) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt index a7effcdab175..7447e45313a5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt @@ -10,6 +10,7 @@ package com.facebook.react.views.view import android.graphics.Color import android.graphics.Rect import androidx.core.view.ViewCompat +import com.facebook.react.R import com.facebook.react.bridge.DynamicFromObject import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap @@ -17,6 +18,7 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.mapbuffer.MapBuffer import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.PointerEvents +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role object ReactMapBufferPropSetter { // ViewProps values @@ -64,6 +66,7 @@ object ReactMapBufferPropSetter { private const val VP_POINTER_OVER_CAPTURE = 44 private const val VP_BORDER_CURVES = 45 // iOS only private const val VP_FG_COLOR = 46 // iOS only? + private const val VP_ROLE = 47 // Yoga values private const val YG_BORDER_WIDTH = 100 @@ -180,6 +183,9 @@ object ReactMapBufferPropSetter { VP_IMPORTANT_FOR_ACCESSIBILITY -> { view.importantForAccessibility(entry.intValue) } + VP_ROLE -> { + view.role(entry.intValue) + } VP_NATIVE_BACKGROUND -> { viewManager.nativeBackground(view, entry.mapBufferValue) } @@ -422,6 +428,10 @@ object ReactMapBufferPropSetter { ViewCompat.setImportantForAccessibility(this, mode) } + private fun ReactViewGroup.role(value: Int) { + setTag(R.id.role, Role.values()[value]) + } + private fun ReactViewGroup.pointerEvents(value: Int) { val pointerEvents = when (value) { diff --git a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 6324b85af446..d2928f810dfb 100644 --- a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -44,4 +44,7 @@ + + + diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.java index 95642f5edd95..83e32f7287ab 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.java +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.java @@ -14,6 +14,7 @@ import com.facebook.react.bridge.JavaOnlyMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.views.view.ReactViewGroup; import com.facebook.react.views.view.ReactViewManager; import java.util.Locale; @@ -78,4 +79,10 @@ public void testAccessibilityStateSelected() { assertThat(mView.getTag(R.id.accessibility_state)).isEqualTo(accessibilityState); assertThat(mView.isSelected()).isEqualTo(true); } + + @Test + public void testRoleList() { + mViewManager.setRole(mView, "list"); + assertThat(mView.getTag(R.id.role)).isEqualTo(Role.LIST); + } } diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index b2ae66a5b95f..0d54c7e77493 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -968,6 +968,7 @@ constexpr static MapBuffer::Key TA_KEY_IS_HIGHLIGHTED = 22; constexpr static MapBuffer::Key TA_KEY_LAYOUT_DIRECTION = 23; constexpr static MapBuffer::Key TA_KEY_ACCESSIBILITY_ROLE = 24; constexpr static MapBuffer::Key TA_KEY_LINE_BREAK_STRATEGY = 25; +constexpr static MapBuffer::Key TA_KEY_ROLE = 26; // constants for ParagraphAttributes serialization constexpr static MapBuffer::Key PA_KEY_MAX_NUMBER_OF_LINES = 0; @@ -1120,6 +1121,9 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) { builder.putString( TA_KEY_ACCESSIBILITY_ROLE, toString(*textAttributes.accessibilityRole)); } + if (textAttributes.role.has_value()) { + builder.putInt(TA_KEY_ROLE, static_cast(*textAttributes.role)); + } return builder.build(); } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPropsMapBuffer.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPropsMapBuffer.cpp index c866cb23a463..a9fa5e60e2c9 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPropsMapBuffer.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPropsMapBuffer.cpp @@ -158,6 +158,10 @@ void AccessibilityProps::propsDiffMapBuffer( } builder.putInt(AP_IMPORTANT_FOR_ACCESSIBILITY, value); } + + if (oldProps.role != newProps.role) { + builder.putInt(AP_ROLE, static_cast(newProps.role)); + } } #endif diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPropsMapBuffer.h b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPropsMapBuffer.h index 37dd41eeec91..51d96ee40a07 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPropsMapBuffer.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPropsMapBuffer.h @@ -14,6 +14,9 @@ namespace facebook::react { +// TODO: "AP" (Accessibility Props) are interleaved with "VP" (View Props). +// Ordinals must be unique between them. + constexpr MapBuffer::Key AP_ACCESSIBILITY_ACTIONS = 0; constexpr MapBuffer::Key AP_ACCESSIBILITY_HINT = 1; constexpr MapBuffer::Key AP_ACCESSIBILITY_LABEL = 2; @@ -25,6 +28,8 @@ constexpr MapBuffer::Key AP_ACCESSIBILITY_VALUE = 7; constexpr MapBuffer::Key AP_ACCESSIBLE = 8; constexpr MapBuffer::Key AP_IMPORTANT_FOR_ACCESSIBILITY = 19; +constexpr MapBuffer::Key AP_ROLE = 47; + // AccessibilityAction values constexpr MapBuffer::Key ACCESSIBILITY_ACTION_NAME = 0; constexpr MapBuffer::Key ACCESSIBILITY_ACTION_LABEL = 1; From 67d7111648ed53008f99f45621e74767c8270d54 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 8 May 2023 15:07:03 -0700 Subject: [PATCH 3/4] Native ARIA Roles: iOS Paper + Fabric Differential Revision: https://internalfb.com/D45432530 fbshipit-source-id: 5244ba962f000a17a176a728227bce0c7ed2c321 --- packages/react-native/React/Views/RCTView.m | 7 +- .../react-native/React/Views/RCTViewManager.m | 132 +++++++++++++----- .../react-native/React/Views/UIView+React.h | 3 + .../react-native/React/Views/UIView+React.m | 37 +++++ .../components/view/AccessibilityProps.cpp | 39 +++--- .../view/accessibilityPropsConversions.h | 4 +- .../RCTAttributedTextUtils.h | 2 + .../RCTAttributedTextUtils.mm | 131 +---------------- 8 files changed, 175 insertions(+), 180 deletions(-) diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index dcb19bfafc25..d6f62fe906cf 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -332,7 +332,12 @@ - (NSString *)accessibilityValue } } NSMutableArray *valueComponents = [NSMutableArray new]; - NSString *roleDescription = self.accessibilityRole ? rolesAndStatesDescription[self.accessibilityRole] : nil; + + // TODO: This logic makes VoiceOver describe some AccessibilityRole which do not have a backing UIAccessibilityTrait. + // It does not run on Fabric, since ViewComponentView sets `accessibilityTraits` directly without passing + // `accessibilityRole` to this view. This behavior should be reconciled. + NSString *role = self.role ?: self.accessibilityRole; + NSString *roleDescription = role ? rolesAndStatesDescription[self.accessibilityRole] : nil; if (roleDescription) { [valueComponents addObject:roleDescription]; } diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index 2b8f2c0a33ab..9a0af4e74cec 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -26,54 +26,97 @@ @implementation RCTConvert (UIAccessibilityTraits) RCT_MULTI_ENUM_CONVERTER( UIAccessibilityTraits, (@{ - @"none" : @(UIAccessibilityTraitNone), + @"adjustable" : @(UIAccessibilityTraitAdjustable), + @"alert" : @(UIAccessibilityTraitNone), + @"alertdialog" : @(UIAccessibilityTraitNone), + @"allowsDirectInteraction" : @(UIAccessibilityTraitAllowsDirectInteraction), + @"application" : @(UIAccessibilityTraitNone), + @"article" : @(UIAccessibilityTraitNone), + @"banner" : @(UIAccessibilityTraitNone), @"button" : @(UIAccessibilityTraitButton), + @"cell" : @(UIAccessibilityTraitNone), + @"checkbox" : @(UIAccessibilityTraitNone), + @"columnheader" : @(UIAccessibilityTraitNone), + @"combobox" : @(UIAccessibilityTraitNone), + @"complementary" : @(UIAccessibilityTraitNone), + @"contentinfo" : @(UIAccessibilityTraitNone), + @"definition" : @(UIAccessibilityTraitNone), + @"dialog" : @(UIAccessibilityTraitNone), + @"directory" : @(UIAccessibilityTraitNone), + @"disabled" : @(UIAccessibilityTraitNotEnabled), + @"document" : @(UIAccessibilityTraitNone), + @"drawerlayout" : @(UIAccessibilityTraitNone), @"dropdownlist" : @(UIAccessibilityTraitNone), - @"togglebutton" : @(UIAccessibilityTraitButton), - @"link" : @(UIAccessibilityTraitLink), + @"feed" : @(UIAccessibilityTraitNone), + @"figure" : @(UIAccessibilityTraitNone), + @"form" : @(UIAccessibilityTraitNone), + @"frequentUpdates" : @(UIAccessibilityTraitUpdatesFrequently), + @"grid" : @(UIAccessibilityTraitNone), + @"group" : @(UIAccessibilityTraitNone), @"header" : @(UIAccessibilityTraitHeader), - @"search" : @(UIAccessibilityTraitSearchField), + @"heading" : @(UIAccessibilityTraitHeader), + @"horizontalscrollview" : @(UIAccessibilityTraitNone), + @"iconmenu" : @(UIAccessibilityTraitNone), @"image" : @(UIAccessibilityTraitImage), @"imagebutton" : @(UIAccessibilityTraitImage | UIAccessibilityTraitButton), - @"selected" : @(UIAccessibilityTraitSelected), - @"plays" : @(UIAccessibilityTraitPlaysSound), + @"img" : @(UIAccessibilityTraitImage), @"key" : @(UIAccessibilityTraitKeyboardKey), @"keyboardkey" : @(UIAccessibilityTraitKeyboardKey), - @"text" : @(UIAccessibilityTraitStaticText), - @"summary" : @(UIAccessibilityTraitSummaryElement), - @"disabled" : @(UIAccessibilityTraitNotEnabled), - @"frequentUpdates" : @(UIAccessibilityTraitUpdatesFrequently), - @"startsMedia" : @(UIAccessibilityTraitStartsMediaSession), - @"adjustable" : @(UIAccessibilityTraitAdjustable), - @"allowsDirectInteraction" : @(UIAccessibilityTraitAllowsDirectInteraction), - @"pageTurn" : @(UIAccessibilityTraitCausesPageTurn), - @"alert" : @(UIAccessibilityTraitNone), - @"checkbox" : @(UIAccessibilityTraitNone), - @"combobox" : @(UIAccessibilityTraitNone), + @"link" : @(UIAccessibilityTraitLink), + @"list" : @(UIAccessibilityTraitNone), + @"listitem" : @(UIAccessibilityTraitNone), + @"log" : @(UIAccessibilityTraitNone), + @"main" : @(UIAccessibilityTraitNone), + @"marquee" : @(UIAccessibilityTraitNone), + @"math" : @(UIAccessibilityTraitNone), @"menu" : @(UIAccessibilityTraitNone), @"menubar" : @(UIAccessibilityTraitNone), @"menuitem" : @(UIAccessibilityTraitNone), + @"meter" : @(UIAccessibilityTraitNone), + @"navigation" : @(UIAccessibilityTraitNone), + @"none" : @(UIAccessibilityTraitNone), + @"note" : @(UIAccessibilityTraitNone), + @"option" : @(UIAccessibilityTraitNone), + @"pager" : @(UIAccessibilityTraitNone), + @"pageTurn" : @(UIAccessibilityTraitCausesPageTurn), + @"plays" : @(UIAccessibilityTraitPlaysSound), + @"presentation" : @(UIAccessibilityTraitNone), @"progressbar" : @(UIAccessibilityTraitUpdatesFrequently), @"radio" : @(UIAccessibilityTraitNone), @"radiogroup" : @(UIAccessibilityTraitNone), + @"region" : @(UIAccessibilityTraitNone), + @"row" : @(UIAccessibilityTraitNone), + @"rowgroup" : @(UIAccessibilityTraitNone), + @"rowheader" : @(UIAccessibilityTraitNone), @"scrollbar" : @(UIAccessibilityTraitNone), + @"scrollview" : @(UIAccessibilityTraitNone), + @"search" : @(UIAccessibilityTraitSearchField), + @"searchbox" : @(UIAccessibilityTraitNone), + @"selected" : @(UIAccessibilityTraitSelected), + @"separator" : @(UIAccessibilityTraitNone), + @"slider" : @(UIAccessibilityTraitNone), + @"slidingdrawer" : @(UIAccessibilityTraitNone), @"spinbutton" : @(UIAccessibilityTraitNone), + @"startsMedia" : @(UIAccessibilityTraitStartsMediaSession), + @"status" : @(UIAccessibilityTraitNone), + @"summary" : @(UIAccessibilityTraitSummaryElement), @"switch" : @(SwitchAccessibilityTrait), @"tab" : @(UIAccessibilityTraitNone), @"tabbar" : @(UIAccessibilityTraitTabBar), + @"table" : @(UIAccessibilityTraitNone), @"tablist" : @(UIAccessibilityTraitNone), + @"tabpanel" : @(UIAccessibilityTraitNone), + @"term" : @(UIAccessibilityTraitNone), + @"text" : @(UIAccessibilityTraitStaticText), @"timer" : @(UIAccessibilityTraitNone), + @"togglebutton" : @(UIAccessibilityTraitButton), @"toolbar" : @(UIAccessibilityTraitNone), - @"pager" : @(UIAccessibilityTraitNone), - @"scrollview" : @(UIAccessibilityTraitNone), - @"horizontalscrollview" : @(UIAccessibilityTraitNone), + @"tooltip" : @(UIAccessibilityTraitNone), + @"tree" : @(UIAccessibilityTraitNone), + @"treegrid" : @(UIAccessibilityTraitNone), + @"treeitem" : @(UIAccessibilityTraitNone), @"viewgroup" : @(UIAccessibilityTraitNone), @"webview" : @(UIAccessibilityTraitNone), - @"drawerlayout" : @(UIAccessibilityTraitNone), - @"slidingdrawer" : @(UIAccessibilityTraitNone), - @"iconmenu" : @(UIAccessibilityTraitNone), - @"list" : @(UIAccessibilityTraitNone), - @"grid" : @(UIAccessibilityTraitNone), }), UIAccessibilityTraitNone, unsignedLongLongValue) @@ -182,22 +225,41 @@ - (RCTShadowView *)shadowView } RCT_CUSTOM_VIEW_PROPERTY(accessibilityRole, UIAccessibilityTraits, RCTView) +{ + UIAccessibilityTraits accessibilityRoleTraits = + json ? [RCTConvert UIAccessibilityTraits:json] : UIAccessibilityTraitNone; + if (view.reactAccessibilityElement.accessibilityRoleTraits != accessibilityRoleTraits) { + view.accessibilityRoleTraits = accessibilityRoleTraits; + view.reactAccessibilityElement.accessibilityRole = json ? [RCTConvert NSString:json] : nil; + [self updateAccessibilityTraitsForRole:view withDefaultView:defaultView]; + } +} + +RCT_CUSTOM_VIEW_PROPERTY(role, UIAccessibilityTraits, RCTView) +{ + UIAccessibilityTraits roleTraits = json ? [RCTConvert UIAccessibilityTraits:json] : UIAccessibilityTraitNone; + if (view.reactAccessibilityElement.roleTraits != roleTraits) { + view.roleTraits = roleTraits; + view.reactAccessibilityElement.role = json ? [RCTConvert NSString:json] : nil; + [self updateAccessibilityTraitsForRole:view withDefaultView:defaultView]; + } +} + +- (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTView *)defaultView { const UIAccessibilityTraits AccessibilityRolesMask = UIAccessibilityTraitNone | UIAccessibilityTraitButton | UIAccessibilityTraitLink | UIAccessibilityTraitSearchField | UIAccessibilityTraitImage | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitStaticText | UIAccessibilityTraitAdjustable | UIAccessibilityTraitHeader | UIAccessibilityTraitSummaryElement | UIAccessibilityTraitTabBar | UIAccessibilityTraitUpdatesFrequently | SwitchAccessibilityTrait; - view.reactAccessibilityElement.accessibilityTraits = - view.reactAccessibilityElement.accessibilityTraits & ~AccessibilityRolesMask; - UIAccessibilityTraits newTraits = json ? [RCTConvert UIAccessibilityTraits:json] : defaultView.accessibilityTraits; - if (newTraits != UIAccessibilityTraitNone) { - UIAccessibilityTraits maskedTraits = newTraits & AccessibilityRolesMask; - view.reactAccessibilityElement.accessibilityTraits |= maskedTraits; - } else { - NSString *role = json ? [RCTConvert NSString:json] : @""; - view.reactAccessibilityElement.accessibilityRole = role; - } + + // Clear any existing traits set for AccessibilityRole + view.reactAccessibilityElement.accessibilityTraits &= ~(AccessibilityRolesMask); + + view.reactAccessibilityElement.accessibilityTraits |= view.reactAccessibilityElement.role + ? view.reactAccessibilityElement.roleTraits + : view.reactAccessibilityElement.accessibilityRole ? view.reactAccessibilityElement.accessibilityRoleTraits + : (defaultView.accessibilityTraits & AccessibilityRolesMask); } RCT_CUSTOM_VIEW_PROPERTY(accessibilityState, NSDictionary, RCTView) diff --git a/packages/react-native/React/Views/UIView+React.h b/packages/react-native/React/Views/UIView+React.h index 21a70337d7ac..d378a8320ba1 100644 --- a/packages/react-native/React/Views/UIView+React.h +++ b/packages/react-native/React/Views/UIView+React.h @@ -117,10 +117,13 @@ * Accessibility properties */ @property (nonatomic, copy) NSString *accessibilityRole; +@property (nonatomic, copy) NSString *role; @property (nonatomic, copy) NSDictionary *accessibilityState; @property (nonatomic, copy) NSArray *accessibilityActions; @property (nonatomic, copy) NSDictionary *accessibilityValueInternal; @property (nonatomic, copy) NSString *accessibilityLanguage; +@property (nonatomic) UIAccessibilityTraits accessibilityRoleTraits; +@property (nonatomic) UIAccessibilityTraits roleTraits; /** * Used in debugging to get a description of the view hierarchy rooted at diff --git a/packages/react-native/React/Views/UIView+React.m b/packages/react-native/React/Views/UIView+React.m index 94ad951e7179..7c6c71829ff6 100644 --- a/packages/react-native/React/Views/UIView+React.m +++ b/packages/react-native/React/Views/UIView+React.m @@ -336,6 +336,16 @@ - (void)setAccessibilityRole:(NSString *)accessibilityRole objc_setAssociatedObject(self, @selector(accessibilityRole), accessibilityRole, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +- (NSString *)role +{ + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setRole:(NSString *)role +{ + objc_setAssociatedObject(self, @selector(role), role, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + - (NSDictionary *)accessibilityState { return objc_getAssociatedObject(self, _cmd); @@ -356,6 +366,33 @@ - (void)setAccessibilityValueInternal:(NSDictionary *)accessibil self, @selector(accessibilityValueInternal), accessibilityValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +- (UIAccessibilityTraits)accessibilityRoleTraits +{ + NSNumber *traitsAsNumber = objc_getAssociatedObject(self, _cmd); + return traitsAsNumber ? [traitsAsNumber unsignedLongLongValue] : UIAccessibilityTraitNone; +} + +- (void)setAccessibilityRoleTraits:(UIAccessibilityTraits)accessibilityRoleTraits +{ + objc_setAssociatedObject( + self, + @selector(accessibilityRoleTraits), + [NSNumber numberWithUnsignedLongLong:accessibilityRoleTraits], + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (UIAccessibilityTraits)roleTraits +{ + NSNumber *traitsAsNumber = objc_getAssociatedObject(self, _cmd); + return traitsAsNumber ? [traitsAsNumber unsignedLongLongValue] : UIAccessibilityTraitNone; +} + +- (void)setRoleTraits:(UIAccessibilityTraits)roleTraits +{ + objc_setAssociatedObject( + self, @selector(roleTraits), [NSNumber numberWithUnsignedLongLong:roleTraits], OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + #pragma mark - Debug - (void)react_addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp index ad6cf1ce83d8..884516a1bb46 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp @@ -171,14 +171,6 @@ AccessibilityProps::AccessibilityProps( "importantForAccessibility", sourceProps.importantForAccessibility, ImportantForAccessibility::Auto)), - role( - CoreFeatures::enablePropIteratorSetter ? sourceProps.role - : convertRawProp( - context, - rawProps, - "role", - sourceProps.role, - {})), testId( CoreFeatures::enablePropIteratorSetter ? sourceProps.testId : convertRawProp( @@ -195,19 +187,30 @@ AccessibilityProps::AccessibilityProps( // to work around here, and (2) would require very careful work to address // this case and not regress the more common cases. if (!CoreFeatures::enablePropIteratorSetter) { - const auto *rawPropValue = + auto *accessibilityRoleValue = rawProps.at("accessibilityRole", nullptr, nullptr); - AccessibilityTraits traits; - std::string roleString; - if (rawPropValue == nullptr || !rawPropValue->hasValue()) { - traits = sourceProps.accessibilityTraits; - roleString = sourceProps.accessibilityRole; + auto *roleValue = rawProps.at("role", nullptr, nullptr); + + auto *precedentRoleValue = roleValue != nullptr ? roleValue : accessibilityRoleValue; + + if (accessibilityRoleValue == nullptr || + !accessibilityRoleValue->hasValue()) { + accessibilityRole = sourceProps.accessibilityRole; + } else { + fromRawValue(context, *accessibilityRoleValue, accessibilityRole); + } + + if (roleValue == nullptr || !roleValue->hasValue()) { + role = sourceProps.role; + } else { + fromRawValue(context, *roleValue, role); + } + + if (precedentRoleValue == nullptr || !precedentRoleValue->hasValue()) { + accessibilityTraits = sourceProps.accessibilityTraits; } else { - fromRawValue(context, *rawPropValue, traits); - fromRawValue(context, *rawPropValue, roleString); + fromRawValue(context, *precedentRoleValue, accessibilityTraits); } - accessibilityTraits = traits; - accessibilityRole = roleString; } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h index 9ee16674181e..1690113e239f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h @@ -30,7 +30,7 @@ inline void fromString(const std::string &string, AccessibilityTraits &result) { result = AccessibilityTraits::Link; return; } - if (string == "image") { + if (string == "image" || string == "img") { result = AccessibilityTraits::Image; return; } @@ -78,7 +78,7 @@ inline void fromString(const std::string &string, AccessibilityTraits &result) { result = AccessibilityTraits::CausesPageTurn; return; } - if (string == "header") { + if (string == "header" || string == "heading") { result = AccessibilityTraits::Header; return; } diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h index b09fd7115ebe..fe0adae88f4a 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h @@ -15,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN NSString *const RCTAttributedStringIsHighlightedAttributeName = @"IsHighlighted"; NSString *const RCTAttributedStringEventEmitterKey = @"EventEmitter"; + +// String representation of either `role` or `accessibilityRole` NSString *const RCTTextAttributesAccessibilityRoleAttributeName = @"AccessibilityRole"; /* diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index 68388d1c8398..7db4677a329f 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -7,6 +7,7 @@ #import "RCTAttributedTextUtils.h" +#include #include #include #include @@ -288,130 +289,12 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex attributes[RCTAttributedStringIsHighlightedAttributeName] = @YES; } - if (textAttributes.accessibilityRole.has_value()) { - auto accessibilityRole = textAttributes.accessibilityRole.value(); - switch (accessibilityRole) { - case AccessibilityRole::None: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("none"); - break; - case AccessibilityRole::Button: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("button"); - break; - case AccessibilityRole::Dropdownlist: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("dropdownlist"); - break; - case AccessibilityRole::Togglebutton: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("togglebutton"); - break; - case AccessibilityRole::Link: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("link"); - break; - case AccessibilityRole::Search: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("search"); - break; - case AccessibilityRole::Image: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("image"); - break; - case AccessibilityRole::Keyboardkey: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("keyboardkey"); - break; - case AccessibilityRole::Text: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("text"); - break; - case AccessibilityRole::Adjustable: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("adjustable"); - break; - case AccessibilityRole::Imagebutton: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("imagebutton"); - break; - case AccessibilityRole::Header: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("header"); - break; - case AccessibilityRole::Summary: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("summary"); - break; - case AccessibilityRole::Alert: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("alert"); - break; - case AccessibilityRole::Checkbox: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("checkbox"); - break; - case AccessibilityRole::Combobox: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("combobox"); - break; - case AccessibilityRole::Menu: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menu"); - break; - case AccessibilityRole::Menubar: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menubar"); - break; - case AccessibilityRole::Menuitem: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menuitem"); - break; - case AccessibilityRole::Progressbar: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("progressbar"); - break; - case AccessibilityRole::Radio: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("radio"); - break; - case AccessibilityRole::Radiogroup: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("radiogroup"); - break; - case AccessibilityRole::Scrollbar: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("scrollbar"); - break; - case AccessibilityRole::Spinbutton: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("spinbutton"); - break; - case AccessibilityRole::Switch: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("switch"); - break; - case AccessibilityRole::Tab: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tab"); - break; - case AccessibilityRole::Tabbar: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tabbar"); - break; - case AccessibilityRole::Tablist: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tablist"); - break; - case AccessibilityRole::Timer: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("timer"); - break; - case AccessibilityRole::List: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("list"); - break; - case AccessibilityRole::Toolbar: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("toolbar"); - break; - case AccessibilityRole::Grid: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("grid"); - break; - case AccessibilityRole::Pager: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("pager"); - break; - case AccessibilityRole::Scrollview: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("scrollview"); - break; - case AccessibilityRole::Horizontalscrollview: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("horizontalscrollview"); - break; - case AccessibilityRole::Viewgroup: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("viewgroup"); - break; - case AccessibilityRole::Webview: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("webview"); - break; - case AccessibilityRole::Drawerlayout: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("drawerlayout"); - break; - case AccessibilityRole::Slidingdrawer: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("slidingdrawer"); - break; - case AccessibilityRole::Iconmenu: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("iconmenu"); - break; - }; + if (textAttributes.role.has_value()) { + std::string roleStr = toString(textAttributes.role.value()); + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = [NSString stringWithCString:roleStr.c_str()]; + } else if (textAttributes.accessibilityRole.has_value()) { + std::string roleStr = toString(textAttributes.accessibilityRole.value()); + attributes[RCTTextAttributesAccessibilityRoleAttributeName] = [NSString stringWithCString:roleStr.c_str()]; } return [attributes copy]; From 6e36a64f6a26b8b4b3a0388c443815da17a2dce0 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 8 May 2023 15:07:35 -0700 Subject: [PATCH 4/4] Native ARIA Roles: Remove JS Shim (#37304) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/37304 ### Stack ARIA roles in React Native are implemented on top of accessibilityRole. This is lossy because there are many more ARIA roles than accessibilityRole. This is especially true for RN on desktop where accessibilityRole was designed around accessibility APIs only available on mobile. This series of changes aims to change this implementation to instead pass the ARIA role to native, alongside any existing accessibilityRole. This gives the platform more control in exactly how to map an ARIA role to native behavior. As an example, this would allow mapping any ARIA role to AutomationControlType on Windows without needing to fork to add new options to accessibilityRole. It also allows greater implementation flexibility for other platforms down the line, but for now, iOS and Android behave the same as before (though with their implementation living in native). ### Diff This removes the JS shimming of `role` to `accessibilityRole`. It can be landed when the native `role` implementation for both iOS and Android are live for versions we service OTA. This will require any out of tree platforms to add their own native implementation for `role` to continue to work. Changelog: [Internal] Differential Revision: D45432518 fbshipit-source-id: c223d0dc8cb1ed967b79681160bad57fc9553243 --- .../Libraries/Components/View/View.js | 6 - .../Components/View/__tests__/View-test.js | 1 + packages/react-native/Libraries/Text/Text.js | 9 - .../Libraries/Text/__tests__/Text-test.js | 1 + .../Utilities/AcessibilityMapping.js | 154 ------------------ 5 files changed, 2 insertions(+), 169 deletions(-) delete mode 100644 packages/react-native/Libraries/Utilities/AcessibilityMapping.js diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js index a10c06231fbf..d9814254743d 100644 --- a/packages/react-native/Libraries/Components/View/View.js +++ b/packages/react-native/Libraries/Components/View/View.js @@ -12,7 +12,6 @@ import type {ViewProps} from './ViewPropTypes'; import flattenStyle from '../../StyleSheet/flattenStyle'; import TextAncestor from '../../Text/TextAncestor'; -import {getAccessibilityRoleFromRole} from '../../Utilities/AcessibilityMapping'; import ViewNativeComponent from './ViewNativeComponent'; import * as React from 'react'; @@ -35,7 +34,6 @@ const View: React.AbstractComponent< accessibilityLabel, accessibilityLabelledBy, accessibilityLiveRegion, - accessibilityRole, accessibilityState, accessibilityValue, 'aria-busy': ariaBusy, @@ -56,7 +54,6 @@ const View: React.AbstractComponent< importantForAccessibility, nativeID, pointerEvents, - role, tabIndex, ...otherProps }: ViewProps, @@ -113,9 +110,6 @@ const View: React.AbstractComponent< accessibilityLabel={ariaLabel ?? accessibilityLabel} focusable={tabIndex !== undefined ? !tabIndex : focusable} accessibilityState={_accessibilityState} - accessibilityRole={ - role ? getAccessibilityRoleFromRole(role) : accessibilityRole - } accessibilityElementsHidden={ariaHidden ?? accessibilityElementsHidden} accessibilityLabelledBy={_accessibilityLabelledBy} accessibilityValue={_accessibilityValue} diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-test.js b/packages/react-native/Libraries/Components/View/__tests__/View-test.js index 66fac79f4ab4..18649af87ee3 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-test.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-test.js @@ -160,6 +160,7 @@ describe('View compat with web', () => { aria-setsize={5} aria-sort="ascending" importantForAccessibility="no-hide-descendants" + role="main" /> `); }); diff --git a/packages/react-native/Libraries/Text/Text.js b/packages/react-native/Libraries/Text/Text.js index df548af47dab..d473178deb54 100644 --- a/packages/react-native/Libraries/Text/Text.js +++ b/packages/react-native/Libraries/Text/Text.js @@ -15,7 +15,6 @@ import * as PressabilityDebug from '../Pressability/PressabilityDebug'; import usePressability from '../Pressability/usePressability'; import flattenStyle from '../StyleSheet/flattenStyle'; import processColor from '../StyleSheet/processColor'; -import {getAccessibilityRoleFromRole} from '../Utilities/AcessibilityMapping'; import Platform from '../Utilities/Platform'; import TextAncestor from './TextAncestor'; import {NativeText, NativeVirtualText} from './TextNativeComponent'; @@ -34,7 +33,6 @@ const Text: React.AbstractComponent< const { accessible, accessibilityLabel, - accessibilityRole, accessibilityState, allowFontScaling, 'aria-busy': ariaBusy, @@ -57,7 +55,6 @@ const Text: React.AbstractComponent< onResponderTerminationRequest, onStartShouldSetResponder, pressRetentionOffset, - role, suppressHighlighting, ...restProps } = props; @@ -234,9 +231,6 @@ const Text: React.AbstractComponent< {...restProps} {...eventHandlersForText} accessibilityLabel={ariaLabel ?? accessibilityLabel} - accessibilityRole={ - role ? getAccessibilityRoleFromRole(role) : accessibilityRole - } accessibilityState={_accessibilityState} isHighlighted={isHighlighted} isPressable={isPressable} @@ -253,9 +247,6 @@ const Text: React.AbstractComponent< {...restProps} {...eventHandlersForText} accessibilityLabel={ariaLabel ?? accessibilityLabel} - accessibilityRole={ - role ? getAccessibilityRoleFromRole(role) : accessibilityRole - } accessibilityState={nativeTextAccessibilityState} accessible={ accessible == null && Platform.OS === 'android' diff --git a/packages/react-native/Libraries/Text/__tests__/Text-test.js b/packages/react-native/Libraries/Text/__tests__/Text-test.js index af02de98211c..ffd7e8d68878 100644 --- a/packages/react-native/Libraries/Text/__tests__/Text-test.js +++ b/packages/react-native/Libraries/Text/__tests__/Text-test.js @@ -169,6 +169,7 @@ describe('Text compat with web', () => { disabled={true} ellipsizeMode="tail" isHighlighted={false} + role="main" selectionColor={null} /> `); diff --git a/packages/react-native/Libraries/Utilities/AcessibilityMapping.js b/packages/react-native/Libraries/Utilities/AcessibilityMapping.js deleted file mode 100644 index 911f3381089f..000000000000 --- a/packages/react-native/Libraries/Utilities/AcessibilityMapping.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import type { - AccessibilityRole, - Role, -} from '../Components/View/ViewAccessibility'; - -// Map role values to AccessibilityRole values -export function getAccessibilityRoleFromRole(role: Role): ?AccessibilityRole { - switch (role) { - case 'alert': - return 'alert'; - case 'alertdialog': - return undefined; - case 'application': - return undefined; - case 'article': - return undefined; - case 'banner': - return undefined; - case 'button': - return 'button'; - case 'cell': - return undefined; - case 'checkbox': - return 'checkbox'; - case 'columnheader': - return undefined; - case 'combobox': - return 'combobox'; - case 'complementary': - return undefined; - case 'contentinfo': - return undefined; - case 'definition': - return undefined; - case 'dialog': - return undefined; - case 'directory': - return undefined; - case 'document': - return undefined; - case 'feed': - return undefined; - case 'figure': - return undefined; - case 'form': - return undefined; - case 'grid': - return 'grid'; - case 'group': - return undefined; - case 'heading': - return 'header'; - case 'img': - return 'image'; - case 'link': - return 'link'; - case 'list': - return 'list'; - case 'listitem': - return undefined; - case 'log': - return undefined; - case 'main': - return undefined; - case 'marquee': - return undefined; - case 'math': - return undefined; - case 'menu': - return 'menu'; - case 'menubar': - return 'menubar'; - case 'menuitem': - return 'menuitem'; - case 'meter': - return undefined; - case 'navigation': - return undefined; - case 'none': - return 'none'; - case 'note': - return undefined; - case 'option': - return undefined; - case 'presentation': - return 'none'; - case 'progressbar': - return 'progressbar'; - case 'radio': - return 'radio'; - case 'radiogroup': - return 'radiogroup'; - case 'region': - return undefined; - case 'row': - return undefined; - case 'rowgroup': - return undefined; - case 'rowheader': - return undefined; - case 'scrollbar': - return 'scrollbar'; - case 'searchbox': - return 'search'; - case 'separator': - return undefined; - case 'slider': - return 'adjustable'; - case 'spinbutton': - return 'spinbutton'; - case 'status': - return undefined; - case 'summary': - return 'summary'; - case 'switch': - return 'switch'; - case 'tab': - return 'tab'; - case 'table': - return undefined; - case 'tablist': - return 'tablist'; - case 'tabpanel': - return undefined; - case 'term': - return undefined; - case 'timer': - return 'timer'; - case 'toolbar': - return 'toolbar'; - case 'tooltip': - return undefined; - case 'tree': - return undefined; - case 'treegrid': - return undefined; - case 'treeitem': - return undefined; - } - - return undefined; -}