mirror of
https://github.com/status-im/react-native.git
synced 2025-01-25 00:39:03 +00:00
072d2709df
Summary: Sometimes, when we implement some custom RN view, we have to proxy all accessible atributes directly to some subview which actually has accesible content. So, in other words, this allows bypass some axillary views in terms of accessibility. Concreate example which this approach supposed to fix: https://github.com/facebook/react-native/pull/14200/files#diff-e5f6b1386b7ba07fd887bca11ec828a4R208 Reviewed By: mmmulani Differential Revision: D5143860 fbshipit-source-id: 6d7ce747f28e5a31d32c925b8ad8fd4b98ce1de1
385 lines
12 KiB
Objective-C
385 lines
12 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "RCTTextField.h"
|
|
|
|
#import <React/RCTBridge.h>
|
|
#import <React/RCTConvert.h>
|
|
#import <React/RCTEventDispatcher.h>
|
|
#import <React/RCTUIManager.h>
|
|
#import <React/RCTUtils.h>
|
|
#import <React/UIView+React.h>
|
|
|
|
#import "RCTTextSelection.h"
|
|
#import "RCTUITextField.h"
|
|
|
|
@interface RCTTextField () <UITextFieldDelegate>
|
|
|
|
@end
|
|
|
|
@implementation RCTTextField
|
|
{
|
|
RCTBridge *_bridge;
|
|
RCTEventDispatcher *_eventDispatcher;
|
|
NSInteger _nativeEventCount;
|
|
BOOL _submitted;
|
|
UITextRange *_previousSelectionRange;
|
|
NSString *_finalText;
|
|
CGSize _previousContentSize;
|
|
}
|
|
|
|
- (instancetype)initWithBridge:(RCTBridge *)bridge
|
|
{
|
|
if (self = [super initWithFrame:CGRectZero]) {
|
|
RCTAssertParam(bridge);
|
|
|
|
_bridge = bridge;
|
|
_eventDispatcher = bridge.eventDispatcher;
|
|
|
|
_textField = [[RCTUITextField alloc] initWithFrame:self.bounds];
|
|
_textField.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
|
|
// Note: `UITextField` fires same events to channels in this order: delegate method, notification center, target action.
|
|
// Usually (presumably) all events with equivalent semantic fires consistently in specified order...
|
|
// but in practice, it is not always true, unfortunately.
|
|
// Surprisingly, seems subscribing via Notification Center is the most reliable way to get these events.
|
|
|
|
_textField.delegate = self;
|
|
|
|
[_textField addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged];
|
|
[_textField addTarget:self action:@selector(textFieldBeginEditing) forControlEvents:UIControlEventEditingDidBegin];
|
|
[_textField addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd];
|
|
[_textField addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit];
|
|
|
|
[_textField addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil];
|
|
|
|
[self addSubview:_textField];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|
|
|
- (void)dealloc
|
|
{
|
|
[_textField removeObserver:self forKeyPath:@"selectedTextRange"];
|
|
}
|
|
|
|
- (void)sendKeyValueForString:(NSString *)string
|
|
{
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
|
|
reactTag:self.reactTag
|
|
text:nil
|
|
key:string
|
|
eventCount:_nativeEventCount];
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
|
|
- (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets
|
|
{
|
|
_reactPaddingInsets = reactPaddingInsets;
|
|
// We apply `paddingInsets` as `_textField`'s `textContainerInset`.
|
|
_textField.textContainerInset = reactPaddingInsets;
|
|
[self setNeedsLayout];
|
|
}
|
|
|
|
- (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets
|
|
{
|
|
_reactBorderInsets = reactBorderInsets;
|
|
// We apply `borderInsets` as `_textView` layout offset.
|
|
_textField.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets);
|
|
[self setNeedsLayout];
|
|
}
|
|
|
|
- (void)setSelection:(RCTTextSelection *)selection
|
|
{
|
|
if (!selection) {
|
|
return;
|
|
}
|
|
|
|
UITextRange *currentSelection = _textField.selectedTextRange;
|
|
UITextPosition *start = [_textField positionFromPosition:_textField.beginningOfDocument offset:selection.start];
|
|
UITextPosition *end = [_textField positionFromPosition:_textField.beginningOfDocument offset:selection.end];
|
|
UITextRange *selectedTextRange = [_textField textRangeFromPosition:start toPosition:end];
|
|
|
|
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
|
if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) {
|
|
_previousSelectionRange = selectedTextRange;
|
|
_textField.selectedTextRange = selectedTextRange;
|
|
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
|
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
|
|
}
|
|
}
|
|
|
|
- (NSString *)text
|
|
{
|
|
return _textField.text;
|
|
}
|
|
|
|
- (void)setText:(NSString *)text
|
|
{
|
|
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
|
if (eventLag == 0 && ![text isEqualToString:self.text]) {
|
|
UITextRange *selection = _textField.selectedTextRange;
|
|
NSInteger oldTextLength = _textField.text.length;
|
|
|
|
_textField.text = text;
|
|
|
|
if (selection.empty) {
|
|
// maintain cursor position relative to the end of the old text
|
|
NSInteger offsetStart = [_textField offsetFromPosition:_textField.beginningOfDocument toPosition:selection.start];
|
|
NSInteger offsetFromEnd = oldTextLength - offsetStart;
|
|
NSInteger newOffset = text.length - offsetFromEnd;
|
|
UITextPosition *position = [_textField positionFromPosition:_textField.beginningOfDocument offset:newOffset];
|
|
_textField.selectedTextRange = [_textField textRangeFromPosition:position toPosition:position];
|
|
}
|
|
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
|
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", _textField.text, eventLag);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Events
|
|
|
|
- (void)textFieldDidChange
|
|
{
|
|
_nativeEventCount++;
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
|
|
reactTag:self.reactTag
|
|
text:_textField.text
|
|
key:nil
|
|
eventCount:_nativeEventCount];
|
|
|
|
// selectedTextRange observer isn't triggered when you type even though the
|
|
// cursor position moves, so we send event again here.
|
|
[self sendSelectionEvent];
|
|
}
|
|
|
|
- (void)textFieldEndEditing
|
|
{
|
|
if (![_finalText isEqualToString:_textField.text]) {
|
|
_finalText = nil;
|
|
// iOS does't send event `UIControlEventEditingChanged` if the change was happened because of autocorrection
|
|
// which was triggered by loosing focus. We assume that if `text` was changed in the middle of loosing focus process,
|
|
// we did not receive that event. So, we call `textFieldDidChange` manually.
|
|
[self textFieldDidChange];
|
|
}
|
|
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
|
|
reactTag:self.reactTag
|
|
text:_textField.text
|
|
key:nil
|
|
eventCount:_nativeEventCount];
|
|
}
|
|
|
|
- (void)textFieldSubmitEditing
|
|
{
|
|
_submitted = YES;
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit
|
|
reactTag:self.reactTag
|
|
text:_textField.text
|
|
key:nil
|
|
eventCount:_nativeEventCount];
|
|
}
|
|
|
|
- (void)textFieldBeginEditing
|
|
{
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
|
|
reactTag:self.reactTag
|
|
text:_textField.text
|
|
key:nil
|
|
eventCount:_nativeEventCount];
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (self->_selectTextOnFocus) {
|
|
[self->_textField selectAll:nil];
|
|
}
|
|
|
|
[self sendSelectionEvent];
|
|
});
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
ofObject:(__unused UITextField *)textField
|
|
change:(__unused NSDictionary *)change
|
|
context:(__unused void *)context
|
|
{
|
|
if ([keyPath isEqualToString:@"selectedTextRange"]) {
|
|
[self sendSelectionEvent];
|
|
}
|
|
}
|
|
|
|
- (void)sendSelectionEvent
|
|
{
|
|
if (_onSelectionChange &&
|
|
_textField.selectedTextRange != _previousSelectionRange &&
|
|
![_textField.selectedTextRange isEqual:_previousSelectionRange]) {
|
|
|
|
_previousSelectionRange = _textField.selectedTextRange;
|
|
|
|
UITextRange *selection = _textField.selectedTextRange;
|
|
NSInteger start = [_textField offsetFromPosition:[_textField beginningOfDocument] toPosition:selection.start];
|
|
NSInteger end = [_textField offsetFromPosition:[_textField beginningOfDocument] toPosition:selection.end];
|
|
_onSelectionChange(@{
|
|
@"selection": @{
|
|
@"start": @(start),
|
|
@"end": @(end),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
#pragma mark - Content Size (in Yoga terms, without any insets)
|
|
|
|
- (CGSize)contentSize
|
|
{
|
|
// Returning value does NOT include border and padding insets.
|
|
CGSize contentSize = self.intrinsicContentSize;
|
|
UIEdgeInsets compoundInsets = self.reactCompoundInsets;
|
|
contentSize.width -= compoundInsets.left + compoundInsets.right;
|
|
contentSize.height -= compoundInsets.top + compoundInsets.bottom;
|
|
return contentSize;
|
|
}
|
|
|
|
- (void)invalidateContentSize
|
|
{
|
|
CGSize contentSize = self.contentSize;
|
|
|
|
if (CGSizeEqualToSize(_previousContentSize, contentSize)) {
|
|
return;
|
|
}
|
|
_previousContentSize = contentSize;
|
|
|
|
[_bridge.uiManager setIntrinsicContentSize:contentSize forView:self];
|
|
}
|
|
|
|
#pragma mark - Layout (in UIKit terms, with all insets)
|
|
|
|
- (CGSize)intrinsicContentSize
|
|
{
|
|
// Returning value DOES include border and padding insets.
|
|
CGSize size = _textField.intrinsicContentSize;
|
|
size.width += _reactBorderInsets.left + _reactBorderInsets.right;
|
|
size.height += _reactBorderInsets.top + _reactBorderInsets.bottom;
|
|
return size;
|
|
}
|
|
|
|
- (CGSize)sizeThatFits:(CGSize)size
|
|
{
|
|
CGFloat compoundHorizontalBorderInset = _reactBorderInsets.left + _reactBorderInsets.right;
|
|
CGFloat compoundVerticalBorderInset = _reactBorderInsets.top + _reactBorderInsets.bottom;
|
|
|
|
size.width -= compoundHorizontalBorderInset;
|
|
size.height -= compoundVerticalBorderInset;
|
|
|
|
// Note: `paddingInsets` already included in `_textView` size
|
|
// because it was applied as `textContainerInset`.
|
|
CGSize fittingSize = [_textField sizeThatFits:size];
|
|
|
|
fittingSize.width += compoundHorizontalBorderInset;
|
|
fittingSize.height += compoundVerticalBorderInset;
|
|
|
|
return fittingSize;
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
[super layoutSubviews];
|
|
[self invalidateContentSize];
|
|
}
|
|
|
|
#pragma mark - UITextFieldDelegate
|
|
|
|
- (BOOL)textField:(RCTTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
|
|
{
|
|
// Only allow single keypresses for `onKeyPress`, pasted text will not be sent.
|
|
if (!_textField.textWasPasted) {
|
|
[self sendKeyValueForString:string];
|
|
}
|
|
|
|
if (_maxLength != nil && ![string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return.
|
|
NSUInteger allowedLength = _maxLength.integerValue - MIN(_maxLength.integerValue, _textField.text.length) + range.length;
|
|
if (string.length > allowedLength) {
|
|
if (string.length > 1) {
|
|
// Truncate the input string so the result is exactly `maxLength`.
|
|
NSString *limitedString = [string substringToIndex:allowedLength];
|
|
NSMutableString *newString = _textField.text.mutableCopy;
|
|
[newString replaceCharactersInRange:range withString:limitedString];
|
|
_textField.text = newString;
|
|
|
|
// Collapse selection at end of insert to match normal paste behavior.
|
|
UITextPosition *insertEnd = [_textField positionFromPosition:_textField.beginningOfDocument
|
|
offset:(range.location + allowedLength)];
|
|
_textField.selectedTextRange = [_textField textRangeFromPosition:insertEnd toPosition:insertEnd];
|
|
[self textFieldDidChange];
|
|
}
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
// This method allows us to detect a `Backspace` keyPress
|
|
// even when there is no more text in the TextField.
|
|
- (BOOL)keyboardInputShouldDelete:(RCTTextField *)textField
|
|
{
|
|
[self textField:_textField shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField
|
|
{
|
|
_finalText = _textField.text;
|
|
|
|
if (_submitted) {
|
|
_submitted = NO;
|
|
return _blurOnSubmit;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)textFieldDidEndEditing:(UITextField *)textField
|
|
{
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
|
|
reactTag:self.reactTag
|
|
text:self.text
|
|
key:nil
|
|
eventCount:_nativeEventCount];
|
|
}
|
|
|
|
#pragma mark - Accessibility
|
|
|
|
- (UIView *)reactAccessibilityElement
|
|
{
|
|
return _textField;
|
|
}
|
|
|
|
#pragma mark - Focus control deledation
|
|
|
|
- (void)reactFocus
|
|
{
|
|
[_textField reactFocus];
|
|
}
|
|
|
|
- (void)reactBlur
|
|
{
|
|
[_textField reactBlur];
|
|
}
|
|
|
|
- (void)didMoveToWindow
|
|
{
|
|
[_textField reactFocusIfNeeded];
|
|
}
|
|
|
|
@end
|