Skip to content

Implement TextInput onContentSizeChange #8457

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions Examples/UIExplorer/TextInputExample.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,21 @@ var TextEventsExample = React.createClass({
class AutoExpandingTextInput extends React.Component {
constructor(props) {
super(props);
this.state = {text: '', height: 0};
this.state = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should FlowType State type be added for this?

text: 'React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and React. The focus of React Native is on developer efficiency across all the platforms you care about — learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native.',
height: 0,
};
}
render() {
return (
<TextInput
{...this.props}
multiline={true}
onChange={(event) => {
this.setState({
text: event.nativeEvent.text,
height: event.nativeEvent.contentSize.height,
});
onContentSizeChange={(event) => {
this.setState({height: event.nativeEvent.contentSize.height});
}}
onChangeText={(text) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe leave this until we've implemented onTextChange?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onChangeText already works, I just mentioned that we should probably rename it for consistency. Unless you mean something else?

this.setState({text});
}}
style={[styles.default, {height: Math.max(35, this.state.height)}]}
value={this.state.text}
Expand Down Expand Up @@ -412,19 +415,19 @@ exports.examples = [
render: function() {
return (
<View>
<TextInput
<TextInput
style={[styles.singleLine, {fontFamily: 'sans-serif'}]}
placeholder="Custom fonts like Sans-Serif are supported"
/>
<TextInput
<TextInput
style={[styles.singleLine, {fontFamily: 'sans-serif', fontWeight: 'bold'}]}
placeholder="Sans-Serif bold"
/>
<TextInput
<TextInput
style={[styles.singleLine, {fontFamily: 'sans-serif', fontStyle: 'italic'}]}
placeholder="Sans-Serif italic"
/>
<TextInput
<TextInput
style={[styles.singleLine, {fontFamily: 'serif'}]}
placeholder="Serif"
/>
Expand Down
15 changes: 9 additions & 6 deletions Examples/UIExplorer/TextInputExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,21 @@ class AutoExpandingTextInput extends React.Component {

constructor(props) {
super(props);
this.state = {text: '', height: 0};
this.state = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here about FlowType: Should FlowType State type be added for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not needed as flow can infer the types from the object literal.

text: 'React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and React. The focus of React Native is on developer efficiency across all the platforms you care about — learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native.',
height: 0,
};
}
render() {
return (
<TextInput
{...this.props}
multiline={true}
onChange={(event) => {
this.setState({
text: event.nativeEvent.text,
height: event.nativeEvent.contentSize.height,
});
onChangeText={(text) => {
this.setState({text});
}}
onContentSizeChange={(event) => {
this.setState({height: event.nativeEvent.contentSize.height});
}}
style={[styles.default, {height: Math.max(35, this.state.height)}]}
value={this.state.text}
Expand Down
12 changes: 11 additions & 1 deletion Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ const TextInput = React.createClass({
* Sets the return key to the label. Use it instead of `returnKeyType`.
* @platform android
*/
returnKeyLabel: PropTypes.string,
returnKeyLabel: PropTypes.string,
/**
* Limits the maximum number of characters that can be entered. Use this
* instead of implementing the logic in JS to avoid flicker.
Expand Down Expand Up @@ -310,6 +310,14 @@ const TextInput = React.createClass({
* Changed text is passed as an argument to the callback handler.
*/
onChangeText: PropTypes.func,
/**
* Callback that is called when the text input's content size changes.
* This will be called with
* `{ nativeEvent: { contentSize: { width, height } } }`.
*
* Only called for multiline text inputs.
*/
onContentSizeChange: PropTypes.func,
/**
* Callback that is called when text input ends.
*/
Expand Down Expand Up @@ -566,6 +574,7 @@ const TextInput = React.createClass({
onFocus={this._onFocus}
onBlur={this._onBlur}
onChange={this._onChange}
onContentSizeChange={this.props.onContentSizeChange}
onSelectionChange={onSelectionChange}
onTextInput={this._onTextInput}
onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue}
Expand Down Expand Up @@ -626,6 +635,7 @@ const TextInput = React.createClass({
onFocus={this._onFocus}
onBlur={this._onBlur}
onChange={this._onChange}
onContentSizeChange={this.props.onContentSizeChange}
onSelectionChange={onSelectionChange}
onTextInput={this._onTextInput}
onEndEditing={this.props.onEndEditing}
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/RCTTextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
@property (nonatomic, strong) NSNumber *maxLength;

@property (nonatomic, copy) RCTDirectEventBlock onChange;
@property (nonatomic, copy) RCTDirectEventBlock onContentSizeChange;
@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;
@property (nonatomic, copy) RCTDirectEventBlock onTextInput;

Expand Down
19 changes: 19 additions & 0 deletions Libraries/Text/RCTTextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ @implementation RCTTextView
BOOL _blockTextShouldChange;
BOOL _nativeUpdatesInFlight;
NSInteger _nativeEventCount;

CGSize _previousContentSize;
BOOL _viewDidCompleteInitialLayout;
}

- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
Expand Down Expand Up @@ -261,6 +264,17 @@ - (void)updateContentSize
size.height = [_textView sizeThatFits:size].height;
_scrollView.contentSize = size;
_textView.frame = (CGRect){CGPointZero, size};

if (_viewDidCompleteInitialLayout && _onContentSizeChange && !CGSizeEqualToSize(_previousContentSize, size)) {
_previousContentSize = size;
_onContentSizeChange(@{
@"contentSize": @{
@"height": @(size.height),
@"width": @(size.width),
},
@"target": self.reactTag,
});
}
}

- (void)updatePlaceholder
Expand Down Expand Up @@ -633,6 +647,11 @@ - (BOOL)resignFirstResponder
- (void)layoutSubviews
{
[super layoutSubviews];

// Start sending content size updates only after the view has been laid out
// otherwise we send multiple events with bad dimensions on initial render.
_viewDidCompleteInitialLayout = YES;

[self updateFrames];
}

Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/RCTTextViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ - (UIView *)view
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, textView.keyboardAppearance, UIKeyboardAppearance)
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onContentSizeChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@

/* package */ static Map getDirectEventTypeConstants() {
return MapBuilder.builder()
.put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange"))
.put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart"))
.put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish"))
.put("topLoadingError", MapBuilder.of("registrationName", "onLoadingError"))
.put("topContentSizeChange", MapBuilder.of("registrationName", "onContentSizeChange"))
.put("topLayout", MapBuilder.of("registrationName", "onLayout"))
.put("topLoadingError", MapBuilder.of("registrationName", "onLoadingError"))
.put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish"))
.put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart"))
.put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange"))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ public void scrollTo(
Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.builder()
.put(ScrollEventType.SCROLL.getJSEventName(), MapBuilder.of("registrationName", "onScroll"))
.put(
ContentSizeChangeEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onContentSizeChange"))
.build();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't get rid of this. We use this internally in apps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event was moved here https://github.com/facebook/react-native/pull/8457/files#diff-0404c7801b7b68d8a58a11f338c5d666R74 since onContentSizeChange is now used by scollview and textinput. Is there a difference between doing it in the manager vs in uimanager that I'm not aware of?

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

package com.facebook.react.views.textinput;

public interface ContentSizeWatcher {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coding convention is Listener instead of Watcher I think

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could rename both to ...Listener as this is what is used usually on android.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the name is probably based on android.text.TextWatcher so we can keep it this way.

public void onLayout();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

package com.facebook.react.views.textinput;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;

/**
* Event emitted by EditText native view when content size changes.
*/
public class ReactContentSizeChangedEvent extends Event<ReactTextChangedEvent> {

public static final String EVENT_NAME = "topContentSizeChange";

private int mContentWidth;
private int mContentHeight;

public ReactContentSizeChangedEvent(
int viewId,
long timestampMs,
int contentSizeWidth,
int contentSizeHeight) {
super(viewId, timestampMs);
mContentWidth = contentSizeWidth;
mContentHeight = contentSizeHeight;
}

@Override
public String getEventName() {
return EVENT_NAME;
}

@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
}

private WritableMap serializeEventData() {
WritableMap eventData = Arguments.createMap();

WritableMap contentSize = Arguments.createMap();
contentSize.putDouble("width", mContentWidth);
contentSize.putDouble("height", mContentHeight);
eventData.putMap("contentSize", contentSize);

eventData.putInt("target", getViewTag());
return eventData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public class ReactEditText extends EditText {
private boolean mContainsImages;
private boolean mBlurOnSubmit;
private @Nullable SelectionWatcher mSelectionWatcher;
private @Nullable ContentSizeWatcher mContentSizeWatcher;
private final InternalKeyListener mKeyListener;

private static final KeyListener sKeyListener = QwertyKeyListener.getInstanceForFullKeyboard();
Expand Down Expand Up @@ -102,15 +103,30 @@ public ReactEditText(Context context) {
// TODO: t6408636 verify if we should schedule a layout after a View does a requestLayout()
@Override
public boolean isLayoutRequested() {
return false;
// If we are watching and updating container height based on content size
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you return true here, in the case of single-line text, the cursor will go out of view after you've typed after the end of the visible line. That means that onContentSizeChange should be limited to only work with multiline texts.

// then we don't want to scroll right away. This isn't perfect -- you might
// want to limit the height the text input can grow to. Possible solution
// is to add another prop that determines whether we should scroll to end
// of text.
if (mContentSizeWatcher != null) {
return isMultiline();
} else {
return false;
}
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (mContentSizeWatcher != null) {
mContentSizeWatcher.onLayout();
}
}

// Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't
// since we only allow JS to change focus, which in turn causes TextView to crash.
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER &&
((getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0 )) {
if (keyCode == KeyEvent.KEYCODE_ENTER && !isMultiline()) {
hideSoftKeyboard();
return true;
}
Expand Down Expand Up @@ -162,6 +178,10 @@ public void removeTextChangedListener(TextWatcher watcher) {
}
}

public void setContentSizeWatcher(ContentSizeWatcher contentSizeWatcher) {
mContentSizeWatcher = contentSizeWatcher;
}

@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
Expand Down Expand Up @@ -212,7 +232,7 @@ public void setInputType(int type) {
mStagedInputType = type;
// Input type password defaults to monospace font, so we need to re-apply the font
super.setTypeface(tf);

// We override the KeyListener so that all keys on the soft input keyboard as well as hardware
// keyboards work. Some KeyListeners like DigitsKeyListener will display the keyboard but not
// accept all input from it
Expand Down Expand Up @@ -329,6 +349,10 @@ private TextWatcherDelegator getTextWatcherDelegator() {
return mTextWatcherDelegator;
}

private boolean isMultiline() {
return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
}

/* package */ void setGravityHorizontal(int gravityHorizontal) {
if (gravityHorizontal == 0) {
gravityHorizontal = mDefaultGravityHorizontal;
Expand Down Expand Up @@ -447,7 +471,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
@Override
public void afterTextChanged(Editable s) {
if (!mIsSettingTextFromJS && mListeners != null) {
for (android.text.TextWatcher listener : mListeners) {
for (TextWatcher listener : mListeners) {
listener.afterTextChanged(s);
}
}
Expand Down
Loading