From 80a3ed7d0c9b0ecd0cce6045672453887b5efbaf Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Fri, 12 Jul 2024 07:50:05 -0700 Subject: [PATCH] Fix Android AlertFragment Title Accessibility (#45395) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45395 ## Changelog: [Android] [Fixed] - Fix AlertDialog title accessibility Pull Request resolved: https://github.com/facebook/react-native/pull/45048 Making title accessible for android AlertFragment Reviewed By: blavalla, susnchen Differential Revision: D58684576 fbshipit-source-id: 83c8cfe9e7aacdf587d325d957694c3c7daa360c --- .../ReactAndroid/build.gradle.kts | 1 + .../react/modules/dialog/AlertFragment.java | 62 +++++++++++++++++-- .../views/alert/layout/alert_title_layout.xml | 22 +++++++ 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/res/views/alert/layout/alert_title_layout.xml diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 1efd8b930b92c2..31af37789763f9 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -653,6 +653,7 @@ android { listOf( "src/main/res/devsupport", "src/main/res/shell", + "src/main/res/views/alert", "src/main/res/views/modal", "src/main/res/views/uimanager")) java.exclude("com/facebook/annotationprocessors") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java index b5e48ae4ec1f27..00cd846162c37d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java @@ -12,11 +12,20 @@ import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; +import android.os.Build; import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.fragment.app.DialogFragment; +import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.R; /** A fragment used to display the dialog. */ @Nullsafe(Nullsafe.Mode.LOCAL) @@ -66,15 +75,55 @@ private static boolean isAppCompatTheme(Context activityContext) { return isAppCompat; } + /** + * Creates a custom dialog title View that has the role of "Heading" and focusable for + * accessibility purposes. + * + * @returns accessible TextView title + */ + private static View getAccessibleTitle(Context activityContext, String titleText) { + LayoutInflater inflater = LayoutInflater.from(activityContext); + + // This layout matches the sizing and styling of AlertDialog's title_template (minus the icon) + // since the whole thing gets tossed out when setting a custom title + View titleContainer = inflater.inflate(R.layout.alert_title_layout, null); + + TextView accessibleTitle = + Assertions.assertNotNull(titleContainer.findViewById(R.id.alert_title)); + accessibleTitle.setText(titleText); + accessibleTitle.setFocusable(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + accessibleTitle.setAccessibilityHeading(true); + } else { + ViewCompat.setAccessibilityDelegate( + accessibleTitle, + new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo( + View view, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(accessibleTitle, info); + info.setHeading(true); + } + }); + } + + return titleContainer; + } + /** * Creates a dialog compatible only with AppCompat activities. This function should be kept in * sync with {@link createAppDialog}. */ private static Dialog createAppCompatDialog( Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) { - AlertDialog.Builder builder = - new AlertDialog.Builder(activityContext).setTitle(arguments.getString(ARG_TITLE)); + AlertDialog.Builder builder = new AlertDialog.Builder(activityContext); + if (arguments.containsKey(ARG_TITLE)) { + String title = Assertions.assertNotNull(arguments.getString(ARG_TITLE)); + View accessibleTitle = getAccessibleTitle(activityContext, title); + builder.setCustomTitle(accessibleTitle); + } if (arguments.containsKey(ARG_BUTTON_POSITIVE)) { builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment); } @@ -104,10 +153,13 @@ private static Dialog createAppCompatDialog( */ private static Dialog createAppDialog( Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) { - android.app.AlertDialog.Builder builder = - new android.app.AlertDialog.Builder(activityContext) - .setTitle(arguments.getString(ARG_TITLE)); + android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(activityContext); + if (arguments.containsKey(ARG_TITLE)) { + String title = Assertions.assertNotNull(arguments.getString(ARG_TITLE)); + View accessibleTitle = getAccessibleTitle(activityContext, title); + builder.setCustomTitle(accessibleTitle); + } if (arguments.containsKey(ARG_BUTTON_POSITIVE)) { builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment); } diff --git a/packages/react-native/ReactAndroid/src/main/res/views/alert/layout/alert_title_layout.xml b/packages/react-native/ReactAndroid/src/main/res/views/alert/layout/alert_title_layout.xml new file mode 100644 index 00000000000000..0677d76b4b0601 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/res/views/alert/layout/alert_title_layout.xml @@ -0,0 +1,22 @@ + + + + + +