Fixed calling TextInput.onChange() on applying autocorrection (iOS), Second part

Reviewed By: mmmulani

Differential Revision: D4459603

fbshipit-source-id: f1ee25a9068213a54f2c088760297ba9c2838623
This commit is contained in:
Valentin Shergin 2017-01-30 15:36:57 -08:00 committed by Facebook Github Bot
parent a3f86ae984
commit a341e9d916
3 changed files with 101 additions and 66 deletions

View File

@ -22,14 +22,9 @@
@property (nonatomic, strong) UIColor *placeholderTextColor; @property (nonatomic, strong) UIColor *placeholderTextColor;
@property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, assign) NSInteger mostRecentEventCount;
@property (nonatomic, strong) NSNumber *maxLength; @property (nonatomic, strong) NSNumber *maxLength;
@property (nonatomic, assign) BOOL textWasPasted;
@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange; @property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
- (void)textFieldDidChange;
- (void)sendKeyValueForString:(NSString *)string;
- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField;
@end @end

View File

@ -16,6 +16,35 @@
#import "RCTTextSelection.h" #import "RCTTextSelection.h"
@interface RCTTextField()
- (BOOL)shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string;
- (BOOL)keyboardInputShouldDelete;
- (BOOL)textFieldShouldEndEditing;
@end
@interface RCTTextFieldDelegateProxy: NSObject <UITextFieldDelegate>
@end
@implementation RCTTextFieldDelegateProxy
- (BOOL)textField:(RCTTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
return [textField shouldChangeCharactersInRange:range replacementString:string];
}
- (BOOL)keyboardInputShouldDelete:(RCTTextField *)textField
{
return [textField keyboardInputShouldDelete];
}
- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField {
return [textField textFieldShouldEndEditing];
}
@end
@implementation RCTTextField @implementation RCTTextField
{ {
RCTEventDispatcher *_eventDispatcher; RCTEventDispatcher *_eventDispatcher;
@ -23,6 +52,9 @@
NSInteger _nativeEventCount; NSInteger _nativeEventCount;
BOOL _submitted; BOOL _submitted;
UITextRange *_previousSelectionRange; UITextRange *_previousSelectionRange;
BOOL _textWasPasted;
NSString *_finalText;
RCTTextFieldDelegateProxy *_delegateProxy;
} }
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
@ -36,6 +68,11 @@
[self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; [self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit];
[self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil]; [self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil];
_blurOnSubmit = YES; _blurOnSubmit = YES;
// We cannot use `self.delegate = self;` here because `UITextField` implements some of these delegate methods itself,
// so if we implement this delegate on self, we will override some of its behaviours.
_delegateProxy = [RCTTextFieldDelegateProxy new];
self.delegate = _delegateProxy;
} }
return self; return self;
} }
@ -176,6 +213,14 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
- (void)textFieldEndEditing - (void)textFieldEndEditing
{ {
if (![_finalText isEqualToString:self.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 [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
reactTag:self.reactTag reactTag:self.reactTag
text:self.text text:self.text
@ -209,15 +254,6 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
}); });
} }
- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField
{
if (_submitted) {
_submitted = NO;
return _blurOnSubmit;
}
return YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath - (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(RCTTextField *)textField ofObject:(RCTTextField *)textField
change:(NSDictionary *)change change:(NSDictionary *)change
@ -277,4 +313,58 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
return result; return result;
} }
#pragma mark - UITextFieldDelegate (Proxied)
- (BOOL)shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
// Only allow single keypresses for `onKeyPress`, pasted text will not be sent.
if (_textWasPasted) {
_textWasPasted = NO;
} else {
[self sendKeyValueForString:string];
}
if (_maxLength != nil && ![string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return.
NSUInteger allowedLength = _maxLength.integerValue - MIN(_maxLength.integerValue, self.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 = self.text.mutableCopy;
[newString replaceCharactersInRange:range withString:limitedString];
self.text = newString;
// Collapse selection at end of insert to match normal paste behavior.
UITextPosition *insertEnd = [self positionFromPosition:self.beginningOfDocument
offset:(range.location + allowedLength)];
self.selectedTextRange = [self 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
{
[self shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""];
return YES;
}
- (BOOL)textFieldShouldEndEditing
{
_finalText = self.text;
if (_submitted) {
_submitted = NO;
return _blurOnSubmit;
}
return YES;
}
@end @end

View File

@ -13,12 +13,9 @@
#import <React/RCTFont.h> #import <React/RCTFont.h>
#import <React/RCTShadowView.h> #import <React/RCTShadowView.h>
#import "RCTTextField.h"
#import "RCTConvert+Text.h" #import "RCTConvert+Text.h"
#import "RCTTextField.h"
@interface RCTTextFieldManager() <UITextFieldDelegate>
@end
@implementation RCTTextFieldManager @implementation RCTTextFieldManager
@ -26,54 +23,7 @@ RCT_EXPORT_MODULE()
- (UIView *)view - (UIView *)view
{ {
RCTTextField *textField = [[RCTTextField alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; return [[RCTTextField alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
textField.delegate = self;
return textField;
}
- (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) {
textField.textWasPasted = NO;
} else {
[textField sendKeyValueForString:string];
}
if (textField.maxLength == nil || [string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return
return YES;
}
NSUInteger allowedLength = textField.maxLength.integerValue - MIN(textField.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];
[textField textFieldDidChange];
}
return NO;
} else {
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
{
return [textField textFieldShouldEndEditing:textField];
} }
RCT_EXPORT_VIEW_PROPERTY(caretHidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(caretHidden, BOOL)