Skip to content

Commit 02ec60b

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Native ARIA Roles: Android Paper + Fabric (#37306)
Summary: Pull Request resolved: #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
1 parent 9db5fa2 commit 02ec60b

File tree

18 files changed

+274
-26
lines changed

18 files changed

+274
-26
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.facebook.react.common.MapBuilder;
2727
import com.facebook.react.common.ReactConstants;
2828
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
29+
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;
2930
import com.facebook.react.uimanager.annotations.ReactProp;
3031
import com.facebook.react.uimanager.events.PointerEventHelper;
3132
import com.facebook.react.uimanager.util.ReactFindViewUtil;
@@ -234,9 +235,10 @@ public void setAccessibilityHint(@NonNull T view, @Nullable String accessibility
234235
@ReactProp(name = ViewProps.ACCESSIBILITY_ROLE)
235236
public void setAccessibilityRole(@NonNull T view, @Nullable String accessibilityRole) {
236237
if (accessibilityRole == null) {
237-
return;
238+
view.setTag(R.id.accessibility_role, null);
239+
} else {
240+
view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole));
238241
}
239-
view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole));
240242
}
241243

242244
@Override
@@ -380,6 +382,16 @@ public void setImportantForAccessibility(
380382
}
381383
}
382384

385+
@Override
386+
@ReactProp(name = ViewProps.ROLE)
387+
public void setRole(@NonNull T view, @Nullable String role) {
388+
if (role == null) {
389+
view.setTag(R.id.role, null);
390+
} else {
391+
view.setTag(R.id.role, Role.fromValue(role));
392+
}
393+
}
394+
383395
@Override
384396
@Deprecated
385397
@ReactProp(name = ViewProps.ROTATION)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ public void setShadowColor(@NonNull T view, int shadowColor) {}
7070
public void setImportantForAccessibility(
7171
@NonNull T view, @Nullable String importantForAccessibility) {}
7272

