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/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/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/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; -} 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/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/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..0d54c7e77493 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: @@ -1111,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; @@ -1263,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/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..884516a1bb46 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp @@ -187,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; } } @@ -227,6 +238,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/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; 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..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; } @@ -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.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 dca3ab892c8d..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,94 +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::Link: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("link"); - break; - case AccessibilityRole::Search: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("search"); - break; - case AccessibilityRole::Image: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("image"); - break; - case AccessibilityRole::Imagebutton: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("imagebutton"); - break; - case AccessibilityRole::Keyboardkey: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("keyboardkey"); - break; - case AccessibilityRole::Text: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("text"); - break; - case AccessibilityRole::Adjustable: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("adjustable"); - break; - case AccessibilityRole::Summary: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("summary"); - break; - case AccessibilityRole::Header: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("header"); - 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::Toolbar: - attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("toolbar"); - 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];