diff --git a/Examples/UIExplorer/js/TextInputExample.android.js b/Examples/UIExplorer/js/TextInputExample.android.js
index d04a32b3e..93ff38f3e 100644
--- a/Examples/UIExplorer/js/TextInputExample.android.js
+++ b/Examples/UIExplorer/js/TextInputExample.android.js
@@ -258,6 +258,93 @@ class ToggleDefaultPaddingExample extends React.Component {
}
}
+type SelectionExampleState = {
+ selection: {
+ start: number;
+ end: number;
+ };
+ value: string;
+};
+
+class SelectionExample extends React.Component {
+ state: SelectionExampleState;
+
+ _textInput: any;
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selection: {start: 0, end: 0},
+ value: props.value
+ };
+ }
+
+ onSelectionChange({nativeEvent: {selection}}) {
+ this.setState({selection});
+ }
+
+ getRandomPosition() {
+ var length = this.state.value.length;
+ return Math.round(Math.random() * length);
+ }
+
+ select(start, end) {
+ this._textInput.focus();
+ this.setState({selection: {start, end}});
+ }
+
+ selectRandom() {
+ var positions = [this.getRandomPosition(), this.getRandomPosition()].sort();
+ this.select(...positions);
+ }
+
+ placeAt(position) {
+ this.select(position, position);
+ }
+
+ placeAtRandom() {
+ this.placeAt(this.getRandomPosition());
+ }
+
+ render() {
+ var length = this.state.value.length;
+
+ return (
+
+ this.setState({value})}
+ onSelectionChange={this.onSelectionChange.bind(this)}
+ ref={textInput => (this._textInput = textInput)}
+ selection={this.state.selection}
+ style={this.props.style}
+ value={this.state.value}
+ />
+
+
+ selection = {JSON.stringify(this.state.selection)}
+
+
+ Place at Start (0, 0)
+
+
+ Place at End ({length}, {length})
+
+
+ Place at Random
+
+
+ Select All
+
+
+ Select Random
+
+
+
+ );
+ }
+}
+
var styles = StyleSheet.create({
multiline: {
height: 60,
@@ -499,19 +586,19 @@ exports.examples = [
placeholder="multiline, aligned top-left"
placeholderTextColor="red"
multiline={true}
- style={[styles.multiline, {textAlign: "left", textAlignVertical: "top"}]}
+ style={[styles.multiline, {textAlign: 'left', textAlignVertical: 'top'}]}
/>
+ style={[styles.multiline, {color: 'blue'}, {textAlign: 'right', textAlignVertical: 'bottom'}]}>
multiline with children, aligned bottom-right
@@ -623,4 +710,22 @@ exports.examples = [
title: 'Toggle Default Padding',
render: function(): ReactElement { return ; },
},
+ {
+ title: 'Text selection & cursor placement',
+ render: function() {
+ return (
+
+
+
+
+ );
+ }
+ },
];
diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js
index 1ac5f8941..08a79e51c 100644
--- a/Libraries/Components/TextInput/TextInput.js
+++ b/Libraries/Components/TextInput/TextInput.js
@@ -393,7 +393,6 @@ const TextInput = React.createClass({
/**
* The start and end of the text input's selection. Set start and end to
* the same value to position the cursor.
- * @platform ios
*/
selection: PropTypes.shape({
start: PropTypes.number.isRequired,
@@ -679,6 +678,10 @@ const TextInput = React.createClass({
children = {children};
}
+ if (props.selection && props.selection.end == null) {
+ props.selection = {start: props.selection.start, end: props.selection.start};
+ }
+
const textContainer =
mListeners;
private @Nullable TextWatcherDelegator mTextWatcherDelegator;
private int mStagedInputType;
@@ -86,6 +87,7 @@ public class ReactEditText extends EditText {
getGravity() & (Gravity.HORIZONTAL_GRAVITY_MASK | Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK);
mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
mNativeEventCount = 0;
+ mMostRecentEventCount = 0;
mIsSettingTextFromJS = false;
mIsJSSettingFocus = false;
mBlurOnSubmit = true;
@@ -182,6 +184,16 @@ public class ReactEditText extends EditText {
mContentSizeWatcher = contentSizeWatcher;
}
+ @Override
+ public void setSelection(int start, int end) {
+ // Skip setting the selection if the text wasn't set because of an out of date value.
+ if (mMostRecentEventCount < mNativeEventCount) {
+ return;
+ }
+
+ super.setSelection(start, end);
+ }
+
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
@@ -265,7 +277,8 @@ public class ReactEditText extends EditText {
// VisibleForTesting from {@link TextInputEventsTestCase}.
public void maybeSetText(ReactTextUpdate reactTextUpdate) {
// Only set the text if it is up to date.
- if (reactTextUpdate.getJsEventCounter() < mNativeEventCount) {
+ mMostRecentEventCount = reactTextUpdate.getJsEventCounter();
+ if (mMostRecentEventCount < mNativeEventCount) {
return;
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
index f1ad86b33..be3568be1 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
@@ -32,6 +32,7 @@ import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.BaseViewManager;
import com.facebook.react.uimanager.LayoutShadowNode;
@@ -230,6 +231,17 @@ public class ReactTextInputManager extends BaseViewManager