Skip to content

Commit 97cbb91

Browse files
nicklockwoodjaysoo
authored andcommitted
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 facebook#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
1 parent 85d4630 commit 97cbb91

File tree

3 files changed

+105
-5
lines changed

3 files changed

+105
-5
lines changed

Examples/UIExplorer/TextInputExample.android.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,29 @@ var TextEventsExample = React.createClass({
7272
}
7373
});
7474

75+
class AutoExpandingTextInput extends React.Component {
76+
constructor(props) {
77+
super(props);
78+
this.state = {text: '', height: 0};
79+
}
80+
render() {
81+
return (
82+
<TextInput
83+
{...this.props}
84+
multiline={true}
85+
onChange={(event) => {
86+
this.setState({
87+
text: event.nativeEvent.text,
88+
height: event.nativeEvent.contentSize.height,
89+
});
90+
}}
91+
style={[styles.default, {height: Math.max(35, this.state.height)}]}
92+
value={this.state.text}
93+
/>
94+
);
95+
}
96+
}
97+
7598
class RewriteExample extends React.Component {
7699
constructor(props) {
77100
super(props);
@@ -385,6 +408,20 @@ exports.examples = [
385408
);
386409
}
387410
},
411+
{
412+
title: 'Auto-expanding',
413+
render: function() {
414+
return (
415+
<View>
416+
<AutoExpandingTextInput
417+
placeholder="height increases with content"
418+
enablesReturnKeyAutomatically={true}
419+
returnKeyType="done"
420+
/>
421+
</View>
422+
);
423+
}
424+
},
388425
{
389426
title: 'Attributed text',
390427
render: function() {

Examples/UIExplorer/TextInputExample.ios.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ var TextEventsExample = React.createClass({
9696
}
9797
});
9898

99+
class AutoExpandingTextInput extends React.Component {
100+
constructor(props) {
101+
super(props);
102+
this.state = {text: '', height: 0};
103+
}
104+
render() {
105+
return (
106+
<TextInput
107+
{...this.props}
108+
multiline={true}
109+
onChange={(event) => {
110+
this.setState({
111+
text: event.nativeEvent.text,
112+
height: event.nativeEvent.contentSize.height,
113+
});
114+
}}
115+
style={[styles.default, {height: Math.max(35, this.state.height)}]}
116+
value={this.state.text}
117+
/>
118+
);
119+
}
120+
}
121+
99122
class RewriteExample extends React.Component {
100123
constructor(props) {
101124
super(props);
@@ -630,6 +653,20 @@ exports.examples = [
630653
);
631654
}
632655
},
656+
{
657+
title: 'Auto-expanding',
658+
render: function() {
659+
return (
660+
<View>
661+
<AutoExpandingTextInput
662+
placeholder="height increases with content"
663+
enablesReturnKeyAutomatically={true}
664+
returnKeyType="done"
665+
/>
666+
</View>
667+
);
668+
}
669+
},
633670
{
634671
title: 'Attributed text',
635672
render: function() {

Libraries/Text/RCTTextView.m

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ @implementation RCTTextView
6161
NSMutableArray<UIView *> *_subviews;
6262
BOOL _blockTextShouldChange;
6363
UITextRange *_previousSelectionRange;
64+
NSUInteger _previousTextLength;
65+
CGFloat _previousContentHeight;
6466
UIScrollView *_scrollView;
6567
}
6668

@@ -437,12 +439,36 @@ - (void)textViewDidChange:(UITextView *)textView
437439
[self updateContentSize];
438440
[self _setPlaceholderVisibility];
439441
_nativeEventCount++;
440-
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
441-
reactTag:self.reactTag
442-
text:textView.text
443-
key:nil
444-
eventCount:_nativeEventCount];
445442

443+
if (!self.reactTag) {
444+
return;
445+
}
446+
447+
// When the context size increases, iOS updates the contentSize twice; once
448+
// with a lower height, then again with the correct height. To prevent a
449+
// spurious event from being sent, we track the previous, and only send the
450+
// update event if it matches our expectation that greater text length
451+
// should result in increased height. This assumption is, of course, not
452+
// necessarily true because shorter text might include more linebreaks, but
453+
// in practice this works well enough.
454+
NSUInteger textLength = textView.text.length;
455+
CGFloat contentHeight = textView.contentSize.height;
456+
if (textLength >= _previousTextLength) {
457+
contentHeight = MAX(contentHeight, _previousContentHeight);
458+
}
459+
_previousTextLength = textLength;
460+
_previousContentHeight = contentHeight;
461+
462+
NSDictionary *event = @{
463+
@"text": self.text,
464+
@"contentSize": @{
465+
@"height": @(contentHeight),
466+
@"width": @(textView.contentSize.width)
467+
},
468+
@"target": self.reactTag,
469+
@"eventCount": @(_nativeEventCount),
470+
};
471+
[_eventDispatcher sendInputEventWithName:@"change" body:event];
446472
}
447473

448474
- (void)textViewDidEndEditing:(UITextView *)textView

0 commit comments

Comments
 (0)