facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
118.27k stars 24.22k forks source link

Accessibility roles 'radio' & 'checkbox' not announced by screenreader on ios since 0.73.0 #43266

Open lutzk opened 6 months ago

lutzk commented 6 months ago

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

  1. install the app with yarn ios
  2. navigate with a screenreader or Accessibility inspector to the text Welcome to React Native and go down from there
  3. listen to the output - it does not announce roles for radio & checkbox - button is 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

NickGerleman commented 6 months ago

Bug added with https://github.com/facebook/react-native/commit/2d07d5f160efdd25f5b3dbfa65c13884df9f3117

Accidentally miscopied some mappings from previous JS shim.

NickGerleman commented 6 months ago

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.

NickGerleman commented 6 months ago

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.

lutzk commented 6 months ago

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

NickGerleman commented 6 months ago

Also FYI @Saadnajmi who needed to touch this recently for macOS.

pal-roy commented 4 months ago

@Saadnajmi Have you had a chance to look at it or @lutzk do we have any ideas here on a potential fix.

jacobarvidsson commented 4 months ago

This seems to apply to 'tab' role as well, but other roles like 'button' and 'header' works correctly.

Saadnajmi commented 4 months ago

@falselobster fyi as well

sladek-jan commented 4 months ago

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 ?

sladek-jan commented 3 months ago

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