Skip to content

Commit

Permalink
Fix new role prop after JS-Shim was removed by Meta (#2101)
Browse files Browse the repository at this point in the history
## Summary:

main targeted PR of #2100 

Prior to 0.73, the new `role` prop was remapped to `accessibilityRole`
on the JS-side. Starting with facebook#37304, the work needs to be completed on
the native side. Since the new prop is ARIA inspired, the mappings are
taking from the [ARIA Core
AAM](https://www.w3.org/TR/core-aam-1.2/#mapping_role_table) which
disagrees with some of the mappings used in the old `accessibilityRole`
prop. Users of the old prop are unaffected, but the new prop will take
the mappings from the spec.

## Test Plan:

Tested a variety of permutations of accessibilityRole and role to
confirm the behavior looks correct in Accessibility Inspector
  • Loading branch information
FalseLobster committed Mar 22, 2024
2 parents ac175fa + b4137f9 commit 26ab78e
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 19 deletions.
2 changes: 1 addition & 1 deletion packages/react-native/React/Base/RCTConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ typedef BOOL css_backface_visibility_t;
+ (RCTTextDecorationLineType)RCTTextDecorationLineType:(id)json;

#if TARGET_OS_OSX // [macOS
+ (NSString *)accessibilityRoleFromTraits:(id)json;
+ (NSString *)accessibilityRoleFromTraits:(id)json useAriaMappings:(BOOL) useAriaMappings;

+ (NSArray<RCTHandledKey *> *)RCTHandledKeyArray:(id)json;
#endif // macOS]
Expand Down
104 changes: 101 additions & 3 deletions packages/react-native/React/Base/RCTConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,104 @@ + (NSPropertyList)NSPropertyList:(id)json
integerValue)

#if TARGET_OS_OSX // [macOS
// This is for the role prop & has slightly different mappings than the
// old accessibilityRole prop for back compatability. `role` matches ARIA Core
// AAM spec and takes precedence. See https://www.w3.org/TR/core-aam-1.1/
+ (NSString*) accessibilityRoleFromAriaRole:(NSString*)ariaRole
{
// rowgroup is explicitly not mapped
if ([ariaRole isEqualToString:@"rowgroup"]) {
return nil;
}
static NSDictionary<NSString *, NSAccessibilityRole> * ariaRoleToNSAccessibilityRole;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ariaRoleToNSAccessibilityRole = @{
@"alert": NSAccessibilityGroupRole,
@"alertdialog": NSAccessibilityGroupRole,
@"application": NSAccessibilityGroupRole,
@"article": NSAccessibilityGroupRole,
@"banner": NSAccessibilityGroupRole,
@"button": NSAccessibilityButtonRole,
@"cell": NSAccessibilityCellRole,
@"checkbox": NSAccessibilityCheckBoxRole,
@"columnheader": NSAccessibilityCellRole,
@"combobox": NSAccessibilityComboBoxRole,
@"complementary": NSAccessibilityGroupRole,
@"contentinfo": NSAccessibilityGroupRole,
@"definition": NSAccessibilityGroupRole,
@"dialog": NSAccessibilityGroupRole,
@"directory": NSAccessibilityListRole,
@"document": NSAccessibilityGroupRole,
@"feed": NSAccessibilityGroupRole,
@"figure": NSAccessibilityGroupRole,
@"form": NSAccessibilityGroupRole,
@"grid": NSAccessibilityTableRole,
@"gridcell": NSAccessibilityCellRole,
@"group": NSAccessibilityGroupRole,
@"heading": NSAccessibilityStaticTextRole,
@"image": NSAccessibilityImageRole,
@"img": NSAccessibilityImageRole,
@"link": NSAccessibilityLinkRole,
@"list": NSAccessibilityListRole,
@"listbox": NSAccessibilityListRole,
@"listitem": NSAccessibilityGroupRole,
@"log": NSAccessibilityGroupRole,
@"main": NSAccessibilityGroupRole,
@"marquee": NSAccessibilityGroupRole,
@"math": NSAccessibilityGroupRole,
@"menu": NSAccessibilityMenuRole,
@"menubar": NSAccessibilityMenuBarRole,
@"menuitem": NSAccessibilityMenuItemRole,
@"menuitemcheckbox": NSAccessibilityMenuItemRole,
@"menuitemradio": NSAccessibilityMenuItemRole,
@"meter": NSAccessibilityLevelIndicatorRole,
@"navigation": NSAccessibilityGroupRole,
@"none": NSAccessibilityGroupRole,
@"note": NSAccessibilityGroupRole,
@"option": NSAccessibilityStaticTextRole,
@"presentation": NSAccessibilityGroupRole,
@"progressbar": NSAccessibilityProgressIndicatorRole,
@"radio": NSAccessibilityRadioButtonRole,
@"radiogroup": NSAccessibilityRadioGroupRole,
@"region": NSAccessibilityGroupRole,
@"row": NSAccessibilityRowRole,
@"rowheader": NSAccessibilityCellRole,
@"scrollbar": NSAccessibilityScrollBarRole,
@"search": NSAccessibilityGroupRole,
@"searchbox": NSAccessibilityTextFieldRole,
@"separator": NSAccessibilitySplitterRole,
@"slider": NSAccessibilitySliderRole,
@"spinbutton": NSAccessibilityIncrementorRole,
@"status": NSAccessibilityGroupRole,
@"switch": NSAccessibilityCheckBoxRole,
@"tab": NSAccessibilityRadioButtonRole,
@"table": NSAccessibilityTableRole,
@"tablist": NSAccessibilityTabGroupRole,
@"tabpanel": NSAccessibilityGroupRole,
@"term": NSAccessibilityGroupRole,
@"textbox": NSAccessibilityTextFieldRole,
@"timer": NSAccessibilityGroupRole,
@"toolbar": NSAccessibilityToolbarRole,
@"tooltip": NSAccessibilityGroupRole,
@"tree": NSAccessibilityOutlineRole,
@"treegrid": NSAccessibilityTableRole,
@"treeitem": NSAccessibilityRowRole,
};
});
NSAccessibilityRole nsRole = [ariaRoleToNSAccessibilityRole valueForKey: ariaRole];
if (nsRole == nil) {
// Fall back to legacy mappings if an aria mapping is not found. This would
// include macOS specific roles like disclosure and legacy accessibilityTrait
// based mappings like adjustable
nsRole = [RCTConvert accessibilityRoleFromTrait:ariaRole];
}
return nsRole;
}

