From e13b9c6e49480e8262df06b7c1e99caab74e801f Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Mon, 29 May 2017 15:56:47 -0700 Subject: [PATCH] RCTTextField was spliited into two classes Summary: Motivation: * We maintain two different implementation of (multilined and singlelined), this change makes the implementations much similar which will help us to support and improve both of them in the (near) future; * We have to have separated RCTView-based container view for (TextField) to support sofisticated bordering and so on; * It opens to us possibility to unify UITextView and UITextField subclasses and remove code duplication across RCTTextView and RCTTextField; * Making things decoupled in general will allow us to fix existing bugs with events. Reviewed By: mmmulani Differential Revision: D5083010 fbshipit-source-id: 2f2d42c2244d2b39256c51480c1f16f4e3947c01 --- .../Text/RCTText.xcodeproj/project.pbxproj | 8 + Libraries/Text/RCTTextField.h | 10 +- Libraries/Text/RCTTextField.m | 318 +++++++++--------- Libraries/Text/RCTTextFieldManager.m | 46 +-- Libraries/Text/RCTTextView.m | 4 +- Libraries/Text/RCTUITextField.h | 28 ++ Libraries/Text/RCTUITextField.m | 116 +++++++ Libraries/Text/RCTUITextView.m | 2 +- RNTester/js/TextInputExample.ios.js | 10 +- React/ReactLegacy.xcodeproj/project.pbxproj | 24 +- 10 files changed, 373 insertions(+), 193 deletions(-) create mode 100644 Libraries/Text/RCTUITextField.h create mode 100644 Libraries/Text/RCTUITextField.m diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index 9c2c8a8f0d8b3a..92648898367d2a 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; }; 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */; }; 58B512161A9E6EFF00147676 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512141A9E6EFF00147676 /* RCTText.m */; }; + 59AF89AA1EDCBCC700F004B1 /* RCTUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 59AF89A91EDCBCC700F004B1 /* RCTUITextField.m */; }; + 59AF89AB1EDCBCC700F004B1 /* RCTUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 59AF89A91EDCBCC700F004B1 /* RCTUITextField.m */; }; 59B125C91E6E4E15004E2A67 /* RCTUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59B125C81E6E4E15004E2A67 /* RCTUITextView.m */; }; 59B125CA1E6E4E15004E2A67 /* RCTUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59B125C81E6E4E15004E2A67 /* RCTUITextView.m */; }; 59F60E911E661BDD0081153B /* RCTShadowTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 59F60E8E1E661BDD0081153B /* RCTShadowTextField.m */; }; @@ -60,6 +62,8 @@ 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextManager.m; sourceTree = ""; }; 58B512141A9E6EFF00147676 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = ""; }; 58B512151A9E6EFF00147676 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = ""; }; + 59AF89A81EDCBCC700F004B1 /* RCTUITextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUITextField.h; sourceTree = ""; }; + 59AF89A91EDCBCC700F004B1 /* RCTUITextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUITextField.m; sourceTree = ""; }; 59B125C71E6E4E15004E2A67 /* RCTUITextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUITextView.h; sourceTree = ""; }; 59B125C81E6E4E15004E2A67 /* RCTUITextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUITextView.m; sourceTree = ""; }; 59F60E8D1E661BDD0081153B /* RCTShadowTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowTextField.h; sourceTree = ""; }; @@ -101,6 +105,8 @@ 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */, 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */, 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */, + 59AF89A81EDCBCC700F004B1 /* RCTUITextField.h */, + 59AF89A91EDCBCC700F004B1 /* RCTUITextField.m */, 59B125C71E6E4E15004E2A67 /* RCTUITextView.h */, 59B125C81E6E4E15004E2A67 /* RCTUITextView.m */, ); @@ -196,6 +202,7 @@ 2D3B5F391D9B106F00451313 /* RCTTextField.m in Sources */, 2D3B5F361D9B106F00451313 /* RCTShadowText.m in Sources */, 2D3B5F3B1D9B106F00451313 /* RCTTextView.m in Sources */, + 59AF89AB1EDCBCC700F004B1 /* RCTUITextField.m in Sources */, 2D3B5F3A1D9B106F00451313 /* RCTTextFieldManager.m in Sources */, 2D3B5F341D9B103100451313 /* RCTRawTextManager.m in Sources */, 59F60E921E661BDD0081153B /* RCTShadowTextField.m in Sources */, @@ -217,6 +224,7 @@ 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */, 19FC5C851D41A4120090108F /* RCTTextSelection.m in Sources */, 1362F1001B4D51F400E06D8C /* RCTTextField.m in Sources */, + 59AF89AA1EDCBCC700F004B1 /* RCTUITextField.m in Sources */, 58B512161A9E6EFF00147676 /* RCTText.m in Sources */, 1362F1011B4D51F400E06D8C /* RCTTextFieldManager.m in Sources */, 59F60E911E661BDD0081153B /* RCTShadowTextField.m in Sources */, diff --git a/Libraries/Text/RCTTextField.h b/Libraries/Text/RCTTextField.h index e7279024dd2fa3..27fd5cf2c4e2c4 100644 --- a/Libraries/Text/RCTTextField.h +++ b/Libraries/Text/RCTTextField.h @@ -10,15 +10,16 @@ #import #import +#import @class RCTBridge; +@class RCTUITextField; -@interface RCTTextField : UITextField +@interface RCTTextField : RCTView @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) BOOL blurOnSubmit; -@property (nonatomic, strong) UIColor *placeholderTextColor; @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, strong) NSNumber *maxLength; @property (nonatomic, assign) UIEdgeInsets reactPaddingInsets; @@ -26,6 +27,11 @@ @property (nonatomic, copy) RCTDirectEventBlock onSelectionChange; +@property (nonatomic, strong) RCTUITextField *textField; + - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; + @end diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index 379d9231e6674c..b6b632a843e83c 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -17,33 +17,9 @@ #import #import "RCTTextSelection.h" +#import "RCTUITextField.h" -@interface RCTTextField() - -- (BOOL)shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string; -- (BOOL)keyboardInputShouldDelete; -- (BOOL)textFieldShouldEndEditing; - -@end - -@interface RCTTextFieldDelegateProxy: NSObject -@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]; -} +@interface RCTTextField () @end @@ -54,9 +30,8 @@ @implementation RCTTextField NSInteger _nativeEventCount; BOOL _submitted; UITextRange *_previousSelectionRange; - BOOL _textWasPasted; NSString *_finalText; - RCTTextFieldDelegateProxy *_delegateProxy; + CGSize _previousContentSize; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -66,30 +41,38 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _bridge = bridge; _eventDispatcher = bridge.eventDispatcher; - _blurOnSubmit = YES; - - [self addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged]; - [self addTarget:self action:@selector(textFieldBeginEditing) forControlEvents:UIControlEventEditingDidBegin]; - [self addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; - [self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; - [self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil]; - - // 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; + + _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 { - [self removeObserver:self forKeyPath:@"selectedTextRange"]; + [_textField removeObserver:self forKeyPath:@"selectedTextRange"]; } -RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - - (void)sendKeyValueForString:(NSString *)string { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress @@ -99,12 +82,22 @@ - (void)sendKeyValueForString:(NSString *)string eventCount:_nativeEventCount]; } -// This method is overridden for `onKeyPress`. The manager -// will not send a keyPress for text that was pasted. -- (void)paste:(id)sender +#pragma mark - Properties + +- (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets { - _textWasPasted = YES; - [super paste:sender]; + _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 @@ -113,90 +106,47 @@ - (void)setSelection:(RCTTextSelection *)selection return; } - UITextRange *currentSelection = self.selectedTextRange; - UITextPosition *start = [self positionFromPosition:self.beginningOfDocument offset:selection.start]; - UITextPosition *end = [self positionFromPosition:self.beginningOfDocument offset:selection.end]; - UITextRange *selectedTextRange = [self textRangeFromPosition:start toPosition:end]; + 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; - self.selectedTextRange = 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 = self.selectedTextRange; - NSInteger oldTextLength = self.text.length; + UITextRange *selection = _textField.selectedTextRange; + NSInteger oldTextLength = _textField.text.length; - super.text = text; + _textField.text = text; if (selection.empty) { // maintain cursor position relative to the end of the old text - NSInteger offsetStart = [self offsetFromPosition:self.beginningOfDocument toPosition:selection.start]; + NSInteger offsetStart = [_textField offsetFromPosition:_textField.beginningOfDocument toPosition:selection.start]; NSInteger offsetFromEnd = oldTextLength - offsetStart; NSInteger newOffset = text.length - offsetFromEnd; - UITextPosition *position = [self positionFromPosition:self.beginningOfDocument offset:newOffset]; - self.selectedTextRange = [self textRangeFromPosition:position toPosition:position]; + 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.", self.text, eventLag); + RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", _textField.text, eventLag); } } -- (void)updatePlaceholder -{ - if (self.placeholder == nil) { - return; - } - - NSMutableDictionary *attributes = [NSMutableDictionary new]; - if (self.placeholderTextColor) { - [attributes setObject:self.placeholderTextColor forKey:NSForegroundColorAttributeName]; - } - - self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder - attributes:attributes]; -} - -- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor -{ - _placeholderTextColor = placeholderTextColor; - [self updatePlaceholder]; -} - -- (void)setPlaceholder:(NSString *)placeholder -{ - super.placeholder = placeholder; - [self updatePlaceholder]; - [self updateIntrinsicContentSize]; -} - -- (CGRect)caretRectForPosition:(UITextPosition *)position -{ - if (_caretHidden) { - return CGRectZero; - } - return [super caretRectForPosition:position]; -} - -#pragma mark - Positioning Overrides - -- (CGRect)textRectForBounds:(CGRect)bounds -{ - return UIEdgeInsetsInsetRect([super textRectForBounds:bounds], self.reactCompoundInsets); -} - -- (CGRect)editingRectForBounds:(CGRect)bounds -{ - return [self textRectForBounds:bounds]; -} - #pragma mark - Events - (void)textFieldDidChange @@ -204,7 +154,7 @@ - (void)textFieldDidChange _nativeEventCount++; [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange reactTag:self.reactTag - text:self.text + text:_textField.text key:nil eventCount:_nativeEventCount]; @@ -215,7 +165,7 @@ - (void)textFieldDidChange - (void)textFieldEndEditing { - if (![_finalText isEqualToString:self.text]) { + 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, @@ -225,7 +175,7 @@ - (void)textFieldEndEditing [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag - text:self.text + text:_textField.text key:nil eventCount:_nativeEventCount]; } @@ -235,7 +185,7 @@ - (void)textFieldSubmitEditing _submitted = YES; [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit reactTag:self.reactTag - text:self.text + text:_textField.text key:nil eventCount:_nativeEventCount]; } @@ -244,13 +194,13 @@ - (void)textFieldBeginEditing { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag - text:self.text + text:_textField.text key:nil eventCount:_nativeEventCount]; dispatch_async(dispatch_get_main_queue(), ^{ if (self->_selectTextOnFocus) { - [self selectAll:nil]; + [self->_textField selectAll:nil]; } [self sendSelectionEvent]; @@ -258,9 +208,9 @@ - (void)textFieldBeginEditing } - (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(RCTTextField *)textField - change:(NSDictionary *)change - context:(void *)context + ofObject:(__unused UITextField *)textField + change:(__unused NSDictionary *)change + context:(__unused void *)context { if ([keyPath isEqualToString:@"selectedTextRange"]) { [self sendSelectionEvent]; @@ -270,14 +220,14 @@ - (void)observeValueForKeyPath:(NSString *)keyPath - (void)sendSelectionEvent { if (_onSelectionChange && - self.selectedTextRange != _previousSelectionRange && - ![self.selectedTextRange isEqual:_previousSelectionRange]) { + _textField.selectedTextRange != _previousSelectionRange && + ![_textField.selectedTextRange isEqual:_previousSelectionRange]) { - _previousSelectionRange = self.selectedTextRange; + _previousSelectionRange = _textField.selectedTextRange; - UITextRange *selection = self.selectedTextRange; - NSInteger start = [self offsetFromPosition:[self beginningOfDocument] toPosition:selection.start]; - NSInteger end = [self offsetFromPosition:[self beginningOfDocument] toPosition:selection.end]; + 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), @@ -287,64 +237,88 @@ - (void)sendSelectionEvent } } -- (BOOL)resignFirstResponder +#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 { - BOOL result = [super resignFirstResponder]; - if (result) - { - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur - reactTag:self.reactTag - text:self.text - key:nil - eventCount:_nativeEventCount]; + CGSize contentSize = self.contentSize; + + if (CGSizeEqualToSize(_previousContentSize, contentSize)) { + return; } - return result; + _previousContentSize = contentSize; + + [_bridge.uiManager setIntrinsicContentSize:contentSize forView:self]; } -- (void)didMoveToWindow +#pragma mark - Layout (in UIKit terms, with all insets) + +- (CGSize)intrinsicContentSize { - [self reactFocusIfNeeded]; + // 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; } -- (void)setFont:(UIFont *)font +- (CGSize)sizeThatFits:(CGSize)size { - [super setFont:font]; - [self updateIntrinsicContentSize]; + 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)updateIntrinsicContentSize +- (void)layoutSubviews { - NSString *text = self.placeholder ?: @""; - CGSize size = [text sizeWithAttributes:@{NSFontAttributeName: self.font}]; - size = CGSizeMake(RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)); - [_bridge.uiManager setIntrinsicContentSize:size forView:self]; + [super layoutSubviews]; + [self invalidateContentSize]; } -#pragma mark - UITextFieldDelegate (Proxied) +#pragma mark - UITextFieldDelegate -- (BOOL)shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string +- (BOOL)textField:(RCTTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { // Only allow single keypresses for `onKeyPress`, pasted text will not be sent. - if (_textWasPasted) { - _textWasPasted = NO; - } else { + 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, self.text.length) + range.length; + 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 = self.text.mutableCopy; + NSMutableString *newString = _textField.text.mutableCopy; [newString replaceCharactersInRange:range withString:limitedString]; - self.text = newString; + _textField.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]; + UITextPosition *insertEnd = [_textField positionFromPosition:_textField.beginningOfDocument + offset:(range.location + allowedLength)]; + _textField.selectedTextRange = [_textField textRangeFromPosition:insertEnd toPosition:insertEnd]; [self textFieldDidChange]; } return NO; @@ -356,15 +330,15 @@ - (BOOL)shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString // This method allows us to detect a `Backspace` keyPress // even when there is no more text in the TextField. -- (BOOL)keyboardInputShouldDelete +- (BOOL)keyboardInputShouldDelete:(RCTTextField *)textField { - [self shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""]; + [self textField:_textField shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""]; return YES; } -- (BOOL)textFieldShouldEndEditing +- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField { - _finalText = self.text; + _finalText = _textField.text; if (_submitted) { _submitted = NO; @@ -374,4 +348,30 @@ - (BOOL)textFieldShouldEndEditing return YES; } +- (void)textFieldDidEndEditing:(UITextField *)textField +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur + reactTag:self.reactTag + text:self.text + key:nil + eventCount:_nativeEventCount]; +} + +#pragma mark - Focus control deledation + +- (void)reactFocus +{ + [_textField reactFocus]; +} + +- (void)reactBlur +{ + [_textField reactBlur]; +} + +- (void)didMoveToWindow +{ + [_textField reactFocusIfNeeded]; +} + @end diff --git a/Libraries/Text/RCTTextFieldManager.m b/Libraries/Text/RCTTextFieldManager.m index 5e74e440d7fb54..104882c57fa983 100644 --- a/Libraries/Text/RCTTextFieldManager.m +++ b/Libraries/Text/RCTTextFieldManager.m @@ -17,7 +17,7 @@ #import "RCTConvert+Text.h" #import "RCTShadowTextField.h" #import "RCTTextField.h" - +#import "RCTUITextField.h" @implementation RCTTextFieldManager @@ -33,45 +33,45 @@ - (UIView *)view return [[RCTTextField alloc] initWithBridge:self.bridge]; } -RCT_EXPORT_VIEW_PROPERTY(caretHidden, BOOL) -RCT_REMAP_VIEW_PROPERTY(autoCorrect, autocorrectionType, UITextAutocorrectionType) -RCT_REMAP_VIEW_PROPERTY(spellCheck, spellCheckingType, UITextSpellCheckingType) -RCT_REMAP_VIEW_PROPERTY(editable, enabled, BOOL) -RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) -RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) +RCT_REMAP_VIEW_PROPERTY(caretHidden, textField.caretHidden, BOOL) +RCT_REMAP_VIEW_PROPERTY(autoCorrect, textField.autocorrectionType, UITextAutocorrectionType) +RCT_REMAP_VIEW_PROPERTY(spellCheck, textField.spellCheckingType, UITextSpellCheckingType) +RCT_REMAP_VIEW_PROPERTY(editable, textField.enabled, BOOL) +RCT_REMAP_VIEW_PROPERTY(placeholder, textField.placeholder, NSString) +RCT_REMAP_VIEW_PROPERTY(placeholderTextColor, textField.placeholderColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection) RCT_EXPORT_VIEW_PROPERTY(text, NSString) RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber) -RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode) -RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL) +RCT_REMAP_VIEW_PROPERTY(clearButtonMode, textField.clearButtonMode, UITextFieldViewMode) +RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, textField.clearsOnBeginEditing, BOOL) RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(blurOnSubmit, BOOL) -RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) -RCT_EXPORT_VIEW_PROPERTY(keyboardAppearance, UIKeyboardAppearance) +RCT_REMAP_VIEW_PROPERTY(keyboardType, textField.keyboardType, UIKeyboardType) +RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, textField.keyboardAppearance, UIKeyboardAppearance) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(returnKeyType, UIReturnKeyType) -RCT_EXPORT_VIEW_PROPERTY(enablesReturnKeyAutomatically, BOOL) -RCT_EXPORT_VIEW_PROPERTY(secureTextEntry, BOOL) -RCT_REMAP_VIEW_PROPERTY(password, secureTextEntry, BOOL) // backwards compatibility -RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) -RCT_REMAP_VIEW_PROPERTY(autoCapitalize, autocapitalizationType, UITextAutocapitalizationType) -RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment, NSTextAlignment) -RCT_REMAP_VIEW_PROPERTY(selectionColor, tintColor, UIColor) +RCT_REMAP_VIEW_PROPERTY(returnKeyType, textField.returnKeyType, UIReturnKeyType) +RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textField.enablesReturnKeyAutomatically, BOOL) +RCT_REMAP_VIEW_PROPERTY(secureTextEntry, textField.secureTextEntry, BOOL) +RCT_REMAP_VIEW_PROPERTY(password, textField.secureTextEntry, BOOL) // backwards compatibility +RCT_REMAP_VIEW_PROPERTY(color, textField.textColor, UIColor) +RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textField.autocapitalizationType, UITextAutocapitalizationType) +RCT_REMAP_VIEW_PROPERTY(textAlign, textField.textAlignment, NSTextAlignment) +RCT_REMAP_VIEW_PROPERTY(selectionColor, textField.tintColor, UIColor) RCT_CUSTOM_VIEW_PROPERTY(fontSize, NSNumber, RCTTextField) { - view.font = [RCTFont updateFont:view.font withSize:json ?: @(defaultView.font.pointSize)]; + view.textField.font = [RCTFont updateFont:view.textField.font withSize:json ?: @(defaultView.textField.font.pointSize)]; } RCT_CUSTOM_VIEW_PROPERTY(fontWeight, NSString, __unused RCTTextField) { - view.font = [RCTFont updateFont:view.font withWeight:json]; // defaults to normal + view.textField.font = [RCTFont updateFont:view.textField.font withWeight:json]; // defaults to normal } RCT_CUSTOM_VIEW_PROPERTY(fontStyle, NSString, __unused RCTTextField) { - view.font = [RCTFont updateFont:view.font withStyle:json]; // defaults to normal + view.textField.font = [RCTFont updateFont:view.textField.font withStyle:json]; // defaults to normal } RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextField) { - view.font = [RCTFont updateFont:view.font withFamily:json ?: defaultView.font.familyName]; + view.textField.font = [RCTFont updateFont:view.textField.font withFamily:json ?: defaultView.textField.font.familyName]; } RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 1e609b6f3cfd2b..2e63a4b2c6aad3 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -547,7 +547,7 @@ - (void)didMoveToWindow [_textView reactFocusIfNeeded]; } -#pragma mark - Content size +#pragma mark - Content Size (in Yoga terms, without any insets) - (CGSize)contentSize { @@ -581,7 +581,7 @@ - (void)invalidateContentSize } } -#pragma mark - Layout +#pragma mark - Layout (in UIKit terms, with all insets) - (CGSize)intrinsicContentSize { diff --git a/Libraries/Text/RCTUITextField.h b/Libraries/Text/RCTUITextField.h new file mode 100644 index 00000000000000..0079539d5f0c45 --- /dev/null +++ b/Libraries/Text/RCTUITextField.h @@ -0,0 +1,28 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/* + * Just regular UITextField... but much better! + */ +@interface RCTUITextField : UITextField + +- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE; + +@property (nonatomic, assign) BOOL caretHidden; +@property (nonatomic, assign, readonly) BOOL textWasPasted; +@property (nonatomic, strong, nullable) UIColor *placeholderColor; +@property (nonatomic, assign) UIEdgeInsets textContainerInset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/Text/RCTUITextField.m b/Libraries/Text/RCTUITextField.m new file mode 100644 index 00000000000000..3ab4036fd7ca9c --- /dev/null +++ b/Libraries/Text/RCTUITextField.m @@ -0,0 +1,116 @@ +/** + * 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 "RCTUITextField.h" + +#import +#import + +@implementation RCTUITextField + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_textDidChange) + name:UITextFieldTextDidChangeNotification + object:self]; + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)_textDidChange +{ + _textWasPasted = NO; +} + +#pragma mark - Properties + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + _textContainerInset = textContainerInset; + [self setNeedsLayout]; +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + [super setPlaceholder:placeholder]; + [self _updatePlaceholder]; +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + _placeholderColor = placeholderColor; + [self _updatePlaceholder]; +} + +- (void)_updatePlaceholder +{ + if (self.placeholder == nil) { + return; + } + + NSMutableDictionary *attributes = [NSMutableDictionary new]; + if (_placeholderColor) { + [attributes setObject:_placeholderColor forKey:NSForegroundColorAttributeName]; + } + + self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder + attributes:attributes]; +} + +#pragma mark - Caret Manipulation + +- (CGRect)caretRectForPosition:(UITextPosition *)position +{ + if (_caretHidden) { + return CGRectZero; + } + + return [super caretRectForPosition:position]; +} + +#pragma mark - Positioning Overrides + +- (CGRect)textRectForBounds:(CGRect)bounds +{ + return UIEdgeInsetsInsetRect([super textRectForBounds:bounds], _textContainerInset); +} + +- (CGRect)editingRectForBounds:(CGRect)bounds +{ + return [self textRectForBounds:bounds]; +} + +#pragma mark - Layout + +- (CGSize)intrinsicContentSize +{ + // Note: `placeholder` defines intrinsic size for ``. + NSString *text = self.placeholder ?: @""; + CGSize size = [text sizeWithAttributes:@{NSFontAttributeName: self.font}]; + size = CGSizeMake(RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)); + size.width += _textContainerInset.left + _textContainerInset.right; + size.height += _textContainerInset.top + _textContainerInset.bottom; + return size; +} + +- (CGSize)sizeThatFits:(CGSize)size +{ + CGSize intrinsicSize = self.intrinsicContentSize; + return CGSizeMake(MIN(size.width, intrinsicSize.width), MIN(size.height, intrinsicSize.height)); +} + +@end diff --git a/Libraries/Text/RCTUITextView.m b/Libraries/Text/RCTUITextView.m index b5ed2168899d37..7b4711a941f5d6 100644 --- a/Libraries/Text/RCTUITextView.m +++ b/Libraries/Text/RCTUITextView.m @@ -9,8 +9,8 @@ #import "RCTUITextView.h" -#import #import +#import @implementation RCTUITextView { diff --git a/RNTester/js/TextInputExample.ios.js b/RNTester/js/TextInputExample.ios.js index 824c484403cb99..88d6f3611fe5cc 100644 --- a/RNTester/js/TextInputExample.ios.js +++ b/RNTester/js/TextInputExample.ios.js @@ -773,6 +773,9 @@ exports.examples = [ backgroundColor: '#eeeeee', borderColor: '#666666', borderWidth: 5, + borderTopWidth: 20, + borderRadius: 10, + borderBottomRightRadius: 20, padding: 10, paddingTop: 20, }} @@ -780,7 +783,7 @@ exports.examples = [ /> Multiline TextInput - +