73+
@Override
74+
public void setRole(@NonNull T view, @Nullable String role) {}
75+
7376
@Override
7477
public void setNativeId(@NonNull T view, String nativeId) {}
7578

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
8989
case ViewProps.IMPORTANT_FOR_ACCESSIBILITY:
9090
mViewManager.setImportantForAccessibility(view, (String) value);
9191
break;
92+
case ViewProps.ROLE:
93+
mViewManager.setRole(view, (String) value);
94+
break;
9295
case ViewProps.NATIVE_ID:
9396
mViewManager.setNativeId(view, (String) value);
9497
break;

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public interface BaseViewManagerInterface<T extends View> {
5252

5353
void setImportantForAccessibility(T view, @Nullable String importantForAccessibility);
5454

55+
void setRole(T view, @Nullable String role);
56+
5557
void setNativeId(T view, @Nullable String nativeId);
5658

5759
void setAccessibilityLabelledBy(T view, @Nullable Dynamic nativeId);

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,87 @@ private void scheduleAccessibilityEventSender(View host) {
9696
mHandler.sendMessageDelayed(msg, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
9797
}
9898

99+
/**
100+
* An ARIA Role representable by View's `role` prop. Ordinals should be kept in sync with
101+
* `facebook::react::Role`.
102+
*/
103+
public enum Role {
104+
ALERT,
105+
ALERTDIALOG,
106+
APPLICATION,
107+
ARTICLE,
108+
BANNER,
109+
BUTTON,
110+
CELL,
111+
CHECKBOX,
112+
COLUMNHEADER,
113+
COMBOBOX,
114+
COMPLEMENTARY,
115+
CONTENTINFO,
116+
DEFINITION,
117+
DIALOG,
118+
DIRECTORY,
119+
DOCUMENT,
120+
FEED,
121+
FIGURE,
122+
FORM,
123+
GRID,
124+
GROUP,
125+
HEADING,
126+
IMG,
127+
LINK,
128+
LIST,
129+
LISTITEM,
130+
LOG,
131+
MAIN,
132+
MARQUEE,
133+
MATH,
134+
MENU,
135+
MENUBAR,
136+
MENUITEM,
137+
METER,
138+
NAVIGATION,
139+
NONE,
140+
NOTE,
141+
OPTION,
142+
PRESENTATION,
143+
PROGRESSBAR,
144+
RADIO,
145+
RADIOGROUP,
146+
REGION,
147+
ROW,
148+
ROWGROUP,
149+
ROWHEADER,
150+
SCROLLBAR,
151+
SEARCHBOX,
152+
SEPARATOR,
153+
SLIDER,
154+
SPINBUTTON,
155+
STATUS,
156+
SUMMARY,
157+
SWITCH,
158+
TAB,
159+
TABLE,
160+
TABLIST,
161+
TABPANEL,
162+
TERM,
163+
TIMER,
164+
TOOLBAR,
165+
TOOLTIP,
166+
TREE,
167+
TREEGRID,
168+
TREEITEM;
169+
170+
public static @Nullable Role fromValue(@Nullable String value) {
171+
for (Role role : Role.values()) {
172+
if (role.name().equalsIgnoreCase(value)) {
173+
return role;
174+
}
175+
}
176+
return null;
177+
}
178+
}
179+
99180
/**
100181
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
101182
* date with their implementation. Details can be seen in their source code here:
@@ -221,6 +302,75 @@ public static AccessibilityRole fromValue(@Nullable String value) {
221302
}
222303
throw new IllegalArgumentException("Invalid accessibility role value: " + value);
223304
}
305+
306+
public static @Nullable AccessibilityRole fromRole(Role role) {
307+
switch (role) {
308+
case ALERT:
309+
return AccessibilityRole.ALERT;
310+
case BUTTON:
311+
return AccessibilityRole.BUTTON;
312+
case CHECKBOX:
313+
return AccessibilityRole.CHECKBOX;
314+
case COMBOBOX:
315+
return AccessibilityRole.COMBOBOX;
316+
case GRID:
317+
return AccessibilityRole.GRID;
318+
case HEADING:
319+
return AccessibilityRole.HEADER;
320+
case IMG:
321+
return AccessibilityRole.IMAGE;
322+
case LINK:
323+
return AccessibilityRole.LINK;
324+
case LIST:
325+
return AccessibilityRole.LIST;
326+
case MENU:
327+
return AccessibilityRole.MENU;
328+
case MENUBAR:
329+
return AccessibilityRole.MENUBAR;
330+
case MENUITEM:
331+
return AccessibilityRole.MENUITEM;
332+
case NONE:
333+
return AccessibilityRole.NONE;
334+
case PROGRESSBAR:
335+
return AccessibilityRole.PROGRESSBAR;
336+
case RADIO:
337+
return AccessibilityRole.RADIO;
338+
case RADIOGROUP:
339+
return AccessibilityRole.RADIOGROUP;
340+
case SCROLLBAR:
341+
return AccessibilityRole.SCROLLBAR;
342+
case SEARCHBOX:
343+
return AccessibilityRole.SEARCH;
344+
case SLIDER:
345+
return AccessibilityRole.ADJUSTABLE;
346+
case SPINBUTTON:
347+
return AccessibilityRole.SPINBUTTON;
348+
case SUMMARY:
349+
return AccessibilityRole.SUMMARY;
350+
case SWITCH:
351+
return AccessibilityRole.SWITCH;
352+
case TAB:
353+
return AccessibilityRole.TAB;
354+
case TABLIST:
355+
return AccessibilityRole.TABLIST;
356+
case TIMER:
357+
return AccessibilityRole.TIMER;
358+
case TOOLBAR:
359+
return AccessibilityRole.TOOLBAR;
360+
default:
361+
// No mapping from ARIA role to AccessibilityRole
362+
return null;
363+
}
364+
}
365+
366+
public static @Nullable AccessibilityRole fromViewTag(View view) {
367+
Role role = (Role) view.getTag(R.id.role);
368+
if (role != null) {
369+
return AccessibilityRole.fromRole(role);
370+
} else {
371+
return (AccessibilityRole) view.getTag(R.id.accessibility_role);
372+
}
373+
}
224374
}
225375

226376
private final HashMap<Integer, String> mAccessibilityActionsMap;
@@ -267,8 +417,7 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo
267417
? AccessibilityNodeInfoCompat.ACTION_COLLAPSE
268418
: AccessibilityNodeInfoCompat.ACTION_EXPAND);
269419
}
270-
final AccessibilityRole accessibilityRole =
271-
(AccessibilityRole) host.getTag(R.id.accessibility_role);
420+
final AccessibilityRole accessibilityRole = AccessibilityRole.fromViewTag(host);
272421
final String accessibilityHint = (String) host.getTag(R.id.accessibility_hint);
273422
if (accessibilityRole != null) {
274423
setRole(info, accessibilityRole, host.getContext());
@@ -551,7 +700,8 @@ public static void setDelegate(
551700
|| view.getTag(R.id.accessibility_actions) != null
552701
|| view.getTag(R.id.react_test_id) != null
553702
|| view.getTag(R.id.accessibility_collection_item) != null
554-
|| view.getTag(R.id.accessibility_links) != null)) {
703+
|| view.getTag(R.id.accessibility_links) != null
704+
|| view.getTag(R.id.role) != null)) {
555705
ViewCompat.setAccessibilityDelegate(
556706
view,
557707
new ReactAccessibilityDelegate(view, originalFocus, originalImportantForAccessibility));

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ public class ViewProps {
166166
public static final String ACCESSIBILITY_VALUE = "accessibilityValue";
167167
public static final String ACCESSIBILITY_LABELLED_BY = "accessibilityLabelledBy";
168168
public static final String IMPORTANT_FOR_ACCESSIBILITY = "importantForAccessibility";
169+
public static final String ROLE = "role";
169170

170171
// DEPRECATED
171172
public static final String ROTATION = "rotation";

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public ReactDrawerLayout(ReactContext reactContext) {
4242
public void onInitializeAccessibilityNodeInfo(
4343
View host, AccessibilityNodeInfoCompat info) {
4444
super.onInitializeAccessibilityNodeInfo(host, info);
45-
final AccessibilityRole accessibilityRole =
46-
(AccessibilityRole) host.getTag(R.id.accessibility_role);
45+
46+
final AccessibilityRole accessibilityRole = AccessibilityRole.fromViewTag(host);
4747
if (accessibilityRole != null) {
4848
info.setClassName(AccessibilityRole.getValue(accessibilityRole));
4949
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewAccessibilityDelegate.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.facebook.react.bridge.ReactSoftExceptionLogger;
1818
import com.facebook.react.bridge.ReadableMap;
1919
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
20+
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
2021

2122
public class ReactScrollViewAccessibilityDelegate extends AccessibilityDelegateCompat {
2223
private final String TAG = ReactScrollViewAccessibilityDelegate.class.getSimpleName();
@@ -122,8 +123,8 @@ private void onInitializeAccessibilityEventInternal(View view, AccessibilityEven
122123

123124
private void onInitializeAccessibilityNodeInfoInternal(
124125
View view, AccessibilityNodeInfoCompat info) {
125-
final ReactAccessibilityDelegate.AccessibilityRole accessibilityRole =
126-
(ReactAccessibilityDelegate.AccessibilityRole) view.getTag(R.id.accessibility_role);
126+
127+
final AccessibilityRole accessibilityRole = AccessibilityRole.fromViewTag(view);
127128

128129
if (accessibilityRole != null) {
129130
ReactAccessibilityDelegate.setRole(info, accessibilityRole, view.getContext());

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import com.facebook.react.uimanager.LayoutShadowNode;
2727
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
2828
import com.facebook.react.uimanager.PixelUtil;
29+
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
30+
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;
2931
import com.facebook.react.uimanager.ReactShadowNode;
3032
import com.facebook.react.uimanager.ViewProps;
3133
import com.facebook.react.uimanager.annotations.ReactProp;
@@ -36,7 +38,6 @@
3638
import java.util.HashMap;
3739
import java.util.List;
3840
import java.util.Map;
39-
import java.util.Objects;
4041

4142
/**
4243
* {@link ReactShadowNode} abstract class for spannable text nodes.
@@ -180,7 +181,11 @@ private static void buildSpannedFromShadowNode(
180181
new SetSpanOperation(
181182
start, end, new ReactBackgroundColorSpan(textShadowNode.mBackgroundColor)));
182183
}
183-
if (textShadowNode.mIsAccessibilityLink) {
184+
boolean roleIsLink =
185+
textShadowNode.mRole != null
186+
? textShadowNode.mRole == Role.LINK
187+
: textShadowNode.mAccessibilityRole == AccessibilityRole.LINK;
188+
if (roleIsLink) {
184189
ops.add(
185190
new SetSpanOperation(start, end, new ReactClickableSpan(textShadowNode.getReactTag())));
186191
}
@@ -325,7 +330,9 @@ protected Spannable spannedFromShadowNode(
325330
protected int mColor;
326331
protected boolean mIsBackgroundColorSet = false;
327332
protected int mBackgroundColor;
328-
protected boolean mIsAccessibilityLink = false;
333+
334+
protected @Nullable AccessibilityRole mAccessibilityRole = null;
335+
protected @Nullable Role mRole = null;
329336

330337
protected int mNumberOfLines = UNSET;
331338
protected int mTextAlign = Gravity.NO_GRAVITY;
@@ -499,9 +506,17 @@ public void setBackgroundColor(@Nullable Integer color) {
499506
}
500507

501508
@ReactProp(name = ViewProps.ACCESSIBILITY_ROLE)
502-
public void setIsAccessibilityLink(@Nullable String accessibilityRole) {
509+
public void setAccessibilityRole(@Nullable String accessibilityRole) {
510+
if (isVirtual()) {
511+
mAccessibilityRole = AccessibilityRole.fromValue(accessibilityRole);
512+
markUpdated();
513+
}
514+
}
515+
516+
@ReactProp(name = ViewProps.ROLE)
517+
public void setRole(@Nullable String role) {
503518
if (isVirtual()) {
504-
mIsAccessibilityLink = Objects.equals(accessibilityRole, "link");
519+
mRole = Role.fromValue(role);
505520
markUpdated();
506521
}
507522
}

0 commit comments

Comments
 (0)