// This function is for accessibilityRole & has slightly different mappings
// than the new role prop for back compatability. role matches ARIA spec and
// takes precedence.
+ (NSString*)accessibilityRoleFromTrait:(NSString*)trait
{
static NSDictionary<NSString *, NSString *> *traitOrRoleToAccessibilityRole;
Expand Down Expand Up @@ -1525,13 +1623,13 @@ + (NSString*)accessibilityRoleFromTrait:(NSString*)trait
return role;
}

+ (NSString *)accessibilityRoleFromTraits:(id)json
+ (NSString *)accessibilityRoleFromTraits:(id)json useAriaMappings:(BOOL)useAriaMappings
{
if ([json isKindOfClass:[NSString class]]) {
return [RCTConvert accessibilityRoleFromTrait:json];
return useAriaMappings ? [RCTConvert accessibilityRoleFromAriaRole:json] : [RCTConvert accessibilityRoleFromTrait:json];
} else if ([json isKindOfClass:[NSArray class]]) {
for (NSString *trait in json) {
NSString *accessibilityRole = [RCTConvert accessibilityRoleFromTrait:trait];
NSString *accessibilityRole = useAriaMappings ? [RCTConvert accessibilityRoleFromAriaRole:json] : [RCTConvert accessibilityRoleFromTrait:trait];
if (![accessibilityRole isEqualToString:NSAccessibilityUnknownRole]) {
return accessibilityRole;
}
Expand Down
4 changes: 0 additions & 4 deletions packages/react-native/React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,7 @@ - (NSString *)accessibilityValue

// TODO: This logic makes VoiceOver describe some AccessibilityRole which do not have a backing UIAccessibilityTrait.
// It does not run on Fabric.
#if !TARGET_OS_OSX // [macOS]
NSString *role = self.role ?: self.accessibilityRole;
#else // [macOS renamed prop so it doesn't conflict with -[NSAccessibility accessibilityRole].
NSString *role = self.role ?: self.accessibilityRoleInternal;
#endif
NSString *roleDescription = role ? rolesAndStatesDescription[role] : nil;
if (roleDescription) {
[valueComponents addObject:roleDescription];
Expand Down
41 changes: 30 additions & 11 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,9 @@ - (RCTShadowView *)shadowView
#else // [macOS accessibilityTraits is gone in react-native and deprecated in react-native-macos, use accessibilityRole instead
RCT_CUSTOM_VIEW_PROPERTY(accessibilityTraits, NSString, RCTView)
{
if (json) {
view.accessibilityRole = [RCTConvert accessibilityRoleFromTraits:json];
} else {
view.accessibilityRole = defaultView.accessibilityRole;
view.reactAccessibilityElement.accessibilityRoleInternal = json ? [RCTConvert accessibilityRoleFromTraits:json useAriaMappings:NO] : nil;
if (view.reactAccessibilityElement.accessibilityRole != view.reactAccessibilityElement.accessibilityRoleInternal) {
[self updateAccessibilityRole:view withDefaultView:defaultView];
}
}
#endif // macOS]
Expand Down Expand Up @@ -293,25 +292,35 @@ - (RCTShadowView *)shadowView
[self updateAccessibilityTraitsForRole:view withDefaultView:defaultView];
}
#else // [macOS
if (json) {
view.reactAccessibilityElement.accessibilityRole = [RCTConvert accessibilityRoleFromTraits:json];
} else {
view.reactAccessibilityElement.accessibilityRole = defaultView.accessibilityRole;
}
// accessibilityRoleInternal is used to cache the converted value from the prop
view.reactAccessibilityElement.accessibilityRoleInternal = json ? [RCTConvert accessibilityRoleFromTraits:json useAriaMappings:NO] : nil;
// update the actual NSAccessibilityRole if it doesn't match
if (view.reactAccessibilityElement.accessibilityRole != view.reactAccessibilityElement.accessibilityRoleInternal) {
[self updateAccessibilityRole:view withDefaultView:defaultView];
}
#endif // macOS]
}

#if !TARGET_OS_OSX // [macOS]
RCT_CUSTOM_VIEW_PROPERTY(role, UIAccessibilityTraits, RCTView)
{
#if !TARGET_OS_OSX // [macOS]
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];
}
#else // [macOS
// role is used to cache the converted value from the prop
view.reactAccessibilityElement.role = json ? [RCTConvert accessibilityRoleFromTraits:json useAriaMappings:YES] : nil;
// update the actual NSAccessibilityRole if it doesn't match
if (view.reactAccessibilityElement.accessibilityRole != view.reactAccessibilityElement.role) {
[self updateAccessibilityRole:view withDefaultView:defaultView];
}
#endif // macOS]
}

#if !TARGET_OS_OSX // [macOS]
- (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTView *)defaultView
{
const UIAccessibilityTraits AccessibilityRolesMask = UIAccessibilityTraitNone | UIAccessibilityTraitButton |
Expand All @@ -328,7 +337,17 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie
: view.reactAccessibilityElement.accessibilityRole ? view.reactAccessibilityElement.accessibilityRoleTraits
: (defaultView.accessibilityTraits & AccessibilityRolesMask);
}
#endif // [macOS]
#else // [macOS
- (void) updateAccessibilityRole:(RCTView *)view withDefaultView:(RCTView *)defaultView
{
// First check the value from `role`
view.reactAccessibilityElement.accessibilityRole = view.reactAccessibilityElement.role ? view.reactAccessibilityElement.role : nil;
// Fallback to `accessibilityRole` if nil, or the defaultView's NSAccessibilityRole
if (view.reactAccessibilityElement.accessibilityRole == nil) {
view.reactAccessibilityElement.accessibilityRole = view.reactAccessibilityElement.accessibilityRoleInternal ? view.reactAccessibilityElement.accessibilityRoleInternal : defaultView.accessibilityRole;
}
}
#endif // macOS]

RCT_CUSTOM_VIEW_PROPERTY(accessibilityState, NSDictionary, RCTView)
{
Expand Down

0 comments on commit 26ab78e

Please sign in to comment.