From 2490cab52d989c9ad8f4205af6d150fe6d788b66 Mon Sep 17 00:00:00 2001 From: fabriziobertoglio1987 Date: Tue, 28 Feb 2023 16:05:57 -0800 Subject: [PATCH] 1/2 TextInput accessibilityErrorMessage (Talkback, Android) (#33468) Summary: **Android**: The functionality consists of calling the [AccessibilityNodeInfo#setError][10] and [#setContentInvalid][13] method to display the error message in the TextInput. **Fixes [https://github.com/facebook/react-native/issues/30848][51] - Adding an accessibilityErrorMessage prop to the TextInput Component**: **Android**: The prop accessibilityErrorMessage triggers the AccessibilityNodeInfo method [setError][10] which automatically sets the correct properties on the AccessibilityNodeInfo that will inform screen readers of this state. The method calls setContentInvalid(true) and setError(youErrorString) on the AccessibilityNodeInfo. **Fixes [https://github.com/facebook/react-native/issues/30859][52] - Detecting changes in the Error state (text inputs)** **Fabric - Android** - Adding accessibilityErrorMessage to field AndroidTextInputState. ReactTextInputManager and ReactEditText receive state updates both from [Javascript][32] and [cpp (fabric)][34]. - accessibilityErrorMessage is added to the fabric AndroidTextInputState field - The updates are received in the ReactAndroid API with method updateState from ReactTextInputManager - After updating the TextInput text with onChangeText, the update of the accessibilityErrorMessage is triggered with method maybeSetAccessibilityError which triggers [setError][10]. More info: - An explanation of [state updates between fabric and ReactAndroid for the TextInput component][34] - [ReactNative renderer state updates][35] **Paper - Android** - Adding accessibilityErrorMessage to ReactTextInputShadowNode to trigger updates in Paper renderer when accessibilityErrorMessage is changed within the onChange callback. Related Links (Android): - [In this diff I'm shipping and deleting mapBufferSerialization for Text measurement][101] - [This diff implement and integrates Mapbuffer into Fabric text measure system][39] - [Refactor ViewPropsMapBuffer -> general MapBuffer props mechanism][100] - [TextInput: support modifying TextInputs with multiple Fragments (Cxx side)][24] - [TextInput: keep C++ state in-sync with updated AttributedStrings in Java][23] - [AccessibilityNodeInfo#setError][11] - [Explanation on how TextInput calls SET_TEXT_AND_SELECTION in Java API][32] - [Fabric: convertRawProp was extended to accept an optional default value][27] - [understanding onChangeText callback][31] - [Editable method replace()][12] - [Change of error state from onChangeText show/hides a TextInput error][30] - [AndroidTextInput: support using commands instead of setNativeProps (native change)][25] - [TextInput: support editing completely empty TextInputs][26] - [[Android] Fix letters duplication when using autoCapitalize https://github.com/facebook/react-native/issues/29070][40] - [Support optional types for C++ TurboModules][28] - [discussion on using announceForAccessibility in ReactEditText][36] - [ fix annoucement delayed to next character][61] - [Announce accessibility state changes happening in the background][29] - [Refactor MountingManager into MountingManager + SurfaceMountingManager][37] iOS Functionalities are included in separate PR https://github.com/facebook/react-native/pull/35908 Documentation PR https://github.com/facebook/react-native-website/pull/3010 Next PR [2/2 TextInput accessibilityErrorMessage (VoiceOver, iOS) https://github.com/facebook/react-native/issues/35908](https://github.com/facebook/react-native/pull/35908) Related https://github.com/facebook/react-native-deprecated-modules/pull/18 ## Changelog [Android] [Added] - Adding TextInput prop accessibilityErrorMessage to announce with TalkBack screenreaders Pull Request resolved: https://github.com/facebook/react-native/pull/33468 Test Plan: **Android - 20 Jan 2023** https://github.com/facebook/react-native/pull/33468#issuecomment-1398228674 **iOS - 20 Jan 2023** https://github.com/facebook/react-native/pull/33468#issuecomment-1398249006
CLICK TO OPEN OLD VIDEO TEST CASES

**PR Branch - Android and iOS 24th June** [88]: Android - accessibilityValue announces correctly with/out errorMessage set with onChangeText or with outside event (Fabric) ([link][88]) **PR Branch - Android** [1]. Test Cases of the functionality (Fabric) ([link][1]) [2]. Test Cases of the functionality (Paper) ([link][2]) **Main Branch** [6]. Android - Runtime Error in main branch when passing value of 1 to TextInput placeholder prop ([link][6]) **Issues Solved** [7]. TalkBack error does not clear error on the next typed character when using onChangeText ([link][7]) **Other Tests** [8]. Setting the TextInput errorMessage state with setTextAndSelection Java API from JavaScript ([link][8]) [9]. Setting the TextInput errorMessage state from fabric TextInput internal state to Java ReactTextUpdate API ([link][9])

[1]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1072101477 "Test Cases of the functionality (Android - Fabric)" [2]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1105964322 "Test Cases of the functionality (Android - Paper)" [3]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1116329282 "Test Cases of the functionality (iOS - Fabric)" [6]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1087020844 "Runtime Error in main branch when passing value of 1 to TextInput placeholder prop" [7]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1096086753 "TalkBack error announcement done on next typed character with onChangeText" [8]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1082594363 "setting the TextInput errorMessage state with setTextAndSelection Java API from JavaScript" [9]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1082598745 "Setting the TextInput errorMessage state from fabric TextInput internal state to Java ReactTextUpdate API" [10]: https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo#setError(java.lang.CharSequence) "AOSP setError" [11]: https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo#setError(java.lang.CharSequence) "AccessibilityNodeInfo#setError" [12]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/text/Editable.java#L28-L52 "Editable method replace" [13]: https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo#setContentInvalid(boolean) "setContentInvalid" [20]: https://github.com/facebook/react-native/commit/60b6c9be8e811241039a6db5dc906a0e88e6ba82 "draft implementation of android_errorMessage " [21]: https://github.com/facebook/react-native/commit/012d92d0b7e5de2436f186cdbff32ba128e537d5 "add errorMessage to ReactTextUpdate and maybeSetAccessibilityError" [22]: https://github.com/fabriziobertoglio1987/react-native/commit/cad239bded5748753cee2266c27809e24c6199fb "rename android_errorMessage to errorMessageAndroid" [23]: https://github.com/fabriziobertoglio1987/react-native/commit/0bae47434ef79eb606c453c5be8105b8df00783a "TextInput: keep C++ state in-sync with updated AttributedStrings in Java" [24]: https://github.com/fabriziobertoglio1987/react-native/commit/0556e86d09404105dc7ff695686b8b7c01911c5c "TextInput: support modifying TextInputs with multiple Fragments (Cxx side)" [25]: https://github.com/fabriziobertoglio1987/react-native/commit/7ab5eb4cafdea695c4c53ce2a737f6302afd6380 "AndroidTextInput: support using commands instead of setNativeProps (native change)" [26]: https://github.com/fabriziobertoglio1987/react-native/commit/b9491b7c5104066b2714045cd7710f995458c9e9 "TextInput: support editing completely empty TextInputs" [27]: https://github.com/fabriziobertoglio1987/react-native/commit/7f1ed6848f89bdccc7f7a5cc76019eec67e76b2f "Fabric: convertRawProp was extended to accept an optional default value" [28]: https://github.com/facebook/react-native/commit/6e0fa5f15eef71abcfb47750eb3669065ba2ab7d "Support optional types for C++ TurboModules" [29]: https://github.com/fabriziobertoglio1987/react-native/commit/baa66f63d8af2b772dea8ff8eda50eba264c3faf "Announce accessibility state changes happening in the background" [30]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1071989570 "Change of error state from onChangeText show/hides a TextInput error" [31]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1074827746 "understanding onChangeText callback" [32]: https://github.com/facebook/react-native/issues/29063#issuecomment-658189938 "Explanation on how TextInput calls SET_TEXT_AND_SELECTION in Java API" [33]: https://github.com/facebook/react-native/pull/33468#discussion_r835036889 "Explanation of TextInput state management with fabric C++ and JAVA API" [34]: https://github.com/facebook/react-native/pull/33468#discussion_r835036889 "state updates between fabric and ReactAndroid for the TextInput component" [35]: https://reactnative.dev/architecture/render-pipeline#react-native-renderer-state-updates "ReactNative renderer state updates" [35]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1080144483 "Analysis on how AndroidTextInputState.cpp sends updates to ReactTextInputManager" [36]: https://github.com/facebook/react-native/pull/33468#discussion_r848162849 "discussion on using announceForAccessibility in ReactEditText" [37]: https://github.com/fabriziobertoglio1987/react-native/commit/29eb632f1cb2ef5459253783eac43e5d7e999742 "Refactor MountingManager into MountingManager + SurfaceMountingManager" [38]: https://github.com/fabriziobertoglio1987/react-native/commit/733f2285067de401b925195266f4cec84c3f7fef "Diff C++ props for Android consumption" [39]: https://github.com/fabriziobertoglio1987/react-native/commit/91b3f5d48aa1322046b8c5335f8e2e1a5e702b67 "This diff implement and integrates Mapbuffer into Fabric text measure system" [40]: https://github.com/facebook/react-native/pull/29070 "[Android] Fix letters duplication when using autoCapitalize https://github.com/facebook/react-native/issues/29070" [50]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12 "Notes from work on iOS/Android: Text input error for screenreaders https://github.com/facebook/react-native/issues/12" [51]: https://github.com/facebook/react-native/issues/30848 "iOS/Android: Text input error for screenreaders https://github.com/facebook/react-native/issues/30848" [52]: https://github.com/facebook/react-native/issues/30859 "Android: Error state change (text inputs) https://github.com/facebook/react-native/issues/30859" [61]: https://github.com/facebook/react-native/pull/33468/commits/eb33c933c8bcb9a8421a6acdb7a51f261121be45 "fix annoucement delayed to next character" [70]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1116966512 "iOS - Paper renderer does not update the accessibilityValue" [71]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1124631221 "Test Cases of the functionality (Fabric) after removing changes to .cpp libs" [72]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1124892802 "Test Cases of the functionality (Paper) after removing changes to .cpp libs" [73]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1132830758 "iOS - announcing error onChangeText and screenreader focus" [74]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1150657065 "iOS - The screenreader announces the TextInput value after the errorMessage is cleared" [75]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/12#issuecomment-1152285978 "iOS - Exception thrown while executing UI block: - [RCTTextView setOnAccessibiltyAction:]: unrecognized selector sent to instance (Paper) (main branch)" [76]: https://github.com/facebook/react-native/issues/30859#issuecomment-1158790381 "iOS - announce lastChar (not entire text) onChangeText and avoid multiple announcements (Fabric)" [77]: https://github.com/facebook/react-native/issues/30859#issuecomment-1158794863 "iOS - announces or does not announce the accessibilityError through Button onPress (not onChangeText) (Fabric)" [78]: https://github.com/facebook/react-native/issues/30859#issuecomment-1158797801 "iOS - the error is announced with accessibilityInvalid true and does not clear after typing text (onChangeText) (Fabric)" [79]: https://github.com/facebook/react-native/issues/30848#issuecomment-1162799299 "iOS - Exception thrown while executing UI block: - RCTUITextView setAccessibilityErrorMessage:]: unrecognized selector sent to instance (iOS - Paper on main branch)" [80]: https://github.com/fabriziobertoglio1987/react-native/commit/e13b9c6e49480e8262df06b7c1e99caab74e801f "RCTTextField was spliited into two classes" [81]: https://github.com/fabriziobertoglio1987/react-native/commit/ee9697e5155aa972564d5aac90ceeb9db100750d "Introducing RCTBackedTextInputDelegate" [82]: https://github.com/fabriziobertoglio1987/react-native/commit/2dd2529b3ab3ace39136a6e24c09f80ae421a17e "Add option to hide context menu for TextInput" [83]: https://github.com/fabriziobertoglio1987/react-native/blob/343eea1e3150cf54d6f7727cd01d13eb7247c7f7/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm#L48-L72 "RCTParagraphComponentAccessibilityProvider accessibilityElements" [84]: https://github.com/fabriziobertoglio1987/react-native/blob/c8790a114f6f21774c43f0e9b9210e7b35d1c243/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm#L613 "RCTTextInputComponentView method _setAttributedString" [85]: https://github.com/fabriziobertoglio1987/react-native/blob/c8790a114f6f21774c43f0e9b9210e7b35d1c243/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm#L146 "RCTTextInputComponentView method updateProps" [86]: https://github.com/fabriziobertoglio1987/react-native/blob/c8790a114f6f21774c43f0e9b9210e7b35d1c243/Libraries/Text/TextInput/RCTBaseTextInputView.m#L150 "RCTBaseTextInputView setAttributedText" [87]: https://github.com/facebook/react-native/issues/30859#issuecomment-1165395361 "iOS - accessibilityValue announces correctly with/out errorMessage set with onChangeText or with outside event" [88]: https://github.com/facebook/react-native/issues/30859#issuecomment-1165398153 "Android - accessibilityValue announces correctly with/out errorMessage set with onChangeText or with outside event" [89]: https://github.com/facebook/react-native/issues/30859#issuecomment-1165413245 "iOS - accessibilityValue announces correctly with/out errorMessage set with onChangeText or with outside event (Fabric)" [100]: https://github.com/fabriziobertoglio1987/react-native/commit/110b191b14e3cb692bb6a33f0f129b4f0215f9a6 "Refactor ViewPropsMapBuffer -> general MapBuffer props mechanism" [101]: https://github.com/fabriziobertoglio1987/react-native/commit/22b6e1c8ec0e69700e9142cf5c9c1ab1e6a84b78 "In this diff I'm shipping and deleting mapBufferSerialization for Text measurement" Reviewed By: blavalla Differential Revision: D38410635 Pulled By: lunaleaps fbshipit-source-id: cd80e9a1be8f5ca017c979d7907974cf72ca4777 --- .../AndroidTextInputNativeComponent.js | 10 ++ Libraries/Components/TextInput/TextInput.d.ts | 8 ++ .../Components/TextInput/TextInput.flow.js | 8 ++ Libraries/Components/TextInput/TextInput.js | 16 ++++ .../TextInput/__tests__/TextInput-test.js | 4 + .../__snapshots__/TextInput-test.js.snap | 2 + .../react/uimanager/BaseViewManager.java | 5 + .../react/views/text/ReactTextUpdate.java | 48 +++++++++- .../com/facebook/react/views/textinput/BUCK | 1 + .../react/views/textinput/ReactEditText.java | 52 +++++++++++ .../textinput/ReactTextInputManager.java | 20 +++- .../textinput/ReactTextInputShadowNode.java | 12 ++- .../main/res/views/uimanager/values/ids.xml | 5 +- .../AndroidTextInputProps.cpp | 5 + .../androidtextinput/AndroidTextInputProps.h | 1 + .../Accessibility/AccessibilityExample.js | 93 ++++++++++++++++++- 16 files changed, 280 insertions(+), 10 deletions(-) diff --git a/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js index e7c28912a0779e..d22042ac61b123 100644 --- a/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -166,6 +166,14 @@ export type NativeProps = $ReadOnly<{| 'off', >, + /** + * String to be read by screenreaders to indicate an error state. The acceptable parameters + * of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates + * the error message. Setting accessibilityInvalid to false removes the error message. + */ + accessibilityErrorMessage?: ?Stringish, + accessibilityInvalid?: ?boolean, + /** * Sets the return key to the label. Use it instead of `returnKeyType`. * @platform android @@ -730,6 +738,8 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { inlineImageLeft: true, editable: true, fontVariant: true, + accessibilityErrorMessage: true, + accessibilityInvalid: true, borderBottomRightRadius: true, borderBottomColor: { process: require('../../StyleSheet/processColor').default, diff --git a/Libraries/Components/TextInput/TextInput.d.ts b/Libraries/Components/TextInput/TextInput.d.ts index 637807aafd3864..4020cee13398f7 100644 --- a/Libraries/Components/TextInput/TextInput.d.ts +++ b/Libraries/Components/TextInput/TextInput.d.ts @@ -531,6 +531,14 @@ export interface TextInputProps TextInputIOSProps, TextInputAndroidProps, AccessibilityProps { + /** + * String to be read by screenreaders to indicate an error state. The acceptable parameters + * of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates + * the error message. Setting accessibilityInvalid to false removes the error message. + */ + accessibilityErrorMessage?: string | undefined; + accessibilityInvalid?: boolean | undefined; + /** * Specifies whether fonts should scale to respect Text Size accessibility settings. * The default is `true`. diff --git a/Libraries/Components/TextInput/TextInput.flow.js b/Libraries/Components/TextInput/TextInput.flow.js index 57259190f1a449..fd1d3acee722c1 100644 --- a/Libraries/Components/TextInput/TextInput.flow.js +++ b/Libraries/Components/TextInput/TextInput.flow.js @@ -523,6 +523,14 @@ export type Props = $ReadOnly<{| ...IOSProps, ...AndroidProps, + /** + * String to be read by screenreaders to indicate an error state. The acceptable parameters + * of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates + * the error message. Setting accessibilityInvalid to false removes the error message. + */ + accessibilityErrorMessage?: ?Stringish, + accessibilityInvalid?: ?boolean, + /** * Can tell `TextInput` to automatically capitalize certain characters. * diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index ec337de164ef8a..9e7cad98a9248f 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -561,6 +561,14 @@ export type Props = $ReadOnly<{| ...IOSProps, ...AndroidProps, + /** + * String to be read by screenreaders to indicate an error state. The acceptable parameters + * of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates + * the error message. Setting accessibilityInvalid to false removes the error message. + */ + accessibilityErrorMessage?: ?Stringish, + accessibilityInvalid?: ?boolean, + /** * Can tell `TextInput` to automatically capitalize certain characters. * @@ -1365,6 +1373,12 @@ function InternalTextInput(props: Props): React.Node { } const accessible = props.accessible !== false; + + const accessibilityErrorMessage = + props.accessibilityInvalid === true + ? props.accessibilityErrorMessage + : null; + const focusable = props.focusable !== false; const config = React.useMemo( @@ -1439,6 +1453,7 @@ function InternalTextInput(props: Props): React.Node { ref={ref} {...otherProps} {...eventHandlers} + accessibilityErrorMessage={accessibilityErrorMessage} accessibilityState={_accessibilityState} accessible={accessible} submitBehavior={submitBehavior} @@ -1490,6 +1505,7 @@ function InternalTextInput(props: Props): React.Node { ref={ref} {...otherProps} {...eventHandlers} + accessibilityErrorMessage={accessibilityErrorMessage} accessibilityState={_accessibilityState} accessibilityLabelledBy={_accessibilityLabelledBy} accessible={accessible} diff --git a/Libraries/Components/TextInput/__tests__/TextInput-test.js b/Libraries/Components/TextInput/__tests__/TextInput-test.js index 4f1adf765e31be..7a6e6c5f54ffaa 100644 --- a/Libraries/Components/TextInput/__tests__/TextInput-test.js +++ b/Libraries/Components/TextInput/__tests__/TextInput-test.js @@ -186,6 +186,7 @@ describe('TextInput', () => { expect(instance.toJSON()).toMatchInlineSnapshot(` { expect(instance.toJSON()).toMatchInlineSnapshot(` { expect(instance.toJSON()).toMatchInlineSnapshot(` { expect(instance.toJSON()).toMatchInlineSnapshot(` and @@ -30,6 +31,7 @@ public class ReactTextUpdate { private final int mSelectionStart; private final int mSelectionEnd; private final int mJustificationMode; + private @Nullable String mAccessibilityErrorMessage; public boolean mContainsMultipleFragments; @@ -59,7 +61,8 @@ public ReactTextUpdate( Layout.BREAK_STRATEGY_HIGH_QUALITY, Layout.JUSTIFICATION_MODE_NONE, -1, - -1); + -1, + null); } public ReactTextUpdate( @@ -85,7 +88,8 @@ public ReactTextUpdate( textBreakStrategy, justificationMode, -1, - -1); + -1, + null); } public ReactTextUpdate( @@ -107,7 +111,8 @@ public ReactTextUpdate( textBreakStrategy, justificationMode, -1, - -1); + -1, + null); } public ReactTextUpdate( @@ -137,21 +142,56 @@ public ReactTextUpdate( mJustificationMode = justificationMode; } + public ReactTextUpdate( + Spannable text, + int jsEventCounter, + boolean containsImages, + float paddingStart, + float paddingTop, + float paddingEnd, + float paddingBottom, + int textAlign, + int textBreakStrategy, + int justificationMode, + int selectionStart, + int selectionEnd, + @Nullable String accessibilityErrorMessage) { + mText = text; + mJsEventCounter = jsEventCounter; + mContainsImages = containsImages; + mPaddingLeft = paddingStart; + mPaddingTop = paddingTop; + mPaddingRight = paddingEnd; + mPaddingBottom = paddingBottom; + mTextAlign = textAlign; + mTextBreakStrategy = textBreakStrategy; + mSelectionStart = selectionStart; + mSelectionEnd = selectionEnd; + mJustificationMode = justificationMode; + mAccessibilityErrorMessage = accessibilityErrorMessage; + } + public static ReactTextUpdate buildReactTextUpdateFromState( Spannable text, int jsEventCounter, int textAlign, int textBreakStrategy, int justificationMode, - boolean containsMultipleFragments) { + boolean containsMultipleFragments, + @Nullable String accessibilityErrorMessage) { ReactTextUpdate reactTextUpdate = new ReactTextUpdate( text, jsEventCounter, false, textAlign, textBreakStrategy, justificationMode); reactTextUpdate.mContainsMultipleFragments = containsMultipleFragments; + reactTextUpdate.mAccessibilityErrorMessage = accessibilityErrorMessage; return reactTextUpdate; } + public @Nullable String getScreenreaderError() { + return mAccessibilityErrorMessage; + } + public Spannable getText() { return mText; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK index 233f1e12611afa..67538981f1baad 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK @@ -34,6 +34,7 @@ rn_android_library( react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"), react_native_target("java/com/facebook/react/views/view:view"), react_native_target("java/com/facebook/react/config:config"), + react_native_target("res:uimanager"), ] + KOTLIN_STDLIB_DEPS, exported_deps = [ react_native_dep("third-party/android/androidx:appcompat"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 6e2e24cc29eaa1..8c1b306810e3ad 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -30,6 +30,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -37,8 +38,10 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; +import com.facebook.react.R; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.common.build.ReactBuildConfig; @@ -157,6 +160,36 @@ public ReactEditText(Context context) { ReactAccessibilityDelegate editTextAccessibilityDelegate = new ReactAccessibilityDelegate( this, this.isFocusable(), this.getImportantForAccessibility()) { + @Override + public void onInitializeAccessibilityNodeInfo( + View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final String accessibilityErrorMessage = + (String) host.getTag(R.id.accessibility_error_message); + boolean contentInvalid = accessibilityErrorMessage == null ? false : true; + if (accessibilityErrorMessage != info.getError()) { + info.setError(accessibilityErrorMessage); + info.setContentInvalid(contentInvalid); + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED + && host.getParent() != null) { + try { + host.getParent().requestSendAccessibilityEvent(host, event); + } catch (AbstractMethodError e) { + FLog.w( + TAG, + host.getParent().getClass().getSimpleName() + + " does not fully implement ViewParent", + e); + } + } + } + @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (action == AccessibilityNodeInfo.ACTION_CLICK) { @@ -539,6 +572,25 @@ public int incrementAndGetEventCounter() { return ++mNativeEventCount; } + /** + * Attempt to set an accessibility error or fail silently. EventCounter is the same one used as + * with text. + * + * @param eventCounter + * @param accessibilityErrorMessage + */ + public void maybeSetAccessibilityError( + int eventCounter, @Nullable String accessibilityErrorMessage) { + String previousScreenreaderError = (String) getTag(R.id.accessibility_error_message); + if (!canUpdateWithEventCount(eventCounter) + || previousScreenreaderError == accessibilityErrorMessage) { + return; + } + + setTag(R.id.accessibility_error_message, accessibilityErrorMessage); + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + public void maybeSetTextFromJS(ReactTextUpdate reactTextUpdate) { mIsSettingTextFromJS = true; maybeSetText(reactTextUpdate); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 7059ca3ed48846..e67955e27fd6f8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -384,6 +384,7 @@ public void updateExtraData(ReactEditText view, Object extraData) { view.maybeSetTextFromState(update); view.maybeSetSelection(update.getJsEventCounter(), selectionStart, selectionEnd); + view.maybeSetAccessibilityError(update.getJsEventCounter(), update.getScreenreaderError()); } } @@ -1338,6 +1339,12 @@ public Object updateState( int currentJustificationMode = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0 : view.getJustificationMode(); + @Nullable + String accessibilityErrorMessage = + props.hasKey("accessibilityErrorMessage") + ? props.getString("accessibilityErrorMessage") + : null; + return ReactTextUpdate.buildReactTextUpdateFromState( spanned, state.getInt("mostRecentEventCount"), @@ -1345,7 +1352,8 @@ public Object updateState( props, TextLayoutManager.isRTL(attributedString), view.getGravityHorizontal()), textBreakStrategy, TextAttributeProps.getJustificationMode(props, currentJustificationMode), - containsMultipleFragments); + containsMultipleFragments, + accessibilityErrorMessage); } public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, MapBuffer state) { @@ -1365,7 +1373,6 @@ public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, M Spannable spanned = TextLayoutManagerMapBuffer.getOrCreateSpannableForText( view.getContext(), attributedString, mReactTextViewManagerCallback); - boolean containsMultipleFragments = attributedString.getMapBuffer(TextLayoutManagerMapBuffer.AS_KEY_FRAGMENTS).getCount() > 1; @@ -1375,6 +1382,12 @@ public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, M int currentJustificationMode = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0 : view.getJustificationMode(); + @Nullable + String accessibilityErrorMessage = + props.hasKey("accessibilityErrorMessage") + ? props.getString("accessibilityErrorMessage") + : null; + return ReactTextUpdate.buildReactTextUpdateFromState( spanned, state.getInt(TX_STATE_KEY_MOST_RECENT_EVENT_COUNT), @@ -1382,6 +1395,7 @@ public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, M props, TextLayoutManagerMapBuffer.isRTL(attributedString), view.getGravityHorizontal()), textBreakStrategy, TextAttributeProps.getJustificationMode(props, currentJustificationMode), - containsMultipleFragments); + containsMultipleFragments, + accessibilityErrorMessage); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java index d53a7f9083aa60..b9a8d926918f30 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -46,11 +46,15 @@ public class ReactTextInputShadowNode extends ReactBaseTextShadowNode @VisibleForTesting public static final String PROP_PLACEHOLDER = "placeholder"; @VisibleForTesting public static final String PROP_SELECTION = "selection"; + @VisibleForTesting + public static final String PROP_ACCESSIBILITY_ERROR_MESSAGE = "accessibilityErrorMessage"; + // Represents the {@code text} property only, not possible nested content. private @Nullable String mText = null; private @Nullable String mPlaceholder = null; private int mSelectionStart = UNSET; private int mSelectionEnd = UNSET; + private @Nullable String mAccessibilityErrorMessage = null; public ReactTextInputShadowNode( @Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) { @@ -194,6 +198,11 @@ public void setPlaceholder(@Nullable String placeholder) { return mPlaceholder; } + @ReactProp(name = PROP_ACCESSIBILITY_ERROR_MESSAGE) + public void setScreenreaderError(String accessibilityErrorMessage) { + mAccessibilityErrorMessage = accessibilityErrorMessage; + } + @ReactProp(name = PROP_SELECTION) public void setSelection(@Nullable ReadableMap selection) { mSelectionStart = mSelectionEnd = UNSET; @@ -247,7 +256,8 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { mTextBreakStrategy, mJustificationMode, mSelectionStart, - mSelectionEnd); + mSelectionEnd, + mAccessibilityErrorMessage); uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); } } diff --git a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 6324b85af44673..0d7621cb058211 100644 --- a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -39,7 +39,10 @@ - + + + + diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index 8c0900480c4c54..7e6de53abce9fc 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -68,6 +68,10 @@ AndroidTextInputProps::AndroidTextInputProps( "underlineColorAndroid", sourceProps.underlineColorAndroid, {})), + accessibilityErrorMessage(convertRawProp(context, rawProps, + "accessibilityErrorMessage", + sourceProps.accessibilityErrorMessage, + {})), inlineImageLeft(CoreFeatures::enablePropIteratorSetter? sourceProps.inlineImageLeft : convertRawProp(context, rawProps, "inlineImageLeft", sourceProps.inlineImageLeft, @@ -431,6 +435,7 @@ folly::dynamic AndroidTextInputProps::getDynamic() const { props["disableFullscreenUI"] = disableFullscreenUI; props["textBreakStrategy"] = textBreakStrategy; props["underlineColorAndroid"] = toAndroidRepr(underlineColorAndroid); + props["accessibilityErrorMessage"] = accessibilityErrorMessage; props["inlineImageLeft"] = inlineImageLeft; props["inlineImagePadding"] = inlineImagePadding; props["importantForAutofill"] = importantForAutofill; diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h index 7652984ce15e86..6c627949aee18d 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h @@ -120,6 +120,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { bool disableFullscreenUI{false}; std::string textBreakStrategy{}; SharedColor underlineColorAndroid{}; + std::string accessibilityErrorMessage{}; std::string inlineImageLeft{}; int inlineImagePadding{0}; std::string importantForAutofill{}; diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index 268550b80624eb..da0840dadfaa76 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -1544,9 +1544,88 @@ function DisplayOptionStatusExample({ ); } +function AccessibilityErrorWithButtons(): React.Node { + const [text, setText] = React.useState(''); + const [error, setError] = React.useState(null); + const [accessibilityInvalid, setAccessibilityInvalid] = React.useState(false); + return ( + + + { + setText(newText); + if (newText === 'Error') { + setError('the newText is: ' + newText); + setAccessibilityInvalid(true); + } else { + setError(null); + setAccessibilityInvalid(false); + } + }} + value={text} + style={styles.default} + /> +