Skip to content

Commit

Permalink
adding iOS functionalities from #33468
Browse files Browse the repository at this point in the history
for original commit history before applying squash see https://github.com/facebook/react-native/pull/33468/commits
  • Loading branch information
fabOnReact committed Jan 20, 2023
1 parent 8ea1cba commit 2d0d4d4
Show file tree
Hide file tree
Showing 19 changed files with 234 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Libraries/Components/TextInput/RCTTextInputViewConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ const RCTTextInputViewConfig = {
allowFontScaling: true,
fontStyle: true,
textTransform: true,
accessibilityErrorMessage: true,
accessibilityInvalid: true,
textAlign: true,
fontFamily: true,
lineHeight: true,
Expand Down
8 changes: 8 additions & 0 deletions Libraries/Components/TextInput/TextInput.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,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`.
Expand Down
8 changes: 8 additions & 0 deletions Libraries/Components/TextInput/TextInput.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
16 changes: 16 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1439,6 +1453,7 @@ function InternalTextInput(props: Props): React.Node {
ref={ref}
{...otherProps}
{...eventHandlers}
accessibilityErrorMessage={accessibilityErrorMessage}
accessibilityState={_accessibilityState}
accessible={accessible}
submitBehavior={submitBehavior}
Expand Down Expand Up @@ -1490,6 +1505,7 @@ function InternalTextInput(props: Props): React.Node {
ref={ref}
{...otherProps}
{...eventHandlers}
accessibilityErrorMessage={accessibilityErrorMessage}
accessibilityState={_accessibilityState}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessible={accessible}
Expand Down
4 changes: 4 additions & 0 deletions Libraries/Components/TextInput/__tests__/TextInput-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ describe('TextInput', () => {

expect(instance.toJSON()).toMatchInlineSnapshot(`
<RCTSinglelineTextInputView
accessibilityErrorMessage={null}
accessible={true}
allowFontScaling={true}
focusable={true}
Expand Down Expand Up @@ -231,6 +232,7 @@ describe('TextInput compat with web', () => {

expect(instance.toJSON()).toMatchInlineSnapshot(`
<RCTSinglelineTextInputView
accessibilityErrorMessage={null}
accessible={true}
allowFontScaling={true}
focusable={true}
Expand Down Expand Up @@ -315,6 +317,7 @@ describe('TextInput compat with web', () => {

expect(instance.toJSON()).toMatchInlineSnapshot(`
<RCTSinglelineTextInputView
accessibilityErrorMessage={null}
accessibilityState={
Object {
"busy": true,
Expand Down Expand Up @@ -407,6 +410,7 @@ describe('TextInput compat with web', () => {

expect(instance.toJSON()).toMatchInlineSnapshot(`
<RCTSinglelineTextInputView
accessibilityErrorMessage={null}
accessible={true}
allowFontScaling={true}
focusable={true}
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
3 changes: 3 additions & 0 deletions Libraries/Text/TextInput/Multiline/RCTUITextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ NS_ASSUME_NONNULL_BEGIN

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

@property (nonatomic, assign, nullable) NSString *accessibilityErrorMessage;
@property (nonatomic, readwrite, nullable) NSString *currentAccessibilityError;
@property (nonatomic, readwrite, nullable) NSString *previousAccessibilityError;
@property (nonatomic, assign) BOOL contextMenuHidden;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@property (nonatomic, copy, nullable) NSString *placeholder;
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
11 changes: 11 additions & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ - (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;
// Triggering the announcement manually fixes screenreader announcement getting cut off
// https://bit.ly/3w18QmV https://bit.ly/3AdVKW3 https://bit.ly/3QHm7c7 https://bit.ly/3BVnmAy
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 @@ -36,6 +36,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 @@ -21,6 +21,9 @@ NS_ASSUME_NONNULL_BEGIN

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

@property (nonatomic, assign, nullable) NSString *accessibilityErrorMessage;
@property (nonatomic, readwrite, nullable) NSString *currentAccessibilityError;
@property (nonatomic, readwrite, nullable) NSString *previousAccessibilityError;
@property (nonatomic, assign) BOOL caretHidden;
@property (nonatomic, assign) BOOL contextMenuHidden;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 @@ -55,6 +57,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 @@ -71,6 +79,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_ignoreNextTextInputCall = NO;
_comingFromJS = NO;
_didMoveToWindow = NO;
_errorMessageRemoved = NO;
[self addSubview:_backedTextInputView];
}

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

NSString *newAccessibilityErrorMessage = RCTNSStringFromString(newTextInputProps.accessibilityErrorMessage);
if (newTextInputProps.text != oldTextInputProps.text && [newAccessibilityErrorMessage length] == 0) {
NSString *text = RCTNSStringFromString(newTextInputProps.text);
_backedTextInputView.accessibilityValue = text;
self.accessibilityElement.accessibilityValue = text;
}

if (newTextInputProps.accessibilityErrorMessage != oldTextInputProps.accessibilityErrorMessage) {
NSString *text = RCTNSStringFromString(newTextInputProps.text);
NSString *error = RCTNSStringFromString(newTextInputProps.accessibilityErrorMessage);
if ([error length] != 0) {
self.triggerAccessibilityAnnouncement = YES;
NSString *errorWithText = [NSString stringWithFormat: @"%@ %@", text, error];
self.accessibilityElement.accessibilityValue = errorWithText;
} else {
self.accessibilityElement.accessibilityValue = text;
self.triggerAccessibilityAnnouncement = NO;
}
}

if (newTextInputProps.traits.enablesReturnKeyAutomatically !=
oldTextInputProps.traits.enablesReturnKeyAutomatically) {
_backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically;
Expand Down Expand Up @@ -236,6 +265,15 @@ - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &
}
}

- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
if (self.triggerAccessibilityAnnouncement) {
[self announceForAccessibilityWithOptions:self.accessibilityElement.accessibilityValue];
self.triggerAccessibilityAnnouncement = NO;
}
}

- (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics
oldLayoutMetrics:(LayoutMetrics const &)oldLayoutMetrics
{
Expand Down Expand Up @@ -594,6 +632,16 @@ - (void)_setAttributedString:(NSAttributedString *)attributedString
UITextRange *selectedRange = _backedTextInputView.selectedTextRange;
NSInteger oldTextLength = _backedTextInputView.attributedText.string.length;
_backedTextInputView.attributedText = attributedString;

// check that current error is not empty
if (self.triggerAccessibilityAnnouncement) {
[self announceForAccessibilityWithOptions:self.accessibilityElement.accessibilityValue];
self.triggerAccessibilityAnnouncement = NO;
} else {
NSString *lastChar = [attributedString.string substringFromIndex:[attributedString.string length] - 1];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, lastChar);
self.triggerAccessibilityAnnouncement = NO;
}
if (selectedRange.empty) {
// Maintaining a cursor position relative to the end of the old text.
NSInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ NS_ASSUME_NONNULL_BEGIN
* Defaults to `self`.
*/
@property (nonatomic, strong, nullable, readonly) NSObject *accessibilityElement;

@property (nonatomic, readwrite) BOOL triggerAccessibilityAnnouncement;
/**
* Insets used when hit testing inside this view.
*/
Expand All @@ -85,6 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
* This is a fragment of temporary workaround that we need only temporary and will get rid of soon.
*/
- (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN;
- (void)announceForAccessibilityWithOptions:(NSString *)announcement;

@end

Expand Down
11 changes: 11 additions & 0 deletions React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,17 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
[self invalidateLayer];
}

- (void)announceForAccessibilityWithOptions:(NSString*)announcement
{
if (@available(iOS 11.0, *)) {
BOOL accessibilityAnnouncementNotEmpty = [announcement length] != 0;
if (accessibilityAnnouncementNotEmpty) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
self.triggerAccessibilityAnnouncement = NO;
}
}
}

- (void)prepareForRecycle
{
[super prepareForRecycle];
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 @@ -87,6 +87,12 @@ TextInputProps::TextInputProps(
"selection",
sourceProps.selection,
std::optional<Selection>())),
accessibilityErrorMessage(convertRawProp(
context,
rawProps,
"accessibilityErrorMessage",
sourceProps.accessibilityErrorMessage,
{})),
inputAccessoryViewID(convertRawProp(
context,
rawProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class TextInputProps final : public ViewProps, public BaseTextProps {

std::string const inputAccessoryViewID{};

std::string accessibilityErrorMessage{""};

bool onKeyPressSync{false};
bool onChangeSync{false};

Expand Down
Loading

0 comments on commit 2d0d4d4

Please sign in to comment.