Accessibility roles 'radio' & 'checkbox' not announced by screenreader on ios since 0.73.0
Description
since version v0.73.0 the roles for radio & checkbox are not announced by Screen readers on ios
this was working in v0.72.10 but stopped working in v0.73.0
Steps to reproduce
- install the app with
yarn ios - navigate with a screenreader or Accessibility inspector to the text
Welcome to React Nativeand go down from there - listen to the output - it does not announce roles for
radio&checkbox-buttonis still announced fine
React Native Version
0.73.5
Affected Platforms
Runtime - iOS
Output of npx react-native info
System:
OS: macOS 13.6.1
CPU: (10) arm64 Apple M1 Max
Memory: 1.12 GB / 32.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 20.10.0
path: ~/.nvm/versions/node/v20.10.0/bin/node
Yarn:
version: 1.22.21
path: ~/.nvm/versions/node/v20.10.0/bin/yarn
npm:
version: 10.2.3
path: ~/.nvm/versions/node/v20.10.0/bin/npm
Watchman:
version: 2023.11.06.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods: Not Found
SDKs:
iOS SDK:
Platforms:
- DriverKit 23.2
- iOS 17.2
- macOS 14.2
- tvOS 17.2
- visionOS 1.0
- watchOS 10.2
Android SDK: Not Found
IDEs:
Android Studio: 2022.3 AI-223.8836.35.2231.10811636
Xcode:
version: 15.2/15C500b
path: /usr/bin/xcodebuild
Languages:
Java:
version: 11.0.15
path: /Users/lutz.kwauka/.sdkman/candidates/java/current/bin/javac
Ruby:
version: 2.6.10
path: /Users/lutz.kwauka/.rbenv/shims/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.2.0
wanted: 18.2.0
react-native:
installed: 0.73.5
wanted: 0.73.5
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: false
Stacktrace or Logs
no crash happened
Reproducer
https://github.com/lutzk/accessibility-reproducer/
Screenshots and Videos
v0.72.10 working:
https://github.com/facebook/react-native/assets/793755/cec0dd90-6b7b-40d1-9fb5-9cd1d00e3f97
v0.73.5 not working:
https://github.com/facebook/react-native/assets/793755/c86794d2-f4fa-43df-835e-93315a63aa46
Bug added with https://github.com/facebook/react-native/commit/2d07d5f160efdd25f5b3dbfa65c13884df9f3117
Accidentally miscopied some mappings from previous JS shim.
Hmm, actually, I am seeing these were mapping to UIAccessibilityTraitNone before that change as well.
Let me check if they are read someone else. The blamed commit is potentially responsible for changes though.
Okay, these roles are specially handled here: https://github.com/facebook/react-native/blob/ec928d7a669fa2624bcf7da520041f140dd0fb03/packages/react-native/React/Views/RCTView.m#L343
There was another change around localization of these strings, but RCTLocalizedString does work a bit differently in OSS and internal (and the translation part has not been wired to external).
@lutzk would you be willing to take a look around this area, to see what might be happening in current OSS build?
IIRC I might have primarily tested this on new arch as well.
Thank you for looking into it! i can have a look into it but i am not experienced with objC so probably not of much help
Also FYI @Saadnajmi who needed to touch this recently for macOS.
@Saadnajmi Have you had a chance to look at it or @lutzk do we have any ideas here on a potential fix.
This seems to apply to 'tab' role as well, but other roles like 'button' and 'header' works correctly.
@falselobster fyi as well
My suspicion is that the if-checks added in 2d07d5f are too strict and they exclude valid cases:
UIAccessibilityTraits accessibilityRoleTraits = json ? [RCTConvert UIAccessibilityTraits:json] : UIAccessibilityTraitNone;
if (view.reactAccessibilityElement.accessibilityRoleTraits != accessibilityRoleTraits) {
view.accessibilityRoleTraits = accessibilityRoleTraits;
view.reactAccessibilityElement.accessibilityRole = json ? [RCTConvert NSString:json] : nil;
[...]
UIAccessibilityTraits roleTraits = json ? [RCTConvert UIAccessibilityTraits:json] : UIAccessibilityTraitNone;
if (view.reactAccessibilityElement.roleTraits != roleTraits) {
view.roleTraits = roleTraits;
view.reactAccessibilityElement.role = json ? [RCTConvert NSString:json] : nil;
[...]
Views such as radio have no matching trait in UIAccessibilityTraits, so the if-block won't be executed and neither role nor accessibilityRole will be set on view.reactAccessibilityElement.
As a result, they have no role and hence none is accounced by the SR.
could that be the case or am I missing something, @NickGerleman ?
Here is how I fixed it in case that someone finds it helpful:
diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m
index 9b4775ceec3c220c521cd8428af1da528bd688de..0cf1787d1a961ab385e97dc8d376e92835a10a19 100644
--- a/React/Views/RCTViewManager.m
+++ b/React/Views/RCTViewManager.m
@@ -231,7 +231,8 @@ - (RCTShadowView *)shadowView
{
UIAccessibilityTraits accessibilityRoleTraits =
json ? [RCTConvert UIAccessibilityTraits:json] : UIAccessibilityTraitNone;
- if (view.reactAccessibilityElement.accessibilityRoleTraits != accessibilityRoleTraits) {
+ NSString *roleString = json ? [RCTConvert NSString:json] : nil;
+ if (view.reactAccessibilityElement.accessibilityRoleTraits != accessibilityRoleTraits || view.reactAccessibilityElement.accessibilityRole != roleString) {
view.accessibilityRoleTraits = accessibilityRoleTraits;
view.reactAccessibilityElement.accessibilityRole = json ? [RCTConvert NSString:json] : nil;
[self updateAccessibilityTraitsForRole:view withDefaultView:defaultView];
@@ -241,7 +242,8 @@ - (RCTShadowView *)shadowView
RCT_CUSTOM_VIEW_PROPERTY(role, UIAccessibilityTraits, RCTView)
{
UIAccessibilityTraits roleTraits = json ? [RCTConvert UIAccessibilityTraits:json] : UIAccessibilityTraitNone;
- if (view.reactAccessibilityElement.roleTraits != roleTraits) {
+ NSString *roleString = json ? [RCTConvert NSString:json] : nil;
+ if (view.reactAccessibilityElement.roleTraits != roleTraits || view.reactAccessibilityElement.role != roleString) {
view.roleTraits = roleTraits;
view.reactAccessibilityElement.role = json ? [RCTConvert NSString:json] : nil;
[self updateAccessibilityTraitsForRole:view withDefaultView:defaultView];
Additionally, we needed to add key-value pairs to React/I18n/strings/<locale>.lproj/Localizable.strings so that roles are read out in the user's target language and not in English. Keys were determined by debugging the RCTLocalizedStringFromKey function, for example:
"rDy1kaz-"="alert"; // "alert"="alert" would not be picked up by RN