diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js index f4ea0d2c1f5219..88ba281370cfec 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/Libraries/Components/View/ReactNativeViewAttributes.js @@ -19,6 +19,7 @@ const UIView = { accessibilityLiveRegion: true, accessibilityRole: true, accessibilityState: true, + accessibilitySpan: true, accessibilityValue: true, accessibilityHint: true, accessibilityLanguage: true, diff --git a/Libraries/NativeComponent/BaseViewConfig.android.js b/Libraries/NativeComponent/BaseViewConfig.android.js index 5cd505439ea78b..6d4a61834d147e 100644 --- a/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/Libraries/NativeComponent/BaseViewConfig.android.js @@ -176,6 +176,7 @@ const validAttributesForNonEventProps = { accessibilityCollection: true, accessibilityCollectionItem: true, accessibilityState: true, + accessibilitySpan: true, accessibilityActions: true, accessibilityValue: true, importantForAccessibility: true, diff --git a/Libraries/Text/Text.d.ts b/Libraries/Text/Text.d.ts index a9d2a1b46a3a2d..d7394fb11bc4ed 100644 --- a/Libraries/Text/Text.d.ts +++ b/Libraries/Text/Text.d.ts @@ -56,6 +56,51 @@ export interface TextPropsIOS { } export interface TextPropsAndroid { + /** + * Used for nested Text accessibility announcements. + * The nested text accessibilityLabel should set to the values of: + * + * None https://developer.android.com/reference/android/text/style/TtsSpan#TYPE_TEXT + * The default type used when accessibilitySpan prop is not set (AccessibilitySpan.NONE) + * Adds the accessibilityLabel announcement on a Nested Text. + * This span type can be used to add morphosyntactic features to the text it spans over, + * or synthesize a something else than the spanned text. + * Use the argument ARG_TEXT to set a different text. + * https://developer.android.com/reference/android/text/style/TtsSpan#ARG_TEXT + * String supplying the text to be synthesized. + * The synthesizer is free to decide how to interpret the text. Can be used with TYPE_TEXT. + * + * Ordinal and Cardinal https://developer.android.com/reference/android/text/style/TtsSpan#ARG_NUMBER + * Argument used to specify a whole number. + * The value can be a string of digits of any size optionally prefixed with a - or +. + * Can be used with TYPE_CARDINAL and TYPE_ORDINAL. + * + * Measure refer to https://developer.android.com/reference/android/text/style/TtsSpan#ARG_UNIT + * Argument used to specify the unit of a measure. + * The unit should always be specified in English singular form. + * Prefixes may be used. Engines will do their best to pronounce them correctly in the language used. + * Engines are expected to at least support the most common ones like "meter", + * "second", "degree celsius" and "degree fahrenheit" with some common prefixes like "milli" and "kilo". + * Can be used with TYPE_MEASURE. + * + * Telephone refer to https://developer.android.com/reference/android/text/style/TtsSpan#ARG_NUMBER_PARTS + * Argument used to specify the main number part of a telephone number. + * Can be a string of digits where the different parts of the telephone + * number can be separated with a space, '-', '/' or '.'. + * Can be used with TYPE_TELEPHONE. + * + * Verbatim refer to https://developer.android.com/reference/android/text/style/TtsSpan#ARG_VERBATIM + * Argument used to specify a string where the characters are read verbatim, except whitespace. + * Can be used with TYPE_VERBATIM. + */ + accessibilitySpan?: + | 'none' + | 'cardinal' + | 'ordinal' + | 'measure' + | 'telephone' + | 'verbatim'; + /** * Specifies the disabled state of the text view for testing purposes. */ diff --git a/Libraries/Text/TextProps.js b/Libraries/Text/TextProps.js index 7971c37362d7cd..4cb691034664f6 100644 --- a/Libraries/Text/TextProps.js +++ b/Libraries/Text/TextProps.js @@ -200,6 +200,52 @@ export type TextProps = $ReadOnly<{| * Android Only */ + /** + * Used for nested Text accessibility announcements. + * The nested text accessibilityLabel should set to the values of: + * + * None https://developer.android.com/reference/android/text/style/TtsSpan#TYPE_TEXT + * The default type used when accessibilitySpan prop is not set (AccessibilitySpan.NONE) + * Adds the accessibilityLabel announcement on a Nested Text. + * This span type can be used to add morphosyntactic features to the text it spans over, + * or synthesize a something else than the spanned text. + * Use the argument ARG_TEXT to set a different text. + * https://developer.android.com/reference/android/text/style/TtsSpan#ARG_TEXT + * String supplying the text to be synthesized. + * The synthesizer is free to decide how to interpret the text. Can be used with TYPE_TEXT. + * + * Ordinal and Cardinal https://developer.android.com/reference/android/text/style/TtsSpan#ARG_NUMBER + * Argument used to specify a whole number. + * The value can be a string of digits of any size optionally prefixed with a - or +. + * Can be used with TYPE_CARDINAL and TYPE_ORDINAL. + * + * Measure refer to https://developer.android.com/reference/android/text/style/TtsSpan#ARG_UNIT + * Argument used to specify the unit of a measure. + * The unit should always be specified in English singular form. + * Prefixes may be used. Engines will do their best to pronounce them correctly in the language used. + * Engines are expected to at least support the most common ones like "meter", + * "second", "degree celsius" and "degree fahrenheit" with some common prefixes like "milli" and "kilo". + * Can be used with TYPE_MEASURE. + * + * Telephone refer to https://developer.android.com/reference/android/text/style/TtsSpan#ARG_NUMBER_PARTS + * Argument used to specify the main number part of a telephone number. + * Can be a string of digits where the different parts of the telephone + * number can be separated with a space, '-', '/' or '.'. + * Can be used with TYPE_TELEPHONE. + * + * Verbatim refer to https://developer.android.com/reference/android/text/style/TtsSpan#ARG_VERBATIM + * Argument used to specify a string where the characters are read verbatim, except whitespace. + * Can be used with TYPE_VERBATIM. + */ + accessibilitySpan?: ?( + | 'none' + | 'cardinal' + | 'ordinal' + | 'measure' + | 'telephone' + | 'verbatim' + ), + /** * Specifies the disabled state of the text view for testing purposes. * diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTtsSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTtsSpan.java new file mode 100644 index 00000000000000..53c71d51207523 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTtsSpan.java @@ -0,0 +1,176 @@ +/* + * 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. + */ + +package com.facebook.react.views.text; + +import android.os.PersistableBundle; +import android.text.style.TtsSpan; +import androidx.annotation.Nullable; +import com.facebook.common.logging.FLog; + +/* + * Used for nested Text accessibility announcements with + * props accessiblitySpan and accessibilityLabel. + * + * Wraps {@link TtsSpan} as a {@link ReactSpan}. + * A span that supplies additional meta-data for the associated text intended + * for text-to-speech engines. If the text is being processed by a + * text-to-speech engine, the engine may use the data in this span in addition + * to or instead of its associated text. + * + * Each instance of a TtsSpan has a type, for example {@link #TYPE_DATE} + * or {@link #TYPE_MEASURE}. And a list of arguments, provided as + * key-value pairs in a bundle. + * + * The inner classes are there for convenience and provide builders for each + * TtsSpan type. + */ +public class ReactTtsSpan extends TtsSpan implements ReactSpan { + private static final String TAG = ReactTtsSpan.class.getSimpleName(); + private static final String TYPE_TELEPHONE_WARNING_MSG = + "Failed to retrieve telephone number (for example '0112123432')."; + private static final String TYPE_MEASURE_WARNING_MSG = + "Failed to retrieve unit type (for ex. meter, second, milli)."; + + public ReactTtsSpan(String type, PersistableBundle args) { + super(type, args); + } + + // https://developer.android.com/reference/android/text/style/TtsSpan + public enum AccessibilitySpan { + NONE, + CARDINAL, + ORDINAL, + MEASURE, + TELEPHONE, + VERBATIM; + + public static String getValue(AccessibilitySpan accessibilitySpan) { + switch (accessibilitySpan) { + case CARDINAL: + return ReactTtsSpan.TYPE_CARDINAL; + case ORDINAL: + return ReactTtsSpan.TYPE_ORDINAL; + case MEASURE: + return ReactTtsSpan.TYPE_MEASURE; + case TELEPHONE: + return ReactTtsSpan.TYPE_TELEPHONE; + case VERBATIM: + return ReactTtsSpan.TYPE_VERBATIM; + case NONE: + return ReactTtsSpan.TYPE_TEXT; + default: + throw new IllegalArgumentException( + "Invalid accessibility span value: " + accessibilitySpan); + } + } + + public static AccessibilitySpan fromValue(@Nullable String value) { + for (AccessibilitySpan accessibilitySpan : AccessibilitySpan.values()) { + if (accessibilitySpan.name().equalsIgnoreCase(value)) { + return accessibilitySpan; + } + } + throw new IllegalArgumentException("Invalid accessibility role value: " + value); + } + } + + public static class Builder { + private String mType; + private final PersistableBundle mArgs = new PersistableBundle(); + + public Builder(String type) { + mType = type; + } + + public Builder(AccessibilitySpan type, @Nullable String accessibilityLabel) { + mType = AccessibilitySpan.getValue(type); + String warningMessage = ""; + if (accessibilityLabel == null) { + return; + } + try { + /* + * The default type used when accessibilitySpan prop is not set (AccessibilitySpan.NONE) + * Adds the accessibilityLabel announcement on a Nested Text. + * + * https://developer.android.com/reference/android/text/style/TtsSpan#TYPE_TEXT + * This span type can be used to add morphosyntactic features to the text it spans over, + * or synthesize a something else than the spanned text. + * Use the argument ARG_TEXT to set a different text. + * + * https://developer.android.com/reference/android/text/style/TtsSpan#ARG_TEXT + * String supplying the text to be synthesized. + * The synthesizer is free to decide how to interpret the text. Can be used with TYPE_TEXT. + */ + if (mType.equals(TYPE_TEXT)) { + setStringArgument(ARG_TEXT, accessibilityLabel); + } + /* + *
Telephone refer to + * https://developer.android.com/reference/android/text/style/TtsSpan#ARG_NUMBER_PARTS + * + *
Argument used to specify the main number part of a telephone number. Can be a string of + * digits where the different parts of the telephone number can be separated with a space, '-', + * '/' or '.'. Can be used with TYPE_TELEPHONE. + */ + if (mType.equals(TYPE_TELEPHONE)) { + warningMessage = TYPE_TELEPHONE_WARNING_MSG; + setStringArgument(ARG_NUMBER_PARTS, accessibilityLabel); + } + /* + *
Measure refer to + * https://developer.android.com/reference/android/text/style/TtsSpan#ARG_UNIT + * + *
Argument used to specify the unit of a measure. The unit should always be specified in + * English singular form. Prefixes may be used. Engines will do their best to pronounce them + * correctly in the language used. Engines are expected to at least support the most common ones + * like "meter", "second", "degree celsius" and "degree fahrenheit" with some common prefixes + * like "milli" and "kilo". Can be used with TYPE_MEASURE. + */ + if (mType.equals(TYPE_MEASURE)) { + warningMessage = TYPE_MEASURE_WARNING_MSG; + setStringArgument(ARG_UNIT, accessibilityLabel); + } + /* + *
Ordinal and Cardinal + * https://developer.android.com/reference/android/text/style/TtsSpan#ARG_NUMBER + * + *
Argument used to specify a whole number. The value can be a string of digits of any size
+ * optionally prefixed with a - or +. Can be used with TYPE_CARDINAL and TYPE_ORDINAL.
+ */
+ if (mType.equals(TYPE_CARDINAL) || mType.equals(TYPE_ORDINAL)) {
+ setStringArgument(ARG_NUMBER, accessibilityLabel);
+ }
+ } catch (Exception e) {
+ // fallback and use accessibilityLabel as text
+ if (mType != TYPE_TEXT) {
+ mType = TYPE_TEXT;
+ setStringArgument(ARG_TEXT, accessibilityLabel);
+ }
+ FLog.w(
+ TAG,
+ "Failed to create Builder with params type: "
+ + type
+ + " and accessibilityLabel: "
+ + accessibilityLabel
+ + " "
+ + warningMessage
+ + "Error: "
+ + e);
+ }
+ }
+
+ public ReactTtsSpan build() {
+ return new ReactTtsSpan(mType, mArgs);
+ }
+
+ public void setStringArgument(String arg, String value) {
+ mArgs.putString(arg, value);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java
index 787ab9bb14db4a..eaa8e090fac8c7 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java
@@ -21,6 +21,7 @@
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.views.text.ReactTtsSpan.AccessibilitySpan;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -53,6 +54,8 @@ public class TextAttributeProps {
public static final short TA_KEY_IS_HIGHLIGHTED = 20;
public static final short TA_KEY_LAYOUT_DIRECTION = 21;
public static final short TA_KEY_ACCESSIBILITY_ROLE = 22;
+ public static final short TA_KEY_ACCESSIBILITY_SPAN = 24;
+ public static final short TA_KEY_ACCESSIBILITY_LABEL = 25;
public static final int UNSET = -1;
@@ -102,6 +105,8 @@ public class TextAttributeProps {
protected @Nullable ReactAccessibilityDelegate.AccessibilityRole mAccessibilityRole = null;
protected boolean mIsAccessibilityRoleSet = false;
+ protected AccessibilitySpan mAccessibilitySpan = AccessibilitySpan.NONE;
+ protected @Nullable String mAccessibilityLabel = null;
protected boolean mIsAccessibilityLink = false;
protected int mFontStyle = UNSET;
@@ -205,6 +210,12 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) {
case TA_KEY_ACCESSIBILITY_ROLE:
result.setAccessibilityRole(entry.getStringValue());
break;
+ case TA_KEY_ACCESSIBILITY_SPAN:
+ result.setAccessibilitySpan(entry.getStringValue());
+ break;
+ case TA_KEY_ACCESSIBILITY_LABEL:
+ result.setAccessibilityLabel(entry.getStringValue());
+ break;
}
}
@@ -609,6 +620,18 @@ private void setAccessibilityRole(@Nullable String accessibilityRole) {
}
}
+ private void setAccessibilitySpan(@Nullable String accessibilitySpan) {
+ if (accessibilitySpan != null) {
+ mAccessibilitySpan = AccessibilitySpan.fromValue(accessibilitySpan);
+ }
+ }
+
+ private void setAccessibilityLabel(@Nullable String accessibilityLabel) {
+ if (accessibilityLabel != null) {
+ mAccessibilityLabel = accessibilityLabel;
+ }
+ }
+
public static int getTextBreakStrategy(@Nullable String textBreakStrategy) {
int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY;
if (textBreakStrategy != null) {
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java
index 9ff17c4359fbe1..0c5018a076288c 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java
@@ -30,6 +30,7 @@
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.common.mapbuffer.MapBuffer;
import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.views.text.ReactTtsSpan.AccessibilitySpan;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
@@ -143,6 +144,17 @@ private static void buildSpannableFromFragment(
if (textAttributes.mIsAccessibilityLink) {
ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag)));
}
+ boolean hasAccessibilitySpan =
+ textAttributes.mAccessibilitySpan != null
+ && textAttributes.mAccessibilitySpan != AccessibilitySpan.NONE;
+ boolean hasAccessibilityLabel =
+ textAttributes.mAccessibilitySpan != null && textAttributes.mAccessibilityLabel != null;
+ if (hasAccessibilitySpan || hasAccessibilityLabel) {
+ ReactTtsSpan.Builder builder =
+ new ReactTtsSpan.Builder(
+ textAttributes.mAccessibilitySpan, textAttributes.mAccessibilityLabel);
+ ops.add(new SetSpanOperation(start, end, builder.build()));
+ }
if (textAttributes.mIsColorSet) {
ops.add(
new SetSpanOperation(
diff --git a/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp
index 63b5a4aab19fce..cce4b85c012a70 100644
--- a/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp
+++ b/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp
@@ -101,6 +101,10 @@ void TextAttributes::apply(TextAttributes textAttributes) {
accessibilityRole = textAttributes.accessibilityRole.has_value()
? textAttributes.accessibilityRole
: accessibilityRole;
+ accessibilitySpan = textAttributes.accessibilitySpan.has_value() ? textAttributes.accessibilitySpan
+ : accessibilitySpan;
+ accessibilityLabel = !textAttributes.accessibilityLabel.empty() ? textAttributes.accessibilityLabel
+ : accessibilityLabel;
}
#pragma mark - Operators
@@ -126,6 +130,8 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const {
isHighlighted,
layoutDirection,
accessibilityRole,
+ accessibilitySpan,
+ accessibilityLabel,
textTransform) ==
std::tie(
rhs.foregroundColor,
@@ -147,6 +153,8 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const {
rhs.isHighlighted,
rhs.layoutDirection,
rhs.accessibilityRole,
+ rhs.accessibilitySpan,
+ rhs.accessibilityLabel,
rhs.textTransform) &&
floatEquality(opacity, rhs.opacity) &&
floatEquality(fontSize, rhs.fontSize) &&
@@ -215,6 +223,8 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const {
debugStringConvertibleItem("isHighlighted", isHighlighted),
debugStringConvertibleItem("layoutDirection", layoutDirection),
debugStringConvertibleItem("accessibilityRole", accessibilityRole),
+ debugStringConvertibleItem("accessibilitySpan", accessibilitySpan),
+ debugStringConvertibleItem("accessibilityLabel", accessibilityLabel),
};
}
#endif
diff --git a/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/ReactCommon/react/renderer/attributedstring/TextAttributes.h
index f53ad73f60e065..ac69e40523f854 100644
--- a/ReactCommon/react/renderer/attributedstring/TextAttributes.h
+++ b/ReactCommon/react/renderer/attributedstring/TextAttributes.h
@@ -81,6 +81,8 @@ class TextAttributes : public DebugStringConvertible {
// construction.
std::optional