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 layoutDirection{}; std::optional accessibilityRole{}; + std::optional accessibilitySpan{}; + std::string accessibilityLabel{}; #pragma mark - Operations @@ -133,7 +135,9 @@ struct hash { textAttributes.textShadowColor, textAttributes.isHighlighted, textAttributes.layoutDirection, - textAttributes.accessibilityRole); + textAttributes.accessibilityRole, + textAttributes.accessibilitySpan, + textAttributes.accessibilityLabel); } }; } // namespace std diff --git a/ReactCommon/react/renderer/attributedstring/conversions.h b/ReactCommon/react/renderer/attributedstring/conversions.h index f585794c678594..6e9222ad449430 100644 --- a/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/ReactCommon/react/renderer/attributedstring/conversions.h @@ -786,6 +786,62 @@ inline void fromRawValue( result = AccessibilityRole::None; } +inline std::string toString(const AccessibilitySpan &accessibilitySpan) { + switch (accessibilitySpan) { + case AccessibilitySpan::Cardinal: + return "cardinal"; + case AccessibilitySpan::Ordinal: + return "ordinal"; + case AccessibilitySpan::Measure: + return "measure"; + case AccessibilitySpan::Telephone: + return "telephone"; + case AccessibilitySpan::Verbatim: + return "verbatim"; + case AccessibilitySpan::None: + return "none"; + } + + LOG(ERROR) << "Unsupported AccessibilitySpan value"; + react_native_assert(false); + // sane default for prod + return "none"; +} + +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + AccessibilitySpan &result) { + react_native_assert(value.hasType()); + if (value.hasType()) { + auto string = (std::string)value; + if (string == "cardinal") { + result = AccessibilitySpan::Cardinal; + } else if (string == "ordinal") { + result = AccessibilitySpan::Ordinal; + } else if (string == "measure") { + result = AccessibilitySpan::Measure; + } else if (string == "telephone") { + result = AccessibilitySpan::Telephone; + } else if (string == "verbatim") { + result = AccessibilitySpan::Verbatim; + } else if (string == "none") { + result = AccessibilitySpan::None; + } else { + LOG(ERROR) << "Unsupported AccessibilitySpan value: " << string; + react_native_assert(false); + // sane default for prod + result = AccessibilitySpan::None; + } + return; + } + + LOG(ERROR) << "Unsupported AccessibilitySpan type"; + react_native_assert(false); + // sane default for prod + result = AccessibilitySpan::None; +} + inline std::string toString(const HyphenationFrequency &hyphenationFrequency) { switch (hyphenationFrequency) { case HyphenationFrequency::None: @@ -1036,6 +1092,14 @@ inline folly::dynamic toDynamic(const TextAttributes &textAttributes) { _textAttributes( "accessibilityRole", toString(*textAttributes.accessibilityRole)); } + if (textAttributes.accessibilitySpan.has_value()) { + _textAttributes( + "accessibilitySpan", toString(*textAttributes.accessibilitySpan)); + } + if (!textAttributes.accessibilityLabel.empty()) { + _textAttributes( + "accessibilityLabel", textAttributes.accessibilityLabel); + } return _textAttributes; } @@ -1110,6 +1174,8 @@ constexpr static MapBuffer::Key TA_KEY_IS_HIGHLIGHTED = 20; constexpr static MapBuffer::Key TA_KEY_LAYOUT_DIRECTION = 21; constexpr static MapBuffer::Key TA_KEY_ACCESSIBILITY_ROLE = 22; constexpr static MapBuffer::Key TA_KEY_LINE_BREAK_STRATEGY = 23; +constexpr static MapBuffer::Key TA_KEY_ACCESSIBILITY_SPAN = 24; +constexpr static MapBuffer::Key TA_KEY_ACCESSIBILITY_LABEL = 25; // constants for ParagraphAttributes serialization constexpr static MapBuffer::Key PA_KEY_MAX_NUMBER_OF_LINES = 0; @@ -1256,6 +1322,14 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) { builder.putString( TA_KEY_ACCESSIBILITY_ROLE, toString(*textAttributes.accessibilityRole)); } + if (textAttributes.accessibilitySpan.has_value()) { + builder.putString( + TA_KEY_ACCESSIBILITY_SPAN, toString(*textAttributes.accessibilitySpan)); + } + if (!textAttributes.accessibilityLabel.empty()) { + builder.putString( + TA_KEY_ACCESSIBILITY_LABEL, textAttributes.accessibilityLabel); + } return builder.build(); } diff --git a/ReactCommon/react/renderer/attributedstring/primitives.h b/ReactCommon/react/renderer/attributedstring/primitives.h index f3ef237c9a829f..fbfcd948d81108 100644 --- a/ReactCommon/react/renderer/attributedstring/primitives.h +++ b/ReactCommon/react/renderer/attributedstring/primitives.h @@ -137,6 +137,15 @@ enum class AccessibilityRole { Toolbar, }; +enum class AccessibilitySpan { + None, + Cardinal, + Ordinal, + Measure, + Telephone, + Verbatim, +}; + enum class TextTransform { None, Uppercase, @@ -232,6 +241,13 @@ struct hash { } }; +template <> +struct hash { + size_t operator()(const facebook::react::AccessibilitySpan &v) const { + return hash()(static_cast(v)); + } +}; + template <> struct hash { size_t operator()(const facebook::react::TextTransform &v) const { diff --git a/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/ReactCommon/react/renderer/components/text/BaseTextProps.cpp index e098e7beab2404..7f6abadde879f5 100644 --- a/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/ReactCommon/react/renderer/components/text/BaseTextProps.cpp @@ -183,6 +183,20 @@ static TextAttributes convertRawProp( sourceTextAttributes.accessibilityRole, defaultTextAttributes.accessibilityRole); + textAttributes.accessibilitySpan = convertRawProp( + context, + rawProps, + "accessibilitySpan", + sourceTextAttributes.accessibilitySpan, + defaultTextAttributes.accessibilitySpan); + + textAttributes.accessibilityLabel = convertRawProp( + context, + rawProps, + "accessibilityLabel", + sourceTextAttributes.accessibilityLabel, + defaultTextAttributes.accessibilityLabel); + // Color (accessed in this order by ViewProps) textAttributes.opacity = convertRawProp( context, @@ -293,6 +307,18 @@ void BaseTextProps::setProp( textAttributes, accessibilityRole, "accessibilityRole"); + REBUILD_FIELD_SWITCH_CASE( + defaults, + value, + textAttributes, + accessibilitySpan, + "accessibilitySpan"); + REBUILD_FIELD_SWITCH_CASE( + defaults, + value, + textAttributes, + accessibilityLabel, + "accessibilityLabel"); REBUILD_FIELD_SWITCH_CASE( defaults, value, textAttributes, opacity, "opacity"); REBUILD_FIELD_SWITCH_CASE( diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index 268550b80624eb..d793edc042b646 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -95,6 +95,9 @@ const styles = StyleSheet.create({ textAlign: 'center', backgroundColor: '#000000c0', }, + redBackground: { + backgroundColor: 'red', + }, scrollView: { height: 50, }, @@ -281,6 +284,146 @@ class AccessibilityExample extends React.Component<{}> { } } +class TtsSpanExamples extends React.Component<{}> { + render(): React.Node { + return ( + <> + + + This text spells as{' '} + + react-native + + + + + + Verbatim spells as{' '} + + react-native + + + + + + Dates are spelled as{' '} + + 02/07/1987 + + + + + + Unit of Measure like meters are spelled as 60 + + s + {' '} + or 10 + + m + {' '} + or 10 + + m + + + + + + Fraction is spelled as + + 1/2 + + + + + + Cardinal number is spelled as{' '} + + 1 + + + + + + Ordinal number is spelled as{' '} + + 1 + + + + + + Digits is spelled as{' '} + + 123450 + + + + + + Money is spelled{' '} + + 1.000.000 IDR + + + + + + Time is spelled{' '} + + 11:30 + + + + + + The telephone is{' '} + + 0118888888 + + + + + ); + } +} + class AutomaticContentGrouping extends React.Component<{}> { render(): React.Node { return ( @@ -1600,6 +1743,12 @@ exports.title = 'Accessibility'; exports.documentationURL = 'https://reactnative.dev/docs/accessibilityinfo'; exports.description = 'Examples of using Accessibility APIs.'; exports.examples = [ + { + title: 'TtsSpan Examples', + render(): React.Element { + return ; + }, + }, { title: 'Accessibility expanded', render(): React.Element {