From 961c1eb42904a4d5516fd7939ba14bc0625309d3 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Tue, 21 Jul 2015 12:37:24 -0700 Subject: [PATCH] [ReactNative] TextInput bug fixes and features Summary: This introduces event counts to make sure JS doesn't set out of date values on native text inputs, which can cause dropped characters and can mess with autocomplete, and obviates the need for the input buffering which added lag and complexity to the component. Made sure to test simulated super-slow JS text event processing to make sure characters aren't dropped, as well as typing obviously correctable words and making sure autocomplete works as expected. TextInput is now a controlled input by default without causing any issues for most cases, so I removed the `controlled` prop. Fixes selection state jumping by restoring it after setting new text values, so highlighting the middle of some text in the new ReWrite example and hitting space will replace that selection with an underscore and keep the cursor at a sensible position as expected, instead of jumping to the end. Ads `maxLength` prop to support the most commonly needed syncronous behavior: preventing the user from typing too many characters. It can also be used to prevent users from continuing to type after entering special characters by changing it to the current length after a regex match. Made sure to verify it works well with pasted input (including in the middle of existing text), truncating it and collapsing the selection the same way it does on the web. Fixes bug in TextEventsExample where it wouldn't show the submit and end events, even though there were firing correctly. --- Examples/UIExplorer/TextInputExample.js | 65 +++++++-- Libraries/Components/TextInput/TextInput.js | 150 +++++++------------- Libraries/Text/RCTTextField.h | 4 +- Libraries/Text/RCTTextField.m | 72 ++++++++-- Libraries/Text/RCTTextFieldManager.m | 2 + Libraries/Text/RCTTextView.h | 2 + Libraries/Text/RCTTextView.m | 46 +++++- Libraries/Text/RCTTextViewManager.m | 2 + React/Base/RCTEventDispatcher.h | 6 +- React/Base/RCTEventDispatcher.m | 5 + 10 files changed, 222 insertions(+), 132 deletions(-) diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index 06cc12ee3..3369b41fb 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -33,7 +33,7 @@ var WithLabel = React.createClass({ {this.props.children} ); - } + }, }); var TextEventsExample = React.createClass({ @@ -41,13 +41,17 @@ var TextEventsExample = React.createClass({ return { curText: '', prevText: '', + prev2Text: '', }; }, updateText: function(text) { - this.setState({ - curText: text, - prevText: this.state.curText, + this.setState((state) => { + return { + curText: text, + prevText: state.curText, + prev2Text: state.prevText, + }; }); }, @@ -73,13 +77,43 @@ var TextEventsExample = React.createClass({ /> {this.state.curText}{'\n'} - (prev: {this.state.prevText}) + (prev: {this.state.prevText}){'\n'} + (prev2: {this.state.prev2Text}) ); } }); +class RewriteExample extends React.Component { + constructor(props) { + super(props); + this.state = {text: ''}; + } + render() { + var limit = 20; + var remainder = limit - this.state.text.length; + var remainderColor = remainder > 5 ? 'blue' : 'red'; + return ( + + { + text = text.replace(/ /g, '_'); + this.setState({text}); + }} + style={styles.default} + value={this.state.text} + /> + + {remainder} + + + ); + } +} + var styles = StyleSheet.create({ page: { paddingBottom: 300, @@ -125,12 +159,19 @@ var styles = StyleSheet.create({ flex: 1, }, label: { - width: 120, - justifyContent: 'flex-end', - flexDirection: 'row', + width: 115, + alignItems: 'flex-end', marginRight: 10, paddingTop: 2, }, + rewriteContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + remainder: { + textAlign: 'right', + width: 24, + }, }); exports.displayName = (undefined: ?string); @@ -143,6 +184,12 @@ exports.examples = [ return ; } }, + { + title: "Live Re-Write ( -> '_') + maxLength", + render: function() { + return ; + } + }, { title: 'Auto-capitalize', render: function() { @@ -276,7 +323,7 @@ exports.examples = [ }, { title: 'Event handling', - render: function(): ReactElement { return }, + render: function(): ReactElement { return ; }, }, { title: 'Colored input text', diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index cc1b00b41..d89291c37 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -31,8 +31,8 @@ var invariant = require('invariant'); var requireNativeComponent = require('requireNativeComponent'); var onlyMultiline = { - onSelectionChange: true, - onTextInput: true, + onSelectionChange: true, // not supported in Open Source yet + onTextInput: true, // not supported in Open Source yet children: true, }; @@ -64,10 +64,6 @@ var viewConfigAndroid = { var RCTTextView = requireNativeComponent('RCTTextView', null); var RCTTextField = requireNativeComponent('RCTTextField', null); -type DefaultProps = { - bufferDelay: number; -}; - type Event = Object; /** @@ -77,30 +73,29 @@ type Event = Object; * types, such as a numeric keypad. * * The simplest use case is to plop down a `TextInput` and subscribe to the - * `onChangeText` events to read the user input. There are also other events, such - * as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple + * `onChangeText` events to read the user input. There are also other events, + * such as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple * example: * * ``` - * * this.setState({input: text})} + * onChangeText={(text) => this.setState({text})} + * value={this.state.text} * /> - * {'user input: ' + this.state.input} - * * ``` * - * The `value` prop can be used to set the value of the input in order to make - * the state of the component clear, but does not behave as a true - * controlled component by default because all operations are asynchronous. - * Setting `value` once is like setting the default value, but you can change it - * continuously based on `onChangeText` events as well. If you really want to - * force the component to always revert to the value you are setting, you can - * set `controlled={true}`. + * Note that some props are only available with multiline={true/false}: * - * The `multiline` prop is not supported in all releases, and some props are - * multiline only. + * var onlyMultiline = { + * onSelectionChange: true, // not supported in Open Source yet + * onTextInput: true, // not supported in Open Source yet + * children: true, + * }; + * + * var notMultiline = { + * onSubmitEditing: true, + * }; */ var TextInput = React.createClass({ @@ -179,6 +174,11 @@ var TextInput = React.createClass({ 'done', 'emergency-call', ]), + /** + * Limits the maximum number of characters that can be entered. Use this + * instead of implementing the logic in JS to avoid flicker. + */ + maxLength: PropTypes.number, /** * If true, the keyboard disables the return key when there is no text and * automatically enables it when there is text. Default value is false. @@ -236,22 +236,15 @@ var TextInput = React.createClass({ */ selectionState: PropTypes.instanceOf(DocumentSelectionState), /** - * The default value for the text input + * The value to show for the text input. TextInput is a controlled + * component, which means the native value will be forced to match this + * value prop if provided. For most uses this works great, but in some + * cases this may cause flickering - one common cause is preventing edits + * by keeping value the same. In addition to simply setting the same value, + * either set `editable={false}`, or set/update `maxLength` to prevent + * unwanted edits without flicker. */ value: PropTypes.string, - /** - * This helps avoid drops characters due to race conditions between JS and - * the native text input. The default should be fine, but if you're - * potentially doing very slow operations on every keystroke then you may - * want to try increasing this. - */ - bufferDelay: PropTypes.number, - /** - * If you really want this to behave as a controlled component, you can set - * this true, but you will probably see flickering, dropped keystrokes, - * and/or laggy typing, depending on how you process onChange events. - */ - controlled: PropTypes.bool, /** * When the clear button should appear on the right side of the text view */ @@ -297,16 +290,9 @@ var TextInput = React.createClass({ React.findNodeHandle(this.refs.input); }, - getDefaultProps: function(): DefaultProps { - return { - bufferDelay: 100, - }; - }, - getInitialState: function() { return { - mostRecentEventCounter: 0, - bufferedValue: this.props.value, + mostRecentEventCount: 0, }; }, @@ -346,52 +332,6 @@ var TextInput = React.createClass({ } }, - _bufferTimeout: (undefined: ?number), - - componentWillReceiveProps: function(newProps: {value: any}) { - if (newProps.value !== this.props.value) { - if (!this.isFocused()) { - // Set the value immediately if the input is not focused since that - // means there is no risk of the user typing immediately. - this.setState({bufferedValue: newProps.value}); - } else { - // The following clear and setTimeout buffers the value such that if more - // characters are typed in quick succession, generating new values, the - // out of date values will get cancelled before they are ever sent to - // native. - // - // If we don't do this, it's likely the out of date values will blow - // away recently typed characters in the native input that JS was not - // yet aware of (since it is informed asynchronously), then the next - // character will be appended to the older value, dropping the - // characters in between. Here is a potential sequence of events - // (recall we have multiple independently serial, interleaved queues): - // - // 1) User types 'R' => send 'R' to JS queue. - // 2) User types 'e' => send 'Re' to JS queue. - // 3) JS processes 'R' and sends 'R' back to native. - // 4) Native recieves 'R' and changes input from 'Re' back to 'R'. - // 5) User types 'a' => send 'Ra' to JS queue. - // 6) JS processes 'Re' and sends 'Re' back to native. - // 7) Native recieves 'Re' and changes input from 'R' back to 'Re'. - // 8) JS processes 'Ra' and sends 'Ra' back to native. - // 9) Native recieves final 'Ra' from JS - 'e' has been dropped! - // - // This isn't 100% foolproop (e.g. if it takes longer than - // `props.bufferDelay` ms to process one keystroke), and there are of - // course other potential algorithms to deal with this, but this is a - // simple solution that seems to reduce the chance of dropped characters - // drastically without compromising native input responsiveness (e.g. by - // introducing delay from a synchronization protocol). - this.clearTimeout(this._bufferTimeout); - this._bufferTimeout = this.setTimeout( - () => this.setState({bufferedValue: newProps.value}), - this.props.bufferDelay - ); - } - } - }, - getChildContext: function(): Object { return {isInAParentText: true}; }, @@ -411,7 +351,7 @@ var TextInput = React.createClass({ _renderIOS: function() { var textContainer; - var props = Object.assign({},this.props); + var props = Object.assign({}, this.props); props.style = [styles.input, this.props.style]; if (!props.multiline) { @@ -430,7 +370,8 @@ var TextInput = React.createClass({ onBlur={this._onBlur} onChange={this._onChange} onSelectionChangeShouldSetResponder={() => true} - text={this.state.bufferedValue} + text={this.props.value} + mostRecentEventCount={this.state.mostRecentEventCount} />; } else { for (var propKey in notMultiline) { @@ -459,14 +400,14 @@ var TextInput = React.createClass({ ref="input" {...props} children={children} - mostRecentEventCounter={this.state.mostRecentEventCounter} + mostRecentEventCount={this.state.mostRecentEventCount} onFocus={this._onFocus} onBlur={this._onBlur} onChange={this._onChange} onSelectionChange={this._onSelectionChange} onTextInput={this._onTextInput} onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue} - text={this.state.bufferedValue} + text={this.props.value} />; } @@ -516,7 +457,7 @@ var TextInput = React.createClass({ password={this.props.password || this.props.secureTextEntry} placeholder={this.props.placeholder} placeholderTextColor={this.props.placeholderTextColor} - text={this.state.bufferedValue} + text={this.props.value} underlineColorAndroid={this.props.underlineColorAndroid} children={children} />; @@ -543,11 +484,20 @@ var TextInput = React.createClass({ }, _onChange: function(event: Event) { - if (this.props.controlled && event.nativeEvent.text !== this.props.value) { - this.refs.input.setNativeProps({text: this.props.value}); - } + var text = event.nativeEvent.text; + var eventCount = event.nativeEvent.eventCount; this.props.onChange && this.props.onChange(event); - this.props.onChangeText && this.props.onChangeText(event.nativeEvent.text); + this.props.onChangeText && this.props.onChangeText(text); + this.setState({mostRecentEventCount: eventCount}, () => { + // This is a controlled component, so make sure to force the native value + // to match. Most usage shouldn't need this, but if it does this will be + // more correct but might flicker a bit and/or cause the cursor to jump. + if (text !== this.props.value && typeof this.props.value === 'string') { + this.refs.input.setNativeProps({ + text: this.props.value, + }); + } + }); }, _onBlur: function(event: Event) { @@ -567,10 +517,6 @@ var TextInput = React.createClass({ _onTextInput: function(event: Event) { this.props.onTextInput && this.props.onTextInput(event); - var counter = event.nativeEvent.eventCounter; - if (counter > this.state.mostRecentEventCounter) { - this.setState({mostRecentEventCounter: counter}); - } }, }); diff --git a/Libraries/Text/RCTTextField.h b/Libraries/Text/RCTTextField.h index ef0a07887..0c8266d7e 100644 --- a/Libraries/Text/RCTTextField.h +++ b/Libraries/Text/RCTTextField.h @@ -11,13 +11,15 @@ @class RCTEventDispatcher; -@interface RCTTextField : UITextField +@interface RCTTextField : UITextField @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, assign) BOOL autoCorrect; @property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, strong) UIColor *placeholderTextColor; +@property (nonatomic, assign) NSInteger mostRecentEventCount; +@property (nonatomic, strong) NSNumber *maxLength; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index 46e9cc7a4..57e0499bd 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -19,6 +19,7 @@ RCTEventDispatcher *_eventDispatcher; NSMutableArray *_reactSubviews; BOOL _jsRequestingFirstResponder; + NSInteger _nativeEventCount; } - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher @@ -31,6 +32,7 @@ [self addTarget:self action:@selector(_textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; [self addTarget:self action:@selector(_textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; _reactSubviews = [[NSMutableArray alloc] init]; + self.delegate = self; } return self; } @@ -38,10 +40,40 @@ RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string +{ + if (_maxLength == nil || [string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return + return YES; + } + NSUInteger allowedLength = _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; + } else { + return YES; + } +} + - (void)setText:(NSString *)text { - if (![text isEqualToString:self.text]) { + NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; + if (eventLag == 0 && ![text isEqualToString:self.text]) { + UITextRange *selection = self.selectedTextRange; [super setText:text]; + self.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds + } else if (eventLag > RCTTextUpdateLagWarningThreshold) { + RCTLogWarn(@"Native TextInput(%@) is %ld events ahead of JS - try to make your JS faster.", self.text, (long)eventLag); } } @@ -122,17 +154,29 @@ static void RCTUpdatePlaceholder(RCTTextField *self) return self.autocorrectionType == UITextAutocorrectionTypeYes; } -#define RCT_TEXT_EVENT_HANDLER(delegateMethod, eventName) \ -- (void)delegateMethod \ -{ \ - [_eventDispatcher sendTextEventWithType:eventName \ - reactTag:self.reactTag \ - text:self.text]; \ +- (void)_textFieldDidChange +{ + _nativeEventCount++; + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange + reactTag:self.reactTag + text:self.text + eventCount:_nativeEventCount]; } -RCT_TEXT_EVENT_HANDLER(_textFieldDidChange, RCTTextEventTypeChange) -RCT_TEXT_EVENT_HANDLER(_textFieldEndEditing, RCTTextEventTypeEnd) -RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit) +- (void)_textFieldEndEditing +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd + reactTag:self.reactTag + text:self.text + eventCount:_nativeEventCount]; +} +- (void)_textFieldSubmitEditing +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit + reactTag:self.reactTag + text:self.text + eventCount:_nativeEventCount]; +} - (void)_textFieldBeginEditing { @@ -143,11 +187,10 @@ RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit) } [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag - text:self.text]; + text:self.text + eventCount:_nativeEventCount]; } -// TODO: we should support shouldChangeTextInRect (see UITextFieldDelegate) - - (BOOL)becomeFirstResponder { _jsRequestingFirstResponder = YES; @@ -163,7 +206,8 @@ RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit) { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur reactTag:self.reactTag - text:self.text]; + text:self.text + eventCount:_nativeEventCount]; } return result; } diff --git a/Libraries/Text/RCTTextFieldManager.m b/Libraries/Text/RCTTextFieldManager.m index cc71b39fa..723ec10f9 100644 --- a/Libraries/Text/RCTTextFieldManager.m +++ b/Libraries/Text/RCTTextFieldManager.m @@ -29,6 +29,7 @@ RCT_REMAP_VIEW_PROPERTY(editable, enabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) 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_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) @@ -56,6 +57,7 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextField) { view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName]; } +RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView { diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index 014e35315..c5012ec09 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -25,6 +25,8 @@ @property (nonatomic, strong) UIColor *textColor; @property (nonatomic, strong) UIColor *placeholderTextColor; @property (nonatomic, strong) UIFont *font; +@property (nonatomic, assign) NSInteger mostRecentEventCount; +@property (nonatomic, strong) NSNumber *maxLength; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index f32debd47..bbb9a6927 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -21,6 +21,7 @@ NSString *_placeholder; UITextView *_placeholderView; UITextView *_textView; + NSInteger _nativeEventCount; } - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher @@ -124,11 +125,41 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) return _textView.text; } +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + if (_maxLength == nil) { + return YES; + } + NSUInteger allowedLength = _maxLength.integerValue - textView.text.length + range.length; + if (text.length > allowedLength) { + if (text.length > 1) { + // Truncate the input string so the result is exactly maxLength + NSString *limitedString = [text substringToIndex:allowedLength]; + NSMutableString *newString = textView.text.mutableCopy; + [newString replaceCharactersInRange:range withString:limitedString]; + textView.text = newString; + // Collapse selection at end of insert to match normal paste behavior + UITextPosition *insertEnd = [textView positionFromPosition:textView.beginningOfDocument + offset:(range.location + allowedLength)]; + textView.selectedTextRange = [textView textRangeFromPosition:insertEnd toPosition:insertEnd]; + [self textViewDidChange:textView]; + } + return NO; + } else { + return YES; + } +} + - (void)setText:(NSString *)text { - if (![text isEqualToString:_textView.text]) { + NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; + if (eventLag == 0 && ![text isEqualToString:_textView.text]) { + UITextRange *selection = _textView.selectedTextRange; [_textView setText:text]; [self _setPlaceholderVisibility]; + _textView.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds + } else if (eventLag > RCTTextUpdateLagWarningThreshold) { + RCTLogWarn(@"Native TextInput(%@) is %ld events ahead of JS - try to make your JS faster.", self.text, (long)eventLag); } } @@ -170,15 +201,18 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag - text:textView.text]; + text:textView.text + eventCount:_nativeEventCount]; } - (void)textViewDidChange:(UITextView *)textView { [self _setPlaceholderVisibility]; + _nativeEventCount++; [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange reactTag:self.reactTag - text:textView.text]; + text:textView.text + eventCount:_nativeEventCount]; } @@ -186,7 +220,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag - text:textView.text]; + text:textView.text + eventCount:_nativeEventCount]; } - (BOOL)becomeFirstResponder @@ -204,7 +239,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) if (result) { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur reactTag:self.reactTag - text:_textView.text]; + text:_textView.text + eventCount:_nativeEventCount]; } return result; } diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m index 570a51115..f47a106bd 100644 --- a/Libraries/Text/RCTTextViewManager.m +++ b/Libraries/Text/RCTTextViewManager.m @@ -29,6 +29,7 @@ RCT_REMAP_VIEW_PROPERTY(editable, textView.editable, BOOL) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(text, NSString) +RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber) RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType) @@ -52,6 +53,7 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView) { view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName]; } +RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView { diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 5576df64f..ebd58e75e 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -28,6 +28,8 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { RCTScrollEventTypeEndAnimation, }; +extern const NSInteger RCTTextUpdateLagWarningThreshold; + @protocol RCTEvent @required @@ -76,12 +78,14 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { */ - (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body; + /** * Send a text input/focus event. */ - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag - text:(NSString *)text; + text:(NSString *)text + eventCount:(NSInteger)eventCount; - (void)sendEvent:(id)event; diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index ac0d1097b..7638ce99d 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -12,6 +12,8 @@ #import "RCTAssert.h" #import "RCTBridge.h" +const NSInteger RCTTextUpdateLagWarningThreshold = 3; + static NSNumber *RCTGetEventID(id event) { return @( @@ -113,6 +115,7 @@ RCT_EXPORT_MODULE() - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag text:(NSString *)text + eventCount:(NSInteger)eventCount { static NSString *events[] = { @"topFocus", @@ -124,8 +127,10 @@ RCT_EXPORT_MODULE() [self sendInputEventWithName:events[type] body:text ? @{ @"text": text, + @"eventCount": @(eventCount), @"target": reactTag } : @{ + @"eventCount": @(eventCount), @"target": reactTag }]; }