Skip to content

Commit cbe934b

Browse files
fabOnReactfacebook-github-bot
authored andcommitted
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 #35908 Documentation PR facebook/react-native-website#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 facebook/react-native-deprecated-modules#18 ## Changelog [Android] [Added] - Adding TextInput prop accessibilityErrorMessage to announce with TalkBack screenreaders Pull Request resolved: #33468 Test Plan: **Android - 20 Jan 2023** #33468 (comment) **iOS - 20 Jan 2023** #33468 (comment) <details><summary>CLICK TO OPEN OLD VIDEO TEST CASES</summary> <p> **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]) </p> </details> [1]: fabOnReact/react-native-notes#12 (comment) "Test Cases of the functionality (Android - Fabric)" [2]: fabOnReact/react-native-notes#12 (comment) "Test Cases of the functionality (Android - Paper)" [3]: fabOnReact/react-native-notes#12 (comment) "Test Cases of the functionality (iOS - Fabric)" [6]: fabOnReact/react-native-notes#12 (comment) "Runtime Error in main branch when passing value of 1 to TextInput placeholder prop" [7]: fabOnReact/react-native-notes#12 (comment) "TalkBack error announcement done on next typed character with onChangeText" [8]: fabOnReact/react-native-notes#12 (comment) "setting the TextInput errorMessage state with setTextAndSelection Java API from JavaScript" [9]: fabOnReact/react-native-notes#12 (comment) "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]: 60b6c9b "draft implementation of android_errorMessage " [21]: 012d92d "add errorMessage to ReactTextUpdate and maybeSetAccessibilityError" [22]: fabOnReact@cad239b "rename android_errorMessage to errorMessageAndroid" [23]: fabOnReact@0bae474 "TextInput: keep C++ state in-sync with updated AttributedStrings in Java" [24]: fabOnReact@0556e86 "TextInput: support modifying TextInputs with multiple Fragments (Cxx side)" [25]: fabOnReact@7ab5eb4 "AndroidTextInput: support using commands instead of setNativeProps (native change)" [26]: fabOnReact@b9491b7 "TextInput: support editing completely empty TextInputs" [27]: fabOnReact@7f1ed68 "Fabric: convertRawProp was extended to accept an optional default value" [28]: 6e0fa5f "Support optional types for C++ TurboModules" [29]: fabOnReact@baa66f6 "Announce accessibility state changes happening in the background" [30]: fabOnReact/react-native-notes#12 (comment) "Change of error state from onChangeText show/hides a TextInput error" [31]: fabOnReact/react-native-notes#12 (comment) "understanding onChangeText callback" [32]: #29063 (comment) "Explanation on how TextInput calls SET_TEXT_AND_SELECTION in Java API" [33]: #33468 (comment) "Explanation of TextInput state management with fabric C++ and JAVA API" [34]: #33468 (comment) "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]: fabOnReact/react-native-notes#12 (comment) "Analysis on how AndroidTextInputState.cpp sends updates to ReactTextInputManager" [36]: #33468 (comment) "discussion on using announceForAccessibility in ReactEditText" [37]: fabOnReact@29eb632 "Refactor MountingManager into MountingManager + SurfaceMountingManager" [38]: fabOnReact@733f228 "Diff C++ props for Android consumption" [39]: fabOnReact@91b3f5d "This diff implement and integrates Mapbuffer into Fabric text measure system" [40]: #29070 "[Android] Fix letters duplication when using autoCapitalize #29070" [50]: fabOnReact/react-native-notes#12 "Notes from work on iOS/Android: Text input error for screenreaders #12" [51]: #30848 "iOS/Android: Text input error for screenreaders #30848" [52]: #30859 "Android: Error state change (text inputs) #30859" [61]: eb33c93 "fix annoucement delayed to next character" [70]: fabOnReact/react-native-notes#12 (comment) "iOS - Paper renderer does not update the accessibilityValue" [71]: fabOnReact/react-native-notes#12 (comment) "Test Cases of the functionality (Fabric) after removing changes to .cpp libs" [72]: fabOnReact/react-native-notes#12 (comment) "Test Cases of the functionality (Paper) after removing changes to .cpp libs" [73]: fabOnReact/react-native-notes#12 (comment) "iOS - announcing error onChangeText and screenreader focus" [74]: fabOnReact/react-native-notes#12 (comment) "iOS - The screenreader announces the TextInput value after the errorMessage is cleared" [75]: fabOnReact/react-native-notes#12 (comment) "iOS - Exception thrown while executing UI block: - [RCTTextView setOnAccessibiltyAction:]: unrecognized selector sent to instance (Paper) (main branch)" [76]: #30859 (comment) "iOS - announce lastChar (not entire text) onChangeText and avoid multiple announcements (Fabric)" [77]: #30859 (comment) "iOS - announces or does not announce the accessibilityError through Button onPress (not onChangeText) (Fabric)" [78]: #30859 (comment) "iOS - the error is announced with accessibilityInvalid true and does not clear after typing text (onChangeText) (Fabric)" [79]: #30848 (comment) "iOS - Exception thrown while executing UI block: - RCTUITextView setAccessibilityErrorMessage:]: unrecognized selector sent to instance (iOS - Paper on main branch)" [80]: fabOnReact@e13b9c6 "RCTTextField was spliited into two classes" [81]: fabOnReact@ee9697e "Introducing RCTBackedTextInputDelegate" [82]: fabOnReact@2dd2529 "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]: #30859 (comment) "iOS - accessibilityValue announces correctly with/out errorMessage set with onChangeText or with outside event" [88]: #30859 (comment) "Android - accessibilityValue announces correctly with/out errorMessage set with onChangeText or with outside event" [89]: #30859 (comment) "iOS - accessibilityValue announces correctly with/out errorMessage set with onChangeText or with outside event (Fabric)" [100]: fabOnReact@110b191 "Refactor ViewPropsMapBuffer -> general MapBuffer props mechanism" [101]: fabOnReact@22b6e1c "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
1 parent ed39d63 commit cbe934b

