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 }]; }