From 481f560f64806ba3324cf722d6bf8c3f36ac74a5 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 25 Jan 2016 05:45:44 -0800 Subject: [PATCH] Added support for auto-resizing text fields Summary: public This diff adds support for auto-resizing multiline text fields. This has been a long-requested feature, with several native solutions having been proposed (see https://github.com/facebook/react-native/pull/1229 and D2846915). Rather than making this a feature of the native component, this diff simply exposes some extra information in the `onChange` event that makes it easy to implement this in pure JS code. I think this is preferable, since it's simpler, works cross-platform, and avoids any controversy about what the API should look like, or how the props should be named. It also makes it easier to implement custom min/max-height logic. Reviewed By: sahrens Differential Revision: D2849889 fb-gh-sync-id: d9ddf4ba4037d388dac0558aa467d958300aa691 --- .../UIExplorer/TextInputExample.android.js | 37 +++++++++++++++++++ Examples/UIExplorer/TextInputExample.ios.js | 37 +++++++++++++++++++ Libraries/Text/RCTTextView.m | 36 +++++++++++++++--- 3 files changed, 105 insertions(+), 5 deletions(-) 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