File tree

16 files changed

+280
-10
lines changed

16 files changed

+280
-10
lines changed

Libraries/Components/TextInput/AndroidTextInputNativeComponent.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@ export type NativeProps = $ReadOnly<{|
166166
'off',
167167
>,
168168

169+
/**
170+
* String to be read by screenreaders to indicate an error state. The acceptable parameters
171+
* of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates
172+
* the error message. Setting accessibilityInvalid to false removes the error message.
173+
*/
174+
accessibilityErrorMessage?: ?Stringish,
175+
accessibilityInvalid?: ?boolean,
176+
169177
/**
170178
* Sets the return key to the label. Use it instead of `returnKeyType`.
171179
* @platform android
@@ -730,6 +738,8 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
730738
inlineImageLeft: true,
731739
editable: true,
732740
fontVariant: true,
741+
accessibilityErrorMessage: true,
742+
accessibilityInvalid: true,
733743
borderBottomRightRadius: true,
734744
borderBottomColor: {
735745
process: require('../../StyleSheet/processColor').default,

Libraries/Components/TextInput/TextInput.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,14 @@ export interface TextInputProps
531531
TextInputIOSProps,
532532
TextInputAndroidProps,
533533
AccessibilityProps {
534+
/**
535+
* String to be read by screenreaders to indicate an error state. The acceptable parameters
536+
* of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates
537+
* the error message. Setting accessibilityInvalid to false removes the error message.
538+
*/
539+
accessibilityErrorMessage?: string | undefined;
540+
accessibilityInvalid?: boolean | undefined;
541+
534542
/**
535543
* Specifies whether fonts should scale to respect Text Size accessibility settings.
536544
* The default is `true`.

Libraries/Components/TextInput/TextInput.flow.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,14 @@ export type Props = $ReadOnly<{|
523523
...IOSProps,
524524
...AndroidProps,
525525

526+
/**
527+
* String to be read by screenreaders to indicate an error state. The acceptable parameters
528+
* of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates
529+
* the error message. Setting accessibilityInvalid to false removes the error message.
530+
*/
531+
accessibilityErrorMessage?: ?Stringish,
532+
accessibilityInvalid?: ?boolean,
533+
526534
/**
527535
* Can tell `TextInput` to automatically capitalize certain characters.
528536
*

Libraries/Components/TextInput/TextInput.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,14 @@ export type Props = $ReadOnly<{|
561561
...IOSProps,
562562
...AndroidProps,
563563

564+
/**
565+
* String to be read by screenreaders to indicate an error state. The acceptable parameters
566+
* of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates
567+
* the error message. Setting accessibilityInvalid to false removes the error message.
568+
*/
569+
accessibilityErrorMessage?: ?Stringish,
570+
accessibilityInvalid?: ?boolean,
571+
564572
/**
565573
* Can tell `TextInput` to automatically capitalize certain characters.
566574
*
@@ -1365,6 +1373,12 @@ function InternalTextInput(props: Props): React.Node {
13651373
}
13661374

13671375
const accessible = props.accessible !== false;
1376+
1377+
const accessibilityErrorMessage =
1378+
props.accessibilityInvalid === true
1379+
? props.accessibilityErrorMessage
1380+
: null;
1381+
13681382
const focusable = props.focusable !== false;
13691383

13701384
const config = React.useMemo(
@@ -1439,6 +1453,7 @@ function InternalTextInput(props: Props): React.Node {
14391453
ref={ref}
14401454
{...otherProps}
14411455
{...eventHandlers}
1456+
accessibilityErrorMessage={accessibilityErrorMessage}
14421457
accessibilityState={_accessibilityState}
14431458
accessible={accessible}
14441459
submitBehavior={submitBehavior}
@@ -1490,6 +1505,7 @@ function InternalTextInput(props: Props): React.Node {
14901505
ref={ref}
14911506
{...otherProps}
14921507
{...eventHandlers}
1508+
accessibilityErrorMessage={accessibilityErrorMessage}
14931509
accessibilityState={_accessibilityState}
14941510
accessibilityLabelledBy={_accessibilityLabelledBy}
14951511
accessible={accessible}

Libraries/Components/TextInput/__tests__/TextInput-test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ describe('TextInput', () => {
186186

187187
expect(instance.toJSON()).toMatchInlineSnapshot(`
188188
<RCTSinglelineTextInputView
189+
accessibilityErrorMessage={null}
189190
accessible={true}
190191
allowFontScaling={true}
191192
focusable={true}
@@ -231,6 +232,7 @@ describe('TextInput compat with web', () => {
231232

232233
expect(instance.toJSON()).toMatchInlineSnapshot(`
233234
<RCTSinglelineTextInputView
235+
accessibilityErrorMessage={null}
234236
accessible={true}
235237
allowFontScaling={true}
236238
focusable={true}
@@ -315,6 +317,7 @@ describe('TextInput compat with web', () => {
315317

316318
expect(instance.toJSON()).toMatchInlineSnapshot(`
317319
<RCTSinglelineTextInputView
320+
accessibilityErrorMessage={null}
318321
accessibilityState={
319322
Object {
320323
"busy": true,
@@ -407,6 +410,7 @@ describe('TextInput compat with web', () => {
407410

408411
expect(instance.toJSON()).toMatchInlineSnapshot(`
409412
<RCTSinglelineTextInputView
413+
accessibilityErrorMessage={null}
410414
accessible={true}
411415
allowFontScaling={true}
412416
focusable={true}

Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
exports[`TextInput tests should render as expected: should deep render when mocked (please verify output manually) 1`] = `
44
<RCTSinglelineTextInputView
5+
accessibilityErrorMessage={null}
56
accessible={true}
67
allowFontScaling={true}
78
focusable={true}
@@ -31,6 +32,7 @@ exports[`TextInput tests should render as expected: should deep render when mock
3132

3233
exports[`TextInput tests should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
3334
<RCTSinglelineTextInputView
35+
accessibilityErrorMessage={null}
3436
accessible={true}
3537
allowFontScaling={true}
3638
focusable={true}

ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ public void setAccessibilityRole(@NonNull T view, @Nullable String accessibility
251251
view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole));
252252
}
253253

254+
@ReactProp(name = "accessibilityErrorMessage")
255+
public void setScreenreaderError(@NonNull T view, @Nullable String accessibilityErrorMessage) {
256+
view.setTag(R.id.accessibility_error_message, accessibilityErrorMessage);
257+
}
258+
254259
@Override
255260
@ReactProp(name = ViewProps.ACCESSIBILITY_COLLECTION)
256261
public void setAccessibilityCollection(

ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import android.text.Layout;
1313
import android.text.Spannable;
14+
import androidx.annotation.Nullable;
1415

1516
/**
1617
* Class that contains the data needed for a text update. Used by both <Text/> and <TextInput/>
@@ -30,6 +31,7 @@ public class ReactTextUpdate {
3031
private final int mSelectionStart;
3132
private final int mSelectionEnd;
3233
private final int mJustificationMode;
34+
private @Nullable String mAccessibilityErrorMessage;
3335

3436
public boolean mContainsMultipleFragments;
3537

@@ -59,7 +61,8 @@ public ReactTextUpdate(
5961
Layout.BREAK_STRATEGY_HIGH_QUALITY,
6062
Layout.JUSTIFICATION_MODE_NONE,
6163
-1,
62-
-1);
64+
-1,
65+
null);
6366
}
6467

6568
public ReactTextUpdate(
@@ -85,7 +88,8 @@ public ReactTextUpdate(
8588
textBreakStrategy,
8689
justificationMode,
8790
-1,
88-
-1);
91+
-1,
92+
null);
8993
}
9094

9195
public ReactTextUpdate(
@@ -107,7 +111,8 @@ public ReactTextUpdate(
107111
textBreakStrategy,
108112
justificationMode,
109113
-1,
110-
-1);
114+
-1,
115+
null);
111116
}
112117

113118
public ReactTextUpdate(
@@ -137,21 +142,56 @@ public ReactTextUpdate(
137142
mJustificationMode = justificationMode;
138143
}
139144

145+
public ReactTextUpdate(
146+
Spannable text,
147+
int jsEventCounter,
148+
boolean containsImages,
149+
float paddingStart,
150+
float paddingTop,
151+
float paddingEnd,
152+
float paddingBottom,
153+
int textAlign,
154+
int textBreakStrategy,
155+
int justificationMode,
156+
int selectionStart,
157+
int selectionEnd,
158+
@Nullable String accessibilityErrorMessage) {
159+
mText = text;
160+
mJsEventCounter = jsEventCounter;
161+
mContainsImages = containsImages;
162+
mPaddingLeft = paddingStart;
163+
mPaddingTop = paddingTop;
164+
mPaddingRight = paddingEnd;
165+
mPaddingBottom = paddingBottom;
166+
mTextAlign = textAlign;
167+
mTextBreakStrategy = textBreakStrategy;
168+
mSelectionStart = selectionStart;
169+
mSelectionEnd = selectionEnd;
170+
mJustificationMode = justificationMode;
171+
mAccessibilityErrorMessage = accessibilityErrorMessage;
172+
}
173+
140174
public static ReactTextUpdate buildReactTextUpdateFromState(
141175
Spannable text,
142176
int jsEventCounter,
143177
int textAlign,
144178
int textBreakStrategy,
145179
int justificationMode,
146-
boolean containsMultipleFragments) {
180+
boolean containsMultipleFragments,
181+
@Nullable String accessibilityErrorMessage) {
147182

148183
ReactTextUpdate reactTextUpdate =
149184
new ReactTextUpdate(
150185
text, jsEventCounter, false, textAlign, textBreakStrategy, justificationMode);
151186
reactTextUpdate.mContainsMultipleFragments = containsMultipleFragments;
187+
reactTextUpdate.mAccessibilityErrorMessage = accessibilityErrorMessage;
152188
return reactTextUpdate;
153189
}
154190

191+
public @Nullable String getScreenreaderError() {
192+
return mAccessibilityErrorMessage;
193+
}
194+
155195
public Spannable getText() {
156196
return mText;
157197
}

ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ rn_android_library(
3434
react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"),
3535
react_native_target("java/com/facebook/react/views/view:view"),
3636
react_native_target("java/com/facebook/react/config:config"),
37+
react_native_target("res:uimanager"),
3738
] + KOTLIN_STDLIB_DEPS,
3839
exported_deps = [
3940
react_native_dep("third-party/android/androidx:appcompat"),

ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@
3030
import android.view.KeyEvent;
3131
import android.view.MotionEvent;
3232
import android.view.View;
33+
import android.view.accessibility.AccessibilityEvent;
3334
import android.view.accessibility.AccessibilityNodeInfo;
3435
import android.view.inputmethod.EditorInfo;
3536
import android.view.inputmethod.InputConnection;
3637
import android.view.inputmethod.InputMethodManager;
3738
import androidx.annotation.Nullable;
3839
import androidx.appcompat.widget.AppCompatEditText;
3940
import androidx.core.view.ViewCompat;
41+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
4042
import com.facebook.common.logging.FLog;
4143
import com.facebook.infer.annotation.Assertions;
44+
import com.facebook.react.R;
4245
import com.facebook.react.bridge.ReactContext;
4346
import com.facebook.react.bridge.ReactSoftExceptionLogger;
4447
import com.facebook.react.common.build.ReactBuildConfig;
@@ -157,6 +160,36 @@ public ReactEditText(Context context) {
157160
ReactAccessibilityDelegate editTextAccessibilityDelegate =
158161
new ReactAccessibilityDelegate(
159162
this, this.isFocusable(), this.getImportantForAccessibility()) {
163+
@Override
164+
public void onInitializeAccessibilityNodeInfo(
165+
View host, AccessibilityNodeInfoCompat info) {
166+
super.onInitializeAccessibilityNodeInfo(host, info);
167+
final String accessibilityErrorMessage =
168+
(String) host.getTag(R.id.accessibility_error_message);
169+
boolean contentInvalid = accessibilityErrorMessage == null ? false : true;
170+
if (accessibilityErrorMessage != info.getError()) {
171+
info.setError(accessibilityErrorMessage);
172+
info.setContentInvalid(contentInvalid);
173+
}
174+
}
175+
176+
@Override
177+
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
178+
super.onInitializeAccessibilityEvent(host, event);
179+
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
180+
&& host.getParent() != null) {
181+
try {
182+
host.getParent().requestSendAccessibilityEvent(host, event);
183+
} catch (AbstractMethodError e) {
184+
FLog.w(
185+
TAG,
186+
host.getParent().getClass().getSimpleName()
187+
+ " does not fully implement ViewParent",
188+
e);
189+
}
190+
}
191+
}
192+
160193
@Override
161194
public boolean performAccessibilityAction(View host, int action, Bundle args) {
162195
if (action == AccessibilityNodeInfo.ACTION_CLICK) {
@@ -539,6 +572,25 @@ public int incrementAndGetEventCounter() {
539572
return ++mNativeEventCount;
540573
}
541574

575+
/**
576+
* Attempt to set an accessibility error or fail silently. EventCounter is the same one used as
577+
* with text.
578+
*
579+
* @param eventCounter
580+
* @param accessibilityErrorMessage
581+
*/
582+
public void maybeSetAccessibilityError(
583+
int eventCounter, @Nullable String accessibilityErrorMessage) {
584+
String previousScreenreaderError = (String) getTag(R.id.accessibility_error_message);
585+
if (!canUpdateWithEventCount(eventCounter)
586+
|| previousScreenreaderError == accessibilityErrorMessage) {
587+
return;
588+
}
589+
590+
setTag(R.id.accessibility_error_message, accessibilityErrorMessage);
591+
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
592+
}
593+
542594
public void maybeSetTextFromJS(ReactTextUpdate reactTextUpdate) {
543595
mIsSettingTextFromJS = true;
544596
maybeSetText(reactTextUpdate);

0 commit comments

Comments
 (0)