Skip to content

Commit 13aea79

Browse files
committed
adding iOS functionalities from facebook#33468
for original commit history before applying squash see https://github.com/facebook/react-native/pull/33468/commits
1 parent 8ea1cba commit 13aea79

File tree

19 files changed

+234
-2
lines changed

19 files changed

+234
-2
lines changed

Libraries/Components/TextInput/RCTTextInputViewConfig.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ const RCTTextInputViewConfig = {
107107
allowFontScaling: true,
108108
fontStyle: true,
109109
textTransform: true,
110+
accessibilityErrorMessage: true,
111+
accessibilityInvalid: true,
110112
textAlign: true,
111113
fontFamily: true,
112114
lineHeight: true,

Libraries/Components/TextInput/TextInput.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,14 @@ export interface TextInputProps
521521
TextInputIOSProps,
522522
TextInputAndroidProps,
523523
AccessibilityProps {
524+
/**
525+
* String to be read by screenreaders to indicate an error state. The acceptable parameters
526+
* of accessibilityErrorMessage is a string. Setting accessibilityInvalid to true activates
527+
* the error message. Setting accessibilityInvalid to false removes the error message.
528+
*/
529+
accessibilityErrorMessage?: string | undefined;
530+
accessibilityInvalid?: boolean | undefined;
531+
524532
/**
525533
* Specifies whether fonts should scale to respect Text Size accessibility settings.
526534
* 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}

Libraries/Text/TextInput/Multiline/RCTUITextView.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ NS_ASSUME_NONNULL_BEGIN
2222

2323
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
2424

25+
@property (nonatomic, assign, nullable) NSString *accessibilityErrorMessage;
26+
@property (nonatomic, readwrite, nullable) NSString *currentAccessibilityError;
27+
@property (nonatomic, readwrite, nullable) NSString *previousAccessibilityError;
2528
@property (nonatomic, assign) BOOL contextMenuHidden;
2629
@property (nonatomic, assign, readonly) BOOL textWasPasted;
2730
@property (nonatomic, copy, nullable) NSString *placeholder;

Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
1515
@protocol RCTBackedTextInputViewProtocol <UITextInput>
1616

1717
@property (nonatomic, copy, nullable) NSAttributedString *attributedText;
18+
@property (nonatomic, assign, nullable) NSString *accessibilityErrorMessage;
19+
@property (nonatomic, readwrite, nullable) NSString *currentAccessibilityError;
20+
@property (nonatomic, readwrite, nullable) NSString *previousAccessibilityError;
1821
@property (nonatomic, copy, nullable) NSString *placeholder;
1922
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
2023
@property (nonatomic, assign, readonly) BOOL textWasPasted;

Libraries/Text/TextInput/RCTBaseTextInputView.m

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
148148

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

151+
NSString *currentAccessibilityError = self.backedTextInputView.currentAccessibilityError;
152+
NSString *previousAccessibilityError = self.backedTextInputView.previousAccessibilityError;
153+
BOOL accessibilityErrorMessageWasRemoved = currentAccessibilityError == nil && ![currentAccessibilityError isEqualToString: previousAccessibilityError];
154+
if (accessibilityErrorMessageWasRemoved) {
155+
BOOL validString = attributedText && [attributedText.string length] != 0;
156+
NSString *lastChar = validString ? [attributedText.string substringFromIndex:[attributedText.string length] - 1] : @"";
157+
self.backedTextInputView.accessibilityValue = nil;
158+
// Triggering the announcement manually fixes screenreader announcement getting cut off
159+
// https://bit.ly/3w18QmV https://bit.ly/3AdVKW3 https://bit.ly/3QHm7c7 https://bit.ly/3BVnmAy
160+
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, lastChar);
161+
}
151162
if (eventLag == 0 && textNeedsUpdate) {
152163
UITextRange *selection = self.backedTextInputView.selectedTextRange;
153164
NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length;

Libraries/Text/TextInput/RCTBaseTextInputViewManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ @implementation RCTBaseTextInputViewManager {
3636
RCT_REMAP_VIEW_PROPERTY(autoCorrect, backedTextInputView.autocorrectionType, UITextAutocorrectionType)
3737
RCT_REMAP_VIEW_PROPERTY(contextMenuHidden, backedTextInputView.contextMenuHidden, BOOL)
3838
RCT_REMAP_VIEW_PROPERTY(editable, backedTextInputView.editable, BOOL)
39+
RCT_REMAP_VIEW_PROPERTY(accessibilityErrorMessage, backedTextInputView.accessibilityErrorMessage, NSString)
3940
RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, backedTextInputView.enablesReturnKeyAutomatically, BOOL)
4041
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, backedTextInputView.keyboardAppearance, UIKeyboardAppearance)
4142
RCT_REMAP_VIEW_PROPERTY(placeholder, backedTextInputView.placeholder, NSString)

Libraries/Text/TextInput/Singleline/RCTUITextField.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ NS_ASSUME_NONNULL_BEGIN
2121

2222
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
2323

24+
@property (nonatomic, assign, nullable) NSString *accessibilityErrorMessage;
25+
@property (nonatomic, readwrite, nullable) NSString *currentAccessibilityError;
26+
@property (nonatomic, readwrite, nullable) NSString *previousAccessibilityError;
2427
@property (nonatomic, assign) BOOL caretHidden;
2528
@property (nonatomic, assign) BOOL contextMenuHidden;
2629
@property (nonatomic, assign, readonly) BOOL textWasPasted;

React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ @implementation RCTTextInputComponentView {
3232
UIView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
3333
NSUInteger _mostRecentEventCount;
3434
NSAttributedString *_lastStringStateWasUpdatedWith;
35+
NSString *currentAccessibilityError;
36+
NSString *previousAccessibilityError;
3537

3638
/*
3739
* UIKit uses either UITextField or UITextView as its UIKit element for <TextInput>. UITextField is for single line
@@ -55,6 +57,12 @@ @implementation RCTTextInputComponentView {
5557
*/
5658
BOOL _comingFromJS;
5759
BOOL _didMoveToWindow;
60+
61+
/*
62+
* A flag that triggers the accessibilityElement.accessibilityValue update and VoiceOver announcement
63+
* to avoid duplicated announcements of accessibilityErrorMessage more info https://bit.ly/3yfUXD8
64+
*/
65+
BOOL _errorMessageRemoved;
5866
}
5967

6068
#pragma mark - UIView overrides
@@ -71,6 +79,7 @@ - (instancetype)initWithFrame:(CGRect)frame
7179
_ignoreNextTextInputCall = NO;
7280
_comingFromJS = NO;
7381
_didMoveToWindow = NO;
82+
_errorMessageRemoved = NO;
7483
[self addSubview:_backedTextInputView];
7584
}
7685

