From 02ec60bd3ea81681c90acd7ea7c3860a2aeef5c4 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 13 May 2023 13:12:42 -0700 Subject: [PATCH] Native ARIA Roles: Android Paper + Fabric (#37306) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/37306 ### 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 replicates the roles to Java. When using MapBuffer we pass the role by ordinal, assuming they keep in sync. We otherwise still pass by string matching the JS side. Previous implementation kept `accessibilityRole` on a view tag of the native view. This adds `role` as a view tag as well. For now, to reuse the existing code, we then expose a single function to query `AccessibilityRole` from the combined view tags. This will do the same mapping previously done in JS, so that any code previously reading for an `AccessibilityRole` will now get one derived from the role view tag if present. Changelog: [Internal] Reviewed By: sammy-SC Differential Revision: D45431381 fbshipit-source-id: a72c7880d41b5cf2c4e1c1f3ebfa6832ce8b4250 --- .../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 b15a2984f20..b8f10af4da9 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.bridge.ReadableType; 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 abstract class BaseViewManager 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 bb809381494..e59a1c4da3b 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 abstract class BaseViewManagerDelegate { 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 626232ed0f3..531e4591ba6 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 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { 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 class ReactAccessibilityDelegate extends ExploreByTouchHelper { } 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 class ReactAccessibilityDelegate extends ExploreByTouchHelper { ? 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 class ReactAccessibilityDelegate extends ExploreByTouchHelper { || 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 3f76fa7dd3d..cc9f7178e65 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 40392887a1c..d5d3931b00b 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 @@ import com.facebook.react.uimanager.events.NativeGestureUtil; 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 bc122f2ee3f..adbb78611c6 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.AssertionException; 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 @@ public class ReactScrollViewAccessibilityDelegate extends AccessibilityDelegateC 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 12c1c5e0572..4fd3a8165f4 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.IllegalViewOperationException; 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.ArrayList; 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 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { 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 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { 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 abstract class ReactBaseTextShadowNode extends LayoutShadowNode { } @ReactProp(name = ViewProps.ACCESSIBILITY_ROLE) - public void setIsAccessibilityLink(@Nullable String accessibilityRole) { + public void setAccessibilityRole(@Nullable String accessibilityRole) { if (isVirtual()) { - mIsAccessibilityLink = Objects.equals(accessibilityRole, "link"); + mAccessibilityRole = AccessibilityRole.fromValue(accessibilityRole); + markUpdated(); + } + } + + @ReactProp(name = ViewProps.ROLE) + public void setRole(@Nullable String role) { + if (isVirtual()) { + 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 bc53523c815..eb03a50743f 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.bridge.ReadableMap; 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 class TextAttributeProps { 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 class TextAttributeProps { 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 @@ public class TextAttributeProps { } 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 44195221646..040171ed008 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.ReadableNativeMap; 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 @@ public class TextLayoutManager { 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 bc0a56384b9..92167031088 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.build.ReactBuildConfig; 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 @@ public class TextLayoutManagerMapBuffer { 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 a7effcdab17..7447e45313a 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 6324b85af44..d2928f810df 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 95642f5edd9..83e32f7287a 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.Arguments; 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 class BaseViewManagerTest { 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 b2ae66a5b95..0d54c7e7749 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 c866cb23a46..a9fa5e60e2c 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 37dd41eeec9..51d96ee40a0 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;