From c2426f7672347f86a9b2f02eb422e57e8cecd0f1 Mon Sep 17 00:00:00 2001 From: Nick Lefever Date: Fri, 1 Dec 2023 04:50:54 +0100 Subject: [PATCH 1/5] [fabric][a11y] Enable accessibility property assignments for View Summary: Paper was rendering text as AXStaticText. This diff updates the `RCTParagraphComponentView` to propagate the same role in Fabric for text. This change will allow to select UI elements based on the text contents. Test Plan: Use the Accessibility Inspector in Zeratul with Fabric enabled. With the changes the text elements are presented in the a11y hierarchy with AXStaticText: {F1162808272} Reviewers: shawndempsey, chpurrer, #rn-desktop Reviewed By: chpurrer Differential Revision: https://phabricator.intern.facebook.com/D51736932 Tasks: T170938725 Tags: uikit-diff --- .../ComponentViews/View/RCTViewComponentView.h | 2 +- .../ComponentViews/View/RCTViewComponentView.mm | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index 7a903401112fa8..3d7564de46e8b1 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -56,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN * transparent in favour of some subview. * Defaults to `self`. */ -@property (nonatomic, strong, nullable, readonly) NSObject *accessibilityElement; +@property (nonatomic, strong, nullable, readonly) RCTPlatformView *accessibilityElement; // [macOS] /** * Insets used when hit testing inside this view. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 55fac1ea49bf0e..c7860b9b9ec0a6 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -19,6 +19,7 @@ #import // [macOS] #import #import +#import // [macOS] #import #import #import @@ -384,10 +385,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId); } -#if !TARGET_OS_OSX // [macOS] // `accessible` if (oldViewProps.accessible != newViewProps.accessible) { +#if !TARGET_OS_OSX // [macOS] self.accessibilityElement.isAccessibilityElement = newViewProps.accessible; +#else // [macOS + self.accessibilityElement.accessibilityElement = newViewProps.accessible; +#endif // macOS] } // `accessibilityLabel` @@ -403,9 +407,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // `accessibilityHint` if (oldViewProps.accessibilityHint != newViewProps.accessibilityHint) { +#if !TARGET_OS_OSX // [macOS] self.accessibilityElement.accessibilityHint = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint); +#else // [macOS + self.accessibilityElement.accessibilityHelp = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint); +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] // `accessibilityViewIsModal` if (oldViewProps.accessibilityViewIsModal != newViewProps.accessibilityViewIsModal) { self.accessibilityElement.accessibilityViewIsModal = newViewProps.accessibilityViewIsModal; @@ -458,6 +467,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (oldViewProps.accessibilityIgnoresInvertColors != newViewProps.accessibilityIgnoresInvertColors) { self.accessibilityIgnoresInvertColors = newViewProps.accessibilityIgnoresInvertColors; } +#endif // [macOS] // `accessibilityValue` if (oldViewProps.accessibilityValue != newViewProps.accessibilityValue) { @@ -476,8 +486,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.accessibilityElement.accessibilityValue = nil; } } -#endif // [macOS] - + // `testId` if (oldViewProps.testId != newViewProps.testId) { SEL setAccessibilityIdentifierSelector = @selector(setAccessibilityIdentifier:); @@ -1303,7 +1312,7 @@ - (void)clearExistingBackgroundImageLayers #pragma mark - Accessibility -- (NSObject *)accessibilityElement +- (RCTPlatformView *)accessibilityElement { return self; } From ed8a46020d99ee8fbde34bfe94d5002b6d2d3313 Mon Sep 17 00:00:00 2001 From: Nick Lefever Date: Fri, 1 Dec 2023 04:53:33 +0100 Subject: [PATCH 2/5] [fabric][a11y] Add role mapping for common traits Summary: Paper was rendering text as AXStaticText. This diff updates the `RCTParagraphComponentView` to propagate the same role in Fabric for text. This change will allow to select UI elements based on the text contents. Test Plan: Use the Accessibility Inspector in Zeratul with Fabric enabled. With the changes the text elements are presented in the a11y hierarchy with AXStaticText: {F1162808272} Reviewers: shawndempsey, chpurrer, #rn-desktop Reviewed By: chpurrer Differential Revision: https://phabricator.intern.facebook.com/D51736931 Tasks: T170938725 --- .../View/RCTViewComponentView.mm | 6 +++ .../React/Fabric/RCTConversions.h | 40 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index c7860b9b9ec0a6..2c789467999558 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -424,6 +424,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (oldViewProps.accessibilityElementsHidden != newViewProps.accessibilityElementsHidden) { self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden; } +#endif // [macOS] // `accessibilityShowsLargeContentViewer` if (oldViewProps.accessibilityShowsLargeContentViewer != newViewProps.accessibilityShowsLargeContentViewer) { @@ -447,10 +448,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // `accessibilityTraits` if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) { +#if !TARGET_OS_OSX // [macOS] self.accessibilityElement.accessibilityTraits = RCTUIAccessibilityTraitsFromAccessibilityTraits(newViewProps.accessibilityTraits); +#else // [macOS + self.accessibilityElement.accessibilityRole = RCTUIAccessibilityRoleFromAccessibilityTraits(newViewProps.accessibilityTraits); +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] // `accessibilityState` if (oldViewProps.accessibilityState != newViewProps.accessibilityState) { self.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected); diff --git a/packages/react-native/React/Fabric/RCTConversions.h b/packages/react-native/React/Fabric/RCTConversions.h index 66ce1342716a7a..f400ee001928c2 100644 --- a/packages/react-native/React/Fabric/RCTConversions.h +++ b/packages/react-native/React/Fabric/RCTConversions.h @@ -130,7 +130,45 @@ inline UIAccessibilityTraits RCTUIAccessibilityTraitsFromAccessibilityTraits( } return result; }; -#endif // [macOS] +#else // [macOS +inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( + facebook::react::AccessibilityTraits accessibilityTraits) +{ + using AccessibilityTraits = facebook::react::AccessibilityTraits; + if ((accessibilityTraits & AccessibilityTraits::Button) != AccessibilityTraits::None) { + return NSAccessibilityButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::Link) != AccessibilityTraits::None) { + return NSAccessibilityLinkRole; + } + if ((accessibilityTraits & AccessibilityTraits::Image) != AccessibilityTraits::None) { + return NSAccessibilityImageRole; + } + if ((accessibilityTraits & AccessibilityTraits::KeyboardKey) != AccessibilityTraits::None) { + return NSAccessibilityButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::StaticText) != AccessibilityTraits::None) { + return NSAccessibilityStaticTextRole; + } + if ((accessibilityTraits & AccessibilityTraits::SummaryElement) != AccessibilityTraits::None) { + return NSAccessibilityStaticTextRole; + } + if ((accessibilityTraits & AccessibilityTraits::SearchField) != AccessibilityTraits::None) { + return NSAccessibilityTextFieldRole; + } + if ((accessibilityTraits & AccessibilityTraits::Adjustable) != AccessibilityTraits::None) { + return NSAccessibilitySliderRole; + } + if ((accessibilityTraits & AccessibilityTraits::Header) != AccessibilityTraits::None) { + return NSAccessibilityStaticTextRole; + } + if ((accessibilityTraits & AccessibilityTraits::Switch) != AccessibilityTraits::None) { + return NSAccessibilityCheckBoxRole; + } + + return NSAccessibilityUnknownRole; +}; +#endif // macOS] inline CATransform3D RCTCATransform3DFromTransformMatrix(const facebook::react::Transform &transformMatrix) { From cabd8d27f08c3671324d1ea8fdf0aff8c41749e4 Mon Sep 17 00:00:00 2001 From: Nick Lefever Date: Fri, 1 Dec 2023 04:57:43 +0100 Subject: [PATCH 3/5] [fabric][a11y] Add role mapping for desktop specific traits Summary: Paper was rendering text as AXStaticText. This diff updates the `RCTParagraphComponentView` to propagate the same role in Fabric for text. This change will allow to select UI elements based on the text contents. Test Plan: Use the Accessibility Inspector in Zeratul with Fabric enabled. With the changes the text elements are presented in the a11y hierarchy with AXStaticText: {F1162808272} Reviewers: shawndempsey, chpurrer, #rn-desktop Reviewed By: chpurrer Differential Revision: https://phabricator.intern.facebook.com/D51736933 Tasks: T170938725 --- .../React/Fabric/RCTConversions.h | 56 ++++++++++++++++++- .../components/view/AccessibilityPrimitives.h | 15 +++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/react-native/React/Fabric/RCTConversions.h b/packages/react-native/React/Fabric/RCTConversions.h index f400ee001928c2..f6047c092e3d69 100644 --- a/packages/react-native/React/Fabric/RCTConversions.h +++ b/packages/react-native/React/Fabric/RCTConversions.h @@ -136,6 +136,15 @@ inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( { using AccessibilityTraits = facebook::react::AccessibilityTraits; if ((accessibilityTraits & AccessibilityTraits::Button) != AccessibilityTraits::None) { + if ((accessibilityTraits & AccessibilityTraits::Bar) != AccessibilityTraits::None) { + return NSAccessibilityToolbarRole; + } + if ((accessibilityTraits & AccessibilityTraits::PopUp) != AccessibilityTraits::None) { + return NSAccessibilityPopUpButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::Menu) != AccessibilityTraits::None) { + return NSAccessibilityMenuButtonRole; + } return NSAccessibilityButtonRole; } if ((accessibilityTraits & AccessibilityTraits::Link) != AccessibilityTraits::None) { @@ -165,7 +174,52 @@ inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( if ((accessibilityTraits & AccessibilityTraits::Switch) != AccessibilityTraits::None) { return NSAccessibilityCheckBoxRole; } - + if ((accessibilityTraits & AccessibilityTraits::UpdatesFrequently) != AccessibilityTraits::None) { + return NSAccessibilityProgressIndicatorRole; + } + if ((accessibilityTraits & AccessibilityTraits::ComboBox) != AccessibilityTraits::None) { + return NSAccessibilityComboBoxRole; + } + if ((accessibilityTraits & AccessibilityTraits::Menu) != AccessibilityTraits::None) { + if ((accessibilityTraits & AccessibilityTraits::Bar) != AccessibilityTraits::None) { + return NSAccessibilityMenuBarRole; + } + if ((accessibilityTraits & AccessibilityTraits::Item) != AccessibilityTraits::None) { + return NSAccessibilityMenuItemRole; + } + return NSAccessibilityMenuRole; + } + if ((accessibilityTraits & AccessibilityTraits::Radio) != AccessibilityTraits::None) { + if ((accessibilityTraits & AccessibilityTraits::Group) != AccessibilityTraits::None) { + return NSAccessibilityRadioGroupRole; + } + return NSAccessibilityRadioButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::ScrollBar) != AccessibilityTraits::None) { + return NSAccessibilityScrollBarRole; + } + if ((accessibilityTraits & AccessibilityTraits::SpinButton) != AccessibilityTraits::None) { + return NSAccessibilityIncrementorRole; + } + if ((accessibilityTraits & AccessibilityTraits::Tab) != AccessibilityTraits::None) { + if ((accessibilityTraits & AccessibilityTraits::List) != AccessibilityTraits::None) { + return NSAccessibilityTabGroupRole; + } + return NSAccessibilityButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::Disclosure) != AccessibilityTraits::None) { + return NSAccessibilityDisclosureTriangleRole; + } + if ((accessibilityTraits & AccessibilityTraits::Group) != AccessibilityTraits::None) { + return NSAccessibilityGroupRole; + } + if ((accessibilityTraits & AccessibilityTraits::List) != AccessibilityTraits::None) { + return NSAccessibilityListRole; + } + if ((accessibilityTraits & AccessibilityTraits::Table) != AccessibilityTraits::None) { + return NSAccessibilityTableRole; + } + return NSAccessibilityUnknownRole; }; #endif // macOS] 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 420a10852af33d..06e9fc9b94e11a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h @@ -34,6 +34,21 @@ enum class AccessibilityTraits : uint32_t { Header = (1 << 15), Switch = (1 << 16), TabBar = (1 << 17), +// [macOS + ComboBox = (1 << 18), + Menu = (1 << 19), + PopUp = (1 << 20), + Bar = (1 << 21), + Item = (1 << 22), + Group = (1 << 23), + List = (1 << 24), + Tab = (1 << 25), + Table = (1 << 26), + Disclosure = (1 << 27), + Radio = (1 << 28), + ScrollBar = (1 << 29), + SpinButton = (1 << 30), +// macOS] }; constexpr enum AccessibilityTraits operator|( From d8bc0e2042611da63c5a97338f6b099da808d67c Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 4 Aug 2025 16:52:21 -0700 Subject: [PATCH 4/5] exclude iOS only a11y props --- .../Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 2c789467999558..4bcb327463c63c 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -424,7 +424,6 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (oldViewProps.accessibilityElementsHidden != newViewProps.accessibilityElementsHidden) { self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden; } -#endif // [macOS] // `accessibilityShowsLargeContentViewer` if (oldViewProps.accessibilityShowsLargeContentViewer != newViewProps.accessibilityShowsLargeContentViewer) { @@ -445,6 +444,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.largeContentTitle = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLargeContentTitle); } } +#endif // [macOS] // `accessibilityTraits` if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) { From bbef9bb4bb5114be67d0d55357b083ef63aeccb9 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 13 Aug 2025 14:03:12 -0700 Subject: [PATCH 5/5] wip --- .../React/Fabric/RCTConversions.h | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/react-native/React/Fabric/RCTConversions.h b/packages/react-native/React/Fabric/RCTConversions.h index f6047c092e3d69..469fb965ac32af 100644 --- a/packages/react-native/React/Fabric/RCTConversions.h +++ b/packages/react-native/React/Fabric/RCTConversions.h @@ -83,6 +83,7 @@ inline UIAccessibilityTraits RCTUIAccessibilityTraitsFromAccessibilityTraits( if ((accessibilityTraits & AccessibilityTraits::Image) != AccessibilityTraits::None) { result |= UIAccessibilityTraitImage; } + // not in macos if ((accessibilityTraits & AccessibilityTraits::Selected) != AccessibilityTraits::None) { result |= UIAccessibilityTraitSelected; } @@ -98,24 +99,29 @@ inline UIAccessibilityTraits RCTUIAccessibilityTraitsFromAccessibilityTraits( if ((accessibilityTraits & AccessibilityTraits::SummaryElement) != AccessibilityTraits::None) { result |= UIAccessibilityTraitSummaryElement; } + // not on macos if ((accessibilityTraits & AccessibilityTraits::NotEnabled) != AccessibilityTraits::None) { result |= UIAccessibilityTraitNotEnabled; } + // not on macos if ((accessibilityTraits & AccessibilityTraits::UpdatesFrequently) != AccessibilityTraits::None) { result |= UIAccessibilityTraitUpdatesFrequently; } if ((accessibilityTraits & AccessibilityTraits::SearchField) != AccessibilityTraits::None) { result |= UIAccessibilityTraitSearchField; } + // not on macos if ((accessibilityTraits & AccessibilityTraits::StartsMediaSession) != AccessibilityTraits::None) { result |= UIAccessibilityTraitStartsMediaSession; } if ((accessibilityTraits & AccessibilityTraits::Adjustable) != AccessibilityTraits::None) { result |= UIAccessibilityTraitAdjustable; } + // not on macos if ((accessibilityTraits & AccessibilityTraits::AllowsDirectInteraction) != AccessibilityTraits::None) { result |= UIAccessibilityTraitAllowsDirectInteraction; } + // not on macOS if ((accessibilityTraits & AccessibilityTraits::CausesPageTurn) != AccessibilityTraits::None) { result |= UIAccessibilityTraitCausesPageTurn; } @@ -136,6 +142,7 @@ inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( { using AccessibilityTraits = facebook::react::AccessibilityTraits; if ((accessibilityTraits & AccessibilityTraits::Button) != AccessibilityTraits::None) { + // check if ((accessibilityTraits & AccessibilityTraits::Bar) != AccessibilityTraits::None) { return NSAccessibilityToolbarRole; } @@ -145,6 +152,7 @@ inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( if ((accessibilityTraits & AccessibilityTraits::Menu) != AccessibilityTraits::None) { return NSAccessibilityMenuButtonRole; } + // check end return NSAccessibilityButtonRole; } if ((accessibilityTraits & AccessibilityTraits::Link) != AccessibilityTraits::None) { @@ -162,6 +170,9 @@ inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( if ((accessibilityTraits & AccessibilityTraits::SummaryElement) != AccessibilityTraits::None) { return NSAccessibilityStaticTextRole; } + if ((accessibilityTraits & AccessibilityTraits::UpdatesFrequently) != AccessibilityTraits::None) { + return NSAccessibilityProgressIndicatorRole; + } if ((accessibilityTraits & AccessibilityTraits::SearchField) != AccessibilityTraits::None) { return NSAccessibilityTextFieldRole; } @@ -174,12 +185,11 @@ inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( if ((accessibilityTraits & AccessibilityTraits::Switch) != AccessibilityTraits::None) { return NSAccessibilityCheckBoxRole; } - if ((accessibilityTraits & AccessibilityTraits::UpdatesFrequently) != AccessibilityTraits::None) { - return NSAccessibilityProgressIndicatorRole; - } + // macos only if ((accessibilityTraits & AccessibilityTraits::ComboBox) != AccessibilityTraits::None) { return NSAccessibilityComboBoxRole; } + // macos only if ((accessibilityTraits & AccessibilityTraits::Menu) != AccessibilityTraits::None) { if ((accessibilityTraits & AccessibilityTraits::Bar) != AccessibilityTraits::None) { return NSAccessibilityMenuBarRole; @@ -201,11 +211,16 @@ inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( if ((accessibilityTraits & AccessibilityTraits::SpinButton) != AccessibilityTraits::None) { return NSAccessibilityIncrementorRole; } + // Align with ARIA: `tablist` -> TabGroup + if ((accessibilityTraits & AccessibilityTraits::TabBar) != AccessibilityTraits::None) { + return NSAccessibilityTabGroupRole; + } if ((accessibilityTraits & AccessibilityTraits::Tab) != AccessibilityTraits::None) { if ((accessibilityTraits & AccessibilityTraits::List) != AccessibilityTraits::None) { return NSAccessibilityTabGroupRole; } - return NSAccessibilityButtonRole; + // Align with ARIA mapping where `tab` maps to a RadioButton. + return NSAccessibilityRadioButtonRole; } if ((accessibilityTraits & AccessibilityTraits::Disclosure) != AccessibilityTraits::None) { return NSAccessibilityDisclosureTriangleRole;