@@ -133,6 +142,26 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
133142
_backedTextInputView.editable = newTextInputProps.traits.editable;
134143
}
135144

145+
NSString *newAccessibilityErrorMessage = RCTNSStringFromString(newTextInputProps.accessibilityErrorMessage);
146+
if (newTextInputProps.text != oldTextInputProps.text && [newAccessibilityErrorMessage length] == 0) {
147+
NSString *text = RCTNSStringFromString(newTextInputProps.text);
148+
_backedTextInputView.accessibilityValue = text;
149+
self.accessibilityElement.accessibilityValue = text;
150+
}
151+
152+
if (newTextInputProps.accessibilityErrorMessage != oldTextInputProps.accessibilityErrorMessage) {
153+
NSString *text = RCTNSStringFromString(newTextInputProps.text);
154+
NSString *error = RCTNSStringFromString(newTextInputProps.accessibilityErrorMessage);
155+
if ([error length] != 0) {
156+
self.triggerAccessibilityAnnouncement = YES;
157+
NSString *errorWithText = [NSString stringWithFormat: @"%@ %@", text, error];
158+
self.accessibilityElement.accessibilityValue = errorWithText;
159+
} else {
160+
self.accessibilityElement.accessibilityValue = text;
161+
self.triggerAccessibilityAnnouncement = NO;
162+
}
163+
}
164+
136165
if (newTextInputProps.traits.enablesReturnKeyAutomatically !=
137166
oldTextInputProps.traits.enablesReturnKeyAutomatically) {
138167
_backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically;
@@ -236,6 +265,15 @@ - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &
236265
}
237266
}
238267

