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
Expand Up @@ -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
108 changes: 99 additions & 9 deletions Libraries/Text/RCTTextField.m
Expand Up @@ -16,13 +16,45 @@


#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;
BOOL _jsRequestingFirstResponder; BOOL _jsRequestingFirstResponder;
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
Expand All @@ -36,6 +68,11 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
[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;
} }
Expand Down Expand Up @@ -176,6 +213,14 @@ - (void)textFieldDidChange


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


RCT_EXPORT_MODULE() 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)
Expand Down

0 comments on commit a341e9d

Please sign in to comment.