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