268+
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
269+
{
270+
[super finalizeUpdates:updateMask];
271+
if (self.triggerAccessibilityAnnouncement) {
272+
[self announceForAccessibilityWithOptions:self.accessibilityElement.accessibilityValue];
273+
self.triggerAccessibilityAnnouncement = NO;
274+
}
275+
}
276+
239277
- (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics
240278
oldLayoutMetrics:(LayoutMetrics const &)oldLayoutMetrics
241279
{
@@ -594,6 +632,16 @@ - (void)_setAttributedString:(NSAttributedString *)attributedString
594632
UITextRange *selectedRange = _backedTextInputView.selectedTextRange;
595633
NSInteger oldTextLength = _backedTextInputView.attributedText.string.length;
596634
_backedTextInputView.attributedText = attributedString;
635+
636+
// check that current error is not empty
637+
if (self.triggerAccessibilityAnnouncement) {
638+
[self announceForAccessibilityWithOptions:self.accessibilityElement.accessibilityValue];
639+
self.triggerAccessibilityAnnouncement = NO;
640+
} else {
641+
NSString *lastChar = [attributedString.string substringFromIndex:[attributedString.string length] - 1];
642+
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, lastChar);
643+
self.triggerAccessibilityAnnouncement = NO;
644+
}
597645
if (selectedRange.empty) {
598646
// Maintaining a cursor position relative to the end of the old text.
599647
NSInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument

React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ NS_ASSUME_NONNULL_BEGIN
6363
* Defaults to `self`.
6464
*/
6565
@property (nonatomic, strong, nullable, readonly) NSObject *accessibilityElement;
66-
66+
@property (nonatomic, readwrite) BOOL triggerAccessibilityAnnouncement;
6767
/**
6868
* Insets used when hit testing inside this view.
6969
*/
@@ -85,6 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
8585
* This is a fragment of temporary workaround that we need only temporary and will get rid of soon.
8686
*/
8787
- (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN;
88+
- (void)announceForAccessibilityWithOptions:(NSString *)announcement;
8889

8990
@end
9091

React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,17 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
412412
[self invalidateLayer];
413413
}
414414

415+
- (void)announceForAccessibilityWithOptions:(NSString*)announcement
416+
{
417+
if (@available(iOS 11.0, *)) {
418+
BOOL accessibilityAnnouncementNotEmpty = [announcement length] != 0;
419+
if (accessibilityAnnouncementNotEmpty) {
420+
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
421+
self.triggerAccessibilityAnnouncement = NO;
422+
}
423+
}
424+
}
425+
415426
- (void)prepareForRecycle
416427
{
417428
[super prepareForRecycle];

React/Views/UIView+React.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
@property (nonatomic, copy) NSArray<NSDictionary *> *accessibilityActions;
122122
@property (nonatomic, copy) NSDictionary *accessibilityValueInternal;
123123
@property (nonatomic, copy) NSString *accessibilityLanguage;
124+
@property (nonatomic, copy) NSString *accessibilityErrorMessage;
124125

125126
/**
126127
* Used in debugging to get a description of the view hierarchy rooted at

React/Views/UIView+React.m

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,17 @@ - (NSString *)accessibilityLanguage
320320
return objc_getAssociatedObject(self, _cmd);
321321
}
322322

323+
- (NSString *)accessibilityErrorMessage
324+
{
325+
return objc_getAssociatedObject(self, _cmd);
326+
}
327+
328+
- (void)setAccessibilityErrorMessage:(NSString *)accessibilityErrorMessage
329+
{
330+
objc_setAssociatedObject(
331+
self, @selector(accessibilityErrorMessage), accessibilityErrorMessage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
332+
}
333+
323334
- (void)setAccessibilityLanguage:(NSString *)accessibilityLanguage
324335
{
325336
objc_setAssociatedObject(

ReactCommon/react/renderer/components/textinput/iostextinput/TextInputProps.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ TextInputProps::TextInputProps(
8787
"selection",
8888
sourceProps.selection,
8989
std::optional<Selection>())),
90+
accessibilityErrorMessage(convertRawProp(
91+
context,
92+
rawProps,
93+
"accessibilityErrorMessage",
94+
sourceProps.accessibilityErrorMessage,
95+
{})),
9096
inputAccessoryViewID(convertRawProp(
9197
context,
9298
rawProps,

ReactCommon/react/renderer/components/textinput/iostextinput/TextInputProps.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class TextInputProps final : public ViewProps, public BaseTextProps {
6868

6969
std::string const inputAccessoryViewID{};
7070

71+
std::string accessibilityErrorMessage{""};
72+
7173
bool onKeyPressSync{false};
7274
bool onChangeSync{false};
7375

0 commit comments

Comments
 (0)