diff --git a/Examples/UIExplorer/TextInputExample.android.js b/Examples/UIExplorer/TextInputExample.android.js index b3f5c6a4e..6e628f350 100644 --- a/Examples/UIExplorer/TextInputExample.android.js +++ b/Examples/UIExplorer/TextInputExample.android.js @@ -72,6 +72,29 @@ var TextEventsExample = React.createClass({ } }); +class AutoExpandingTextInput extends React.Component { + constructor(props) { + super(props); + this.state = {text: '', height: 0}; + } + render() { + return ( + { + this.setState({ + text: event.nativeEvent.text, + height: event.nativeEvent.contentSize.height, + }); + }} + style={[styles.default, {height: Math.max(35, this.state.height)}]} + value={this.state.text} + /> + ); + } +} + class RewriteExample extends React.Component { constructor(props) { super(props); @@ -385,6 +408,20 @@ exports.examples = [ ); } }, + { + title: 'Auto-expanding', + render: function() { + return ( + + + + ); + } + }, { title: 'Attributed text', render: function() { diff --git a/Examples/UIExplorer/TextInputExample.ios.js b/Examples/UIExplorer/TextInputExample.ios.js index 486cf411b..49079ff8b 100644 --- a/Examples/UIExplorer/TextInputExample.ios.js +++ b/Examples/UIExplorer/TextInputExample.ios.js @@ -96,6 +96,29 @@ var TextEventsExample = React.createClass({ } }); +class AutoExpandingTextInput extends React.Component { + constructor(props) { + super(props); + this.state = {text: '', height: 0}; + } + render() { + return ( + { + this.setState({ + text: event.nativeEvent.text, + height: event.nativeEvent.contentSize.height, + }); + }} + style={[styles.default, {height: Math.max(35, this.state.height)}]} + value={this.state.text} + /> + ); + } +} + class RewriteExample extends React.Component { constructor(props) { super(props); @@ -630,6 +653,20 @@ exports.examples = [ ); } }, + { + title: 'Auto-expanding', + render: function() { + return ( + + + + ); + } + }, { title: 'Attributed text', render: function() { diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index ed1acffde..b9c6a74b2 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -61,6 +61,8 @@ NSMutableArray *_subviews; BOOL _blockTextShouldChange; UITextRange *_previousSelectionRange; + NSUInteger _previousTextLength; + CGFloat _previousContentHeight; UIScrollView *_scrollView; } @@ -437,12 +439,36 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [self updateContentSize]; [self _setPlaceholderVisibility]; _nativeEventCount++; - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange - reactTag:self.reactTag - text:textView.text - key:nil - eventCount:_nativeEventCount]; + if (!self.reactTag) { + return; + } + + // When the context size increases, iOS updates the contentSize twice; once + // with a lower height, then again with the correct height. To prevent a + // spurious event from being sent, we track the previous, and only send the + // update event if it matches our expectation that greater text length + // should result in increased height. This assumption is, of course, not + // necessarily true because shorter text might include more linebreaks, but + // in practice this works well enough. + NSUInteger textLength = textView.text.length; + CGFloat contentHeight = textView.contentSize.height; + if (textLength >= _previousTextLength) { + contentHeight = MAX(contentHeight, _previousContentHeight); + } + _previousTextLength = textLength; + _previousContentHeight = contentHeight; + + NSDictionary *event = @{ + @"text": self.text, + @"contentSize": @{ + @"height": @(contentHeight), + @"width": @(textView.contentSize.width) + }, + @"target": self.reactTag, + @"eventCount": @(_nativeEventCount), + }; + [_eventDispatcher sendInputEventWithName:@"change" body:event]; } - (void)textViewDidEndEditing:(UITextView *)textView