Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1/2 TextInput accessibilityErrorMessage (Talkback, Android) #33468

Closed
wants to merge 160 commits into from
Closed
Show file tree
Hide file tree
Changes from 125 commits
Commits
Show all changes
160 commits
Select commit Hold shift + click to select a range
ad9dc3e
draft - testing functionality
fabOnReact Mar 4, 2022
431a9d5
draft
fabOnReact Mar 6, 2022
a4ef7a7
Merge branch 'main' into text-input-errors
fabOnReact Mar 12, 2022
60b6c9b
draft implementation of android_errorMessage
fabOnReact Mar 12, 2022
fb30702
applying same solution from PR 28952
fabOnReact Mar 12, 2022
f22e1f3
adding ErrorExample to TextInputSharedExamples
fabOnReact Mar 12, 2022
caab17c
Merge branch 'main' into text-input-errors
fabOnReact Mar 16, 2022
06c4908
Android NDK: Module react_codegen_rncore depends on undefined modules…
fabOnReact Mar 16, 2022
d1b6182
Revert "Android NDK: Module react_codegen_rncore depends on undefined…
fabOnReact Mar 23, 2022
3092ac8
testing solution from https://github.com/facebook/react-native/pull/2…
fabOnReact Mar 18, 2022
e521859
restore original example
fabOnReact Mar 18, 2022
f92f843
update example
fabOnReact Mar 18, 2022
d94f343
call setError in ReactTextInputManager
fabOnReact Mar 18, 2022
6bd7240
Error onBlur example
fabOnReact Mar 18, 2022
202292a
draft examples
fabOnReact Mar 18, 2022
2820a95
handle errors from diffent type of callbacks
fabOnReact Mar 18, 2022
b8846c9
adding onEndEditing callback
fabOnReact Mar 18, 2022
bf75148
revert changes to ReactEditText onChangeText
fabOnReact Mar 22, 2022
5e9b4c3
add errorMessage to ReactTextUpdate and maybeSetErrorMessage
fabOnReact Mar 23, 2022
97c2dff
update method alphabetical order
fabOnReact Mar 23, 2022
31bfa40
rename android_errorMessage to errorMessageAndroid
fabOnReact Mar 23, 2022
394f794
remove ViewDefault for ERROR
fabOnReact Mar 23, 2022
3a8347a
remove call to setError onAttachedToWindow
fabOnReact Mar 23, 2022
f39e5f1
Merge branch 'main' into text-input-errors
fabOnReact Mar 23, 2022
60bc4e3
adding @platform android to errorMessageAndroid comments
fabOnReact Mar 23, 2022
3c48da3
moving TextInput example at the bottom of the list
fabOnReact Mar 23, 2022
aca0d99
adding errorMessageAndroid to ParagraphAttributes (fabric AndroidText…
fabOnReact Mar 30, 2022
37f0004
adding /ReactAndroid/hermes-engine/.cxx to .gitignore
fabOnReact Mar 31, 2022
0708d07
Merge branch 'main' into text-input-errors
fabOnReact Mar 31, 2022
9df9484
using std::optional<std::string>
fabOnReact Apr 1, 2022
17852df
add comments in ParagraphAttributes
fabOnReact Apr 1, 2022
add6198
draft solution to trigger setError when changing state in Paper
fabOnReact Apr 1, 2022
3d5100c
adding Nullable flag
fabOnReact Apr 1, 2022
53fe1c0
Merge branch 'main' into text-input-errors
fabOnReact Apr 4, 2022
6c0f432
use value_or instead of pointer *paragraphAttributes.errorMessageAndroid
fabOnReact Apr 4, 2022
0ee77ed
adding @VisibleForTesting to PROP_ERROR_MESSAGE
fabOnReact Apr 4, 2022
84edc00
Merge branch 'main' into text-input-errors
fabOnReact Apr 7, 2022
299eba6
Avoid any error visual styling
fabOnReact Apr 8, 2022
27dd208
use androidx Nullable instead of java
fabOnReact Apr 8, 2022
2e660d2
rename errorMessageAndroid in android_accessibilityErrorMessage
fabOnReact Apr 8, 2022
e1c8f54
rename prop to android_accessibilityError
fabOnReact Apr 8, 2022
d4a390b
rename prop in ReactTextUpdate
fabOnReact Apr 8, 2022
f29a21c
rename prop in .xml files
fabOnReact Apr 8, 2022
79c5e4f
adding buck res dependency
fabOnReact Apr 8, 2022
cc602e0
adding comment
fabOnReact Apr 8, 2022
4ba33c3
Paper maybeSetErrorMessage calls errorMessage on every prop updates
fabOnReact Apr 11, 2022
6f69758
renaming ReactTextUpdate getErrorMessage
fabOnReact Apr 11, 2022
4df9d95
rename android_accessibilityError to screenreaderErrorAndroid
fabOnReact Apr 11, 2022
99b4a24
rename android_accessibilityError to screenreaderErrorAndroid java files
fabOnReact Apr 11, 2022
1365f50
rename android_accessibilityError to screenreaderErrorAndroid cpp files
fabOnReact Apr 11, 2022
3a1ef4a
rename android_accessibilityError to screenreaderErrorAndroid .h files
fabOnReact Apr 11, 2022
994089c
Merge branch 'main' into text-input-errors
fabOnReact Apr 11, 2022
eb33c93
fix annoucement delayed to next character
fabOnReact Apr 14, 2022
bb2df1f
trigger call only on TYPE_VIEW_TEXT_CHANGED
fabOnReact Apr 15, 2022
de50984
remove convertion to (View)
fabOnReact Apr 15, 2022
7252c15
adding try catch statement and logging
fabOnReact Apr 15, 2022
586af1f
adding return statement if error does not change
fabOnReact Apr 15, 2022
d1b6f67
remove nested if
fabOnReact Apr 15, 2022
56087d3
only trigger requestSendAccEvent when error present
fabOnReact Apr 15, 2022
3d831a9
remove check on accessibilityErrorMessage != null
fabOnReact Apr 15, 2022
0513007
Merge branch 'main' into text-input-errors
fabOnReact Apr 15, 2022
425056d
Merge branch 'main' into text-input-errors
fabOnReact May 2, 2022
ea9bb4c
Pods update
fabOnReact May 2, 2022
7f7b281
implementing iOS functionalities for https://github.com/facebook/reac…
fabOnReact May 3, 2022
ef89ba2
rename screenreaderErrorAndroid to screenreaderError
fabOnReact May 3, 2022
af8a0bd
Revert "Pods update"
fabOnReact May 3, 2022
29b99ba
update Error example
fabOnReact May 5, 2022
f70870e
Merge branch 'main' into text-input-errors
fabOnReact May 20, 2022
bb4597e
improving iOS solution announce screenreaderError
fabOnReact May 20, 2022
d96a284
fix circleci failures
fabOnReact May 23, 2022
9a58e2e
move screenreaderError to AccessibilityProps
fabOnReact May 23, 2022
9958dcd
removing diffing in viewPropConversions
fabOnReact May 23, 2022
ad56244
rename errorMessage in screenreaderError
fabOnReact May 23, 2022
bb7b0c3
remove screenreaderError from AccessibilityProps
fabOnReact May 23, 2022
195fb0b
using the same default prop for screenreaderError
fabOnReact May 23, 2022
343eea1
update iOS logic to trigger error message
fabOnReact May 24, 2022
521c907
Merge branch 'main' into text-input-errors
fabOnReact May 24, 2022
efba00c
Merge branch 'main' into text-input-errors
fabOnReact May 25, 2022
dac4b96
Rename screenreaderError to accessibilityErrorMessage
fabOnReact Jun 7, 2022
1d00ea8
Merge branch 'main' into text-input-errors
fabOnReact Jun 7, 2022
6bac85e
draft solution Android accessibilityInvalid
fabOnReact Jun 7, 2022
54aca18
draft - basic example with accInvalid
fabOnReact Jun 7, 2022
8e06f08
adding accessibilityInvalid to iOS TextInput props
fabOnReact Jun 7, 2022
ee964b6
moving prop type from AndroidProps to Props
fabOnReact Jun 7, 2022
0e5ad7c
fix flow circleci error
fabOnReact Jun 7, 2022
8670b13
update TextInput-test.js snapshot
fabOnReact Jun 7, 2022
d801b87
rename variable screenreaderError to accessibilityErrorMessage
fabOnReact Jun 7, 2022
53d8464
update Example
fabOnReact Jun 7, 2022
cabb212
rename remaing configs to accessibilityErrorMessage
fabOnReact Jun 7, 2022
318a964
set accessibilityValue to text value if there is no error
fabOnReact Jun 8, 2022
f1a8b00
update Example based on new API updates
fabOnReact Jun 9, 2022
aefa74c
Merge branch 'main' into text-input-errors
fabOnReact Jun 9, 2022
086dce7
adding more AccessibilityError examples
fabOnReact Jun 9, 2022
ddd7f53
handle different scenarios
fabOnReact Jun 9, 2022
9f1e465
minor change
fabOnReact Jun 10, 2022
1574fc4
Merge branch 'main' into text-input-errors
fabOnReact Jun 10, 2022
3b5acf3
iOS - Exception thrown while executing UI block: - RCTUITextView setA…
fabOnReact Jun 13, 2022
f978045
draft solution - announcing accessibilityErrorMessage on iOS Paper
fabOnReact Jun 14, 2022
037089f
include attributedText.string and avoid 2 announcements
fabOnReact Jun 14, 2022
04f5e14
Paper iOS - update accessibilityValue onChangeText
fabOnReact Jun 16, 2022
f594d51
iOS Paper - set error outside of onChangeText
fabOnReact Jun 16, 2022
582b1e8
fix the following scenarios iOS Paper
fabOnReact Jun 16, 2022
e4a95c6
Merge branch 'main' into text-input-errors
fabOnReact Jun 17, 2022
b53ed9d
iOS - apply Paper improvements to Fabric
fabOnReact Jun 17, 2022
8cd7f42
update variable name in RCTUITextField
fabOnReact Jun 17, 2022
dce9b91
test/improve check on accessibilityValue != text
fabOnReact Jun 20, 2022
90aa959
Implement solution for multiline textinput
fabOnReact Jun 20, 2022
3c4296f
avoid initializing lastChar if errorMessageRemoved is false
fabOnReact Jun 20, 2022
3f3312d
Rename var ErrorMessageString -> ScreenReaderError
fabOnReact Jun 20, 2022
9858e54
The attributedText property is nil by default
fabOnReact Jun 20, 2022
16d7d4d
rm update accessibilityValue in RCTBaseTextInputView
fabOnReact Jun 20, 2022
99b39d5
Reintroduce fix - avoid repeat error announcement
fabOnReact Jun 21, 2022
34c5526
update Accessibility Example
fabOnReact Jun 21, 2022
933aa3c
rename _errorMessage to currentScreenreaderError
fabOnReact Jun 21, 2022
30f3f96
implementing strict check in setAttributed string
fabOnReact Jun 21, 2022
de3c2f3
implement previousScreenreaderError check on Fabric
fabOnReact Jun 22, 2022
ce5e52a
using mutableCopy to set previousScreenreaderError value
fabOnReact Jun 22, 2022
b2a6964
rename currentScreenreaderError to currentAccError
fabOnReact Jun 22, 2022
9a3117b
rename currentScreenreaderError to currentAccError in Fabric
fabOnReact Jun 22, 2022
596df4d
fix duplicated VoiceOver announcements
fabOnReact Jun 23, 2022
f5a3bb6
update accessibilityValue onChangeText
fabOnReact Jun 24, 2022
84786a2
update Accessibility Example
fabOnReact Jun 24, 2022
c8790a1
Merge branch 'main' into text-input-errors
fabOnReact Jun 24, 2022
f4acead
Merge branch 'main' into text-input-errors
fabOnReact Jul 28, 2022
63fd956
minor changes
fabOnReact Jul 28, 2022
5e5e2c0
Merge branch 'main' into text-input-errors
fabOnReact Aug 12, 2022
cb0c506
avoid reiterate prop documentation in the docblock
fabOnReact Aug 12, 2022
d85c90a
adding comments as suggested by Brett
fabOnReact Aug 12, 2022
7d0c4db
Merge branch 'main' into text-input-errors
fabOnReact Aug 22, 2022
a92e7f6
adding accessibilityError message to buildReactTextUpdateFromState
fabOnReact Aug 22, 2022
114b4e0
Merge branch 'main' into text-input-errors
fabOnReact Oct 13, 2022
011178c
Merge branch 'main' into text-input-errors
fabOnReact Oct 14, 2022
9c48846
Merge branch 'main' into text-input-errors
fabOnReact Oct 17, 2022
9829e68
minor change
fabOnReact Oct 17, 2022
869dd49
adding flow type for accessibilityErrorMessage
fabOnReact Oct 17, 2022
b43c547
typescript types
fabOnReact Oct 17, 2022
043b24a
eslint Strings must use singlequote. (quotes)
fabOnReact Oct 17, 2022
6c9e233
Merge branch 'main' into text-input-errors
fabOnReact Nov 5, 2022
2818fa4
trigger onChangeText accessibilityErrorMessage with props instead of
fabOnReact Nov 5, 2022
c3ee8f9
this update is triggered with textInputRef.setTextAndSelection(newText,
fabOnReact Nov 5, 2022
5d43830
fabric does not use ReactTextUpdate to announce errorMessage onChange…
fabOnReact Nov 5, 2022
46e3d06
removing accessibilityErrorMessage from ParagraphAttributes state
fabOnReact Nov 5, 2022
af2c906
set accessibilityErrorMessage in case MapBuffer serialization is
fabOnReact Nov 5, 2022
b1b8bf0
adding @Nullable to accessibilityErrorMessage
fabOnReact Nov 5, 2022
f0d0186
change .cpp default value
fabOnReact Nov 5, 2022
b006276
Merge branch 'main' into text-input-errors
fabOnReact Dec 1, 2022
9d1adec
replace readwrite with assign
fabOnReact Dec 1, 2022
e4094c9
remove pressed state
fabOnReact Dec 1, 2022
ca6d811
fix error: auto property synthesis will not synthesize property 'acce…
fabOnReact Dec 5, 2022
bd826f4
Merge branch 'main' into text-input-errors
fabOnReact Dec 5, 2022
1f9c10e
Merge branch 'main' into text-input-errors
fabOnReact Dec 14, 2022
61b049e
adding type null or String
fabOnReact Dec 14, 2022
29a13b6
fix flow errors circleci
fabOnReact Dec 14, 2022
e19d492
Merge branch 'main' into text-input-errors
fabOnReact Jan 19, 2023
085730f
move iOS functionalities to a new PR
fabOnReact Jan 20, 2023
f51645e
updating example
fabOnReact Jan 20, 2023
f138da4
Merge branch 'main' into text-input-errors
fabOnReact Jan 20, 2023
e3f2d68
moving RCTTextInputViewConfig to iOS PR
fabOnReact Jan 23, 2023
954a58f
Merge branch 'main' into text-input-errors
fabOnReact Jan 23, 2023
27b963b
Merge branch 'main' into text-input-errors
fabOnReact Feb 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ 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,

/**
* Setting accessibilityInvalid to true activates the error message. Setting accessibilityInvalid
* to false removes the error message.
*/
accessibilityInvalid?: ?boolean,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the rationale for adding two props when they could instead be one? (Also, probably no need to reiterate in the type of the prop in the docblock, since the type is already defined and will be presumably extracted for any reference documentation purposes.)

Suggested change
/**
* 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,
/**
* Setting accessibilityInvalid to true activates the error message. Setting accessibilityInvalid
* to false removes the error message.
*/
accessibilityInvalid?: ?boolean,
/**
* String to be read by screen readers to indicate an error state. Setting this to null or
* undefined will remove the error message.
*/
accessibilityErrorMessage?: ?Stringish,

Copy link
Contributor Author

@fabOnReact fabOnReact Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the rationale for adding two props when they could instead be one?

@yungsters It was introduced with the code review #33468 (comment)

Adapts this API to the Web API aria-errormessage
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-errormessage

When there is a user-created error, you want to let them know it exists and tell them how to fix it. There are two attributes you need to use: set aria-invalid="true" to define the object as being in an error state, then add the aria-errormessage attribute with the value being the id of the element containing the error message text for that object.

This API (accessibilityErrorMessage and accessibilityInvalid) is built with the Android AccessbilityNodeInfo API. The API allows adding accessibility information to each View node and announcing them with talkback.

Each android view and widget (EditText, TextView, or View) stores its accessibility information in its AccessibilityNodeInfo node.
TalkBack reads this information when the screenreader's focus changes.

The error message of an EditText is set on the AccessibillityNodeInfo using the following APIs:

#33468 (comment)

something to note is that the value of aria-errormessage is meant to be the id of an element that contains the error message.

This API relies on the AccessbilityNodeInfo#setError. It does not add a hidden text element to the View hierarchy using react-native. Instead, we use the Native Android API (AccessibilityNodeInfo) to add the error message to the node of the EditText. TalkBack announces the error message when focusing on the EditText (TextInput).

I'll be happy to remove the accessibililtyInvalid prop and further improve the PR. Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to reiterate in the type of the prop in the docblock, since the type is already defined and will be presumably extracted for any reference documentation purposes.

I removed the documentation in the docblock of prop accessibilityInvalid with commit cb0c506

Copy link
Contributor

@lunaleaps lunaleaps Aug 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is interesting because it doesn't completely follow the web standards since accessibilityErrorMessage doesn't provide the elementId but it also introduces this extra logic around accessibilityInvalid which is part of the web standards.

I think I'd lean towards keeping the two separate props but including in the documentation that we're modelling this after aria standards. See my comment here: facebook/react-native-website#3010 (comment)

Copy link
Contributor Author

@fabOnReact fabOnReact Aug 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot, @lunaleaps. I updated the documentation as requested with commit facebook/react-native-website@8510e8c 🙏 .

preview of the documentation

Screen Shot 2022-08-30 at 21 08 14

/**
* Sets the return key to the label. Use it instead of `returnKeyType`.
* @platform android
Expand Down Expand Up @@ -725,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')},
borderRadius: true,
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Components/TextInput/RCTTextInputViewConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ const RCTTextInputViewConfig = {
allowFontScaling: true,
fontStyle: true,
textTransform: true,
accessibilityErrorMessage: true,
accessibilityInvalid: true,
textAlign: true,
fontFamily: true,
lineHeight: true,
Expand Down
22 changes: 22 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,20 @@ 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,

/**
* Setting accessibilityInvalid to true activates the error message. Setting accessibilityInvalid
* to false removes the error message.
* The default value is `false`.
*/
accessibilityInvalid?: ?boolean,

/**
* Can tell `TextInput` to automatically capitalize certain characters.
*
Expand Down Expand Up @@ -1239,6 +1253,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(
Expand Down Expand Up @@ -1295,6 +1315,7 @@ function InternalTextInput(props: Props): React.Node {
{...props}
{...eventHandlers}
accessible={accessible}
accessibilityErrorMessage={accessibilityErrorMessage}
submitBehavior={submitBehavior}
caretHidden={caretHidden}
dataDetectorTypes={props.dataDetectorTypes}
Expand Down Expand Up @@ -1342,6 +1363,7 @@ function InternalTextInput(props: Props): React.Node {
{...props}
{...eventHandlers}
accessible={accessible}
accessibilityErrorMessage={accessibilityErrorMessage}
autoCapitalize={autoCapitalize}
submitBehavior={submitBehavior}
caretHidden={caretHidden}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

exports[`TextInput tests should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessibilityErrorMessage={null}
accessible={true}
allowFontScaling={true}
focusable={true}
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Text/TextInput/Multiline/RCTUITextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, readwrite, nullable) NSString *currentAccessibilityError;
Copy link
Contributor

@lunaleaps lunaleaps Nov 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabriziobertoglio1987 I'm getting a build error because this differs from Libraries/Text/TextInput/Singleline/RCTUITextField.h

Re

stderr: In file included from xplat/js/react-native-github/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm:15:
xplat/js/react-native-github/Libraries/Text/TextInput/Singleline/RCTUITextField.h:32:54: error: 'retain (or strong)' attribute on property 'accessibilityErrorMessage' does not match the property inherited from 'RCTBackedTextInputViewProtocol' [-Werror,-Wproperty-attribute-mismatch]
[@property]() (nonatomic, readwrite, nullable) NSString *accessibilityErrorMessage;

Which property declaration should we use? cc @blavalla if you have thoughts

Copy link
Contributor Author

@fabOnReact fabOnReact Dec 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lunaleaps The RCTTextInputComponentView.mm is the iOS Fabric Component for TextInput. Creates an instance _backedTextInput of type BackedTextInputViewProtocol using RCTUITextView or RCTUITextField constructors.

_backedTextInputView = props.traits.multiline ? [RCTUITextView new] : [RCTUITextField new];

BackedTextInputViewProtocol, RCTUITextView and RCTUITextField headers should match (currently they don't, accessibilityErrorMessage type differs in RCTBackedTextInputViewProtocol).
Sorry for the mistake. I'm rebasing, building, testing and pushing the commit soon. Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9d1adec

fix_wrong_type_error.mp4

@property (nonatomic, readwrite, nullable) NSString *previousAccessibilityError;

@property (nonatomic, assign) CGFloat preferredMaxLayoutWidth;

Expand Down
16 changes: 16 additions & 0 deletions Libraries/Text/TextInput/Multiline/RCTUITextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ - (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defa
[self _updatePlaceholder];
}

- (void)setAccessibilityErrorMessage:(NSString *)accessibilityErrorMessage
{
self.previousAccessibilityError = [self.currentAccessibilityError mutableCopy];
self.currentAccessibilityError = accessibilityErrorMessage;
NSString *text = self.attributedText == nil ? @"" : self.attributedText.string;
NSString *lastChar = [text length] == 0 ? @"" : [text substringFromIndex:[text length] - 1];
if (accessibilityErrorMessage != nil) {
NSString *errorWithLastCharacter = [NSString stringWithFormat: @"%@ %@", lastChar, accessibilityErrorMessage];
NSString *errorWithText = [NSString stringWithFormat: @"%@ %@", text, accessibilityErrorMessage];
self.accessibilityValue = errorWithText;
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, errorWithLastCharacter);
} else {
self.accessibilityValue = nil;
}
}

- (NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
return _defaultTextAttributes;
Expand Down
3 changes: 3 additions & 0 deletions Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
@protocol RCTBackedTextInputViewProtocol <UITextInput>

@property (nonatomic, copy, nullable) NSAttributedString *attributedText;
@property (nonatomic, assign, nullable) NSString *accessibilityErrorMessage;
@property (nonatomic, readwrite, nullable) NSString *currentAccessibilityError;
@property (nonatomic, readwrite, nullable) NSString *previousAccessibilityError;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
Expand Down
9 changes: 9 additions & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ - (void)setAttributedText:(NSAttributedString *)attributedText

textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO);

NSString *currentAccessibilityError = self.backedTextInputView.currentAccessibilityError;
NSString *previousAccessibilityError = self.backedTextInputView.previousAccessibilityError;
BOOL accessibilityErrorMessageWasRemoved = currentAccessibilityError == nil && ![currentAccessibilityError isEqualToString: previousAccessibilityError];
if (accessibilityErrorMessageWasRemoved) {
BOOL validString = attributedText && [attributedText.string length] != 0;
NSString *lastChar = validString ? [attributedText.string substringFromIndex:[attributedText.string length] - 1] : @"";
self.backedTextInputView.accessibilityValue = nil;
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, lastChar);
}
if (eventLag == 0 && textNeedsUpdate) {
UITextRange *selection = self.backedTextInputView.selectedTextRange;
NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length;
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ @implementation RCTBaseTextInputViewManager
RCT_REMAP_VIEW_PROPERTY(autoCorrect, backedTextInputView.autocorrectionType, UITextAutocorrectionType)
RCT_REMAP_VIEW_PROPERTY(contextMenuHidden, backedTextInputView.contextMenuHidden, BOOL)
RCT_REMAP_VIEW_PROPERTY(editable, backedTextInputView.editable, BOOL)
RCT_REMAP_VIEW_PROPERTY(accessibilityErrorMessage, backedTextInputView.accessibilityErrorMessage, NSString)
RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, backedTextInputView.enablesReturnKeyAutomatically, BOOL)
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, backedTextInputView.keyboardAppearance, UIKeyboardAppearance)
RCT_REMAP_VIEW_PROPERTY(placeholder, backedTextInputView.placeholder, NSString)
Expand Down
3 changes: 3 additions & 0 deletions Libraries/Text/TextInput/Singleline/RCTUITextField.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign) UIEdgeInsets textContainerInset;
@property (nonatomic, assign, getter=isEditable) BOOL editable;
@property (nonatomic, readwrite, nullable) NSString *currentAccessibilityError;
@property (nonatomic, readwrite, nullable) NSString *previousAccessibilityError;
@property (nonatomic, readwrite, nullable) NSString *accessibilityErrorMessage;
@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID;
@property (nonatomic, assign, readonly) CGFloat zoomScale;
Expand Down
16 changes: 16 additions & 0 deletions Libraries/Text/TextInput/Singleline/RCTUITextField.m
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ - (void)setEditable:(BOOL)editable
self.enabled = editable;
}

- (void)setAccessibilityErrorMessage:(NSString *)accessibilityErrorMessage
{
self.previousAccessibilityError = [self.currentAccessibilityError mutableCopy];
self.currentAccessibilityError = accessibilityErrorMessage;
NSString *text = self.attributedText == nil ? @"" : self.attributedText.string;
NSString *lastChar = [text length] == 0 ? @"" : [text substringFromIndex:[text length] - 1];
if (accessibilityErrorMessage != nil) {
NSString *errorWithLastCharacter = [NSString stringWithFormat: @"%@ %@", lastChar, accessibilityErrorMessage];
NSString *errorWithText = [NSString stringWithFormat: @"%@ %@", text, accessibilityErrorMessage];
self.accessibilityValue = errorWithText;
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, errorWithLastCharacter);
} else {
self.accessibilityValue = nil;
}
}

- (void)setSecureTextEntry:(BOOL)secureTextEntry
{
if (self.secureTextEntry == secureTextEntry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ @implementation RCTTextInputComponentView {
UIView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
NSUInteger _mostRecentEventCount;
NSAttributedString *_lastStringStateWasUpdatedWith;
NSString *currentAccessibilityError;
NSString *previousAccessibilityError;

/*
* UIKit uses either UITextField or UITextView as its UIKit element for <TextInput>. UITextField is for single line
Expand All @@ -56,6 +58,12 @@ @implementation RCTTextInputComponentView {
*/
BOOL _comingFromJS;
BOOL _didMoveToWindow;

/*
* A flag that triggers the accessibilityElement.accessibilityValue update and VoiceOver announcement
* to avoid duplicated announcements of accessibilityErrorMessage more info https://bit.ly/3yfUXD8
*/
BOOL _errorMessageRemoved;
}

#pragma mark - UIView overrides
Expand All @@ -72,6 +80,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_ignoreNextTextInputCall = NO;
_comingFromJS = NO;
_didMoveToWindow = NO;
_errorMessageRemoved = NO;
[self addSubview:_backedTextInputView];
}

Expand Down Expand Up @@ -134,6 +143,22 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
_backedTextInputView.editable = newTextInputProps.traits.editable;
}

if (newTextInputProps.accessibilityErrorMessage != oldTextInputProps.accessibilityErrorMessage || newTextInputProps.text != oldTextInputProps.text) {
NSString *text = RCTNSStringFromString(newTextInputProps.text);
NSString *error = RCTNSStringFromString(newTextInputProps.accessibilityErrorMessage);
NSString *lastChar = [text length] == 0 ? @"" : [text substringFromIndex:[text length] - 1];
if ([error length] != 0) {
NSString *errorWithLastCharacter = [NSString stringWithFormat: @"%@ %@", lastChar, error];
NSString *errorWithText = [NSString stringWithFormat: @"%@ %@", text, error];
self.accessibilityElement.accessibilityValue = errorWithText;
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, errorWithLastCharacter);
self->_errorMessageRemoved = NO;
} else {
self.accessibilityElement.accessibilityValue = text;
fabOnReact marked this conversation as resolved.
Show resolved Hide resolved
self->_errorMessageRemoved = YES;
}
}

if (newTextInputProps.traits.enablesReturnKeyAutomatically !=
oldTextInputProps.traits.enablesReturnKeyAutomatically) {
_backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically;
Expand Down Expand Up @@ -590,6 +615,13 @@ - (void)_setAttributedString:(NSAttributedString *)attributedString
UITextRange *selectedRange = _backedTextInputView.selectedTextRange;
NSInteger oldTextLength = _backedTextInputView.attributedText.string.length;
_backedTextInputView.attributedText = attributedString;
if (self->_errorMessageRemoved) {
_backedTextInputView.accessibilityValue = attributedString.string;
self.accessibilityElement.accessibilityValue = attributedString.string;
NSString *lastChar = [attributedString.string substringFromIndex:[attributedString.string length] - 1];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, lastChar);
self->_errorMessageRemoved = NO;
}
if (selectedRange.empty) {
// Maintaining a cursor position relative to the end of the old text.
NSInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
Expand Down
1 change: 1 addition & 0 deletions React/Views/UIView+React.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
@property (nonatomic, copy) NSArray<NSDictionary *> *accessibilityActions;
@property (nonatomic, copy) NSDictionary *accessibilityValueInternal;
@property (nonatomic, copy) NSString *accessibilityLanguage;
@property (nonatomic, copy) NSString *accessibilityErrorMessage;

/**
* Used in debugging to get a description of the view hierarchy rooted at
Expand Down
11 changes: 11 additions & 0 deletions React/Views/UIView+React.m
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,17 @@ - (NSString *)accessibilityLanguage
return objc_getAssociatedObject(self, _cmd);
}

- (NSString *)accessibilityErrorMessage
{
return objc_getAssociatedObject(self, _cmd);
}

- (void)setAccessibilityErrorMessage:(NSString *)accessibilityErrorMessage
{
objc_setAssociatedObject(
self, @selector(accessibilityErrorMessage), accessibilityErrorMessage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)setAccessibilityLanguage:(NSString *)accessibilityLanguage
{
objc_setAssociatedObject(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ public void setAccessibilityRole(@NonNull T view, @Nullable String accessibility
view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole));
}

@ReactProp(name = "accessibilityErrorMessage")
public void setScreenreaderError(@NonNull T view, @Nullable String accessibilityErrorMessage) {
view.setTag(R.id.accessibility_error_message, accessibilityErrorMessage);
}

@Override
@ReactProp(name = ViewProps.ACCESSIBILITY_COLLECTION)
public void setAccessibilityCollection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import android.text.Layout;
import android.text.Spannable;
import androidx.annotation.Nullable;

/**
* Class that contains the data needed for a text update. Used by both <Text/> and <TextInput/>
Expand All @@ -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;

Expand Down Expand Up @@ -59,7 +61,8 @@ public ReactTextUpdate(
Layout.BREAK_STRATEGY_HIGH_QUALITY,
Layout.JUSTIFICATION_MODE_NONE,
-1,
-1);
-1,
null);
}

public ReactTextUpdate(
Expand All @@ -85,7 +88,8 @@ public ReactTextUpdate(
textBreakStrategy,
justificationMode,
-1,
-1);
-1,
null);
}

public ReactTextUpdate(
Expand All @@ -107,7 +111,8 @@ public ReactTextUpdate(
textBreakStrategy,
justificationMode,
-1,
-1);
-1,
null);
}

public ReactTextUpdate(
Expand All @@ -122,7 +127,8 @@ public ReactTextUpdate(
int textBreakStrategy,
int justificationMode,
int selectionStart,
int selectionEnd) {
int selectionEnd,
@Nullable String accessibilityErrorMessage) {
mText = text;
mJsEventCounter = jsEventCounter;
mContainsImages = containsImages;
Expand All @@ -135,6 +141,7 @@ public ReactTextUpdate(
mSelectionStart = selectionStart;
mSelectionEnd = selectionEnd;
mJustificationMode = justificationMode;
mAccessibilityErrorMessage = accessibilityErrorMessage;
}

public static ReactTextUpdate buildReactTextUpdateFromState(
Expand All @@ -143,15 +150,21 @@ public static ReactTextUpdate buildReactTextUpdateFromState(
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;
}
Expand Down
Loading