Skip to content

Commit

Permalink
Fixed calling TextInput.onChange() on applying autocorrection (iOS), …
Browse files Browse the repository at this point in the history
…Second part

Reviewed By: mmmulani

Differential Revision: D4459603

fbshipit-source-id: f1ee25a9068213a54f2c088760297ba9c2838623
  • Loading branch information
shergin authored and facebook-github-bot committed Jan 30, 2017
1 parent a3f86ae commit a341e9d
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 66 deletions.
5 changes: 0 additions & 5 deletions Libraries/Text/RCTTextField.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,9 @@
@property (nonatomic, strong) UIColor *placeholderTextColor;
@property (nonatomic, assign) NSInteger mostRecentEventCount;
@property (nonatomic, strong) NSNumber *maxLength;
@property (nonatomic, assign) BOOL textWasPasted;

@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;

- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;

- (void)textFieldDidChange;
- (void)sendKeyValueForString:(NSString *)string;
- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField;

@end
108 changes: 99 additions & 9 deletions Libraries/Text/RCTTextField.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,45 @@

#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
{
RCTEventDispatcher *_eventDispatcher;
BOOL _jsRequestingFirstResponder;
NSInteger _nativeEventCount;
BOOL _submitted;
UITextRange *_previousSelectionRange;
BOOL _textWasPasted;
NSString *_finalText;
RCTTextFieldDelegateProxy *_delegateProxy;
}

- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
Expand All @@ -36,6 +68,11 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
[self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit];
[self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil];
_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;
}
Expand Down Expand Up @@ -176,6 +213,14 @@ - (void)textFieldDidChange

- (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
reactTag:self.reactTag
text:self.text
Expand Down Expand Up @@ -209,15 +254,6 @@ - (void)textFieldBeginEditing
});
}

- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField
{
if (_submitted) {
_submitted = NO;
return _blurOnSubmit;
}
return YES;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(RCTTextField *)textField
change:(NSDictionary *)change
Expand Down Expand Up @@ -277,4 +313,58 @@ - (BOOL)resignFirstResponder
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
54 changes: 2 additions & 52 deletions Libraries/Text/RCTTextFieldManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,67 +13,17 @@
#import <React/RCTFont.h>
#import <React/RCTShadowView.h>

#import "RCTTextField.h"
#import "RCTConvert+Text.h"
#import "RCTTextField.h"

@interface RCTTextFieldManager() <UITextFieldDelegate>

@end

@implementation RCTTextFieldManager

RCT_EXPORT_MODULE()

- (UIView *)view
{
RCTTextField *textField = [[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];
return [[RCTTextField alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
}

RCT_EXPORT_VIEW_PROPERTY(caretHidden, BOOL)
Expand Down

0 comments on commit a341e9d

Please sign in to comment.