Add TextInput controlled selection prop on Android
Summary: Android PR for TextInput selection, based on the iOS implementation in #8958. ** Test plan ** Tested using the text selection example in UIExplorer. Closes https://github.com/facebook/react-native/pull/8962 Differential Revision: D3819285 Pulled By: andreicoman11 fbshipit-source-id: 9a2408af2a8b694258c88ab5c46322830c71452a
This commit is contained in:
parent
2ea65ec872
commit
3c1b69c1a9
|
@ -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 (
|
||||
<View>
|
||||
<TextInput
|
||||
multiline={this.props.multiline}
|
||||
onChangeText={(value) => 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}
|
||||
/>
|
||||
<View>
|
||||
<Text>
|
||||
selection = {JSON.stringify(this.state.selection)}
|
||||
</Text>
|
||||
<Text onPress={this.placeAt.bind(this, 0)}>
|
||||
Place at Start (0, 0)
|
||||
</Text>
|
||||
<Text onPress={this.placeAt.bind(this, length)}>
|
||||
Place at End ({length}, {length})
|
||||
</Text>
|
||||
<Text onPress={this.placeAtRandom.bind(this)}>
|
||||
Place at Random
|
||||
</Text>
|
||||
<Text onPress={this.select.bind(this, 0, length)}>
|
||||
Select All
|
||||
</Text>
|
||||
<Text onPress={this.selectRandom.bind(this)}>
|
||||
Select Random
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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'}]}
|
||||
/>
|
||||
<TextInput
|
||||
autoCorrect={true}
|
||||
placeholder="multiline, aligned center"
|
||||
placeholderTextColor="green"
|
||||
multiline={true}
|
||||
style={[styles.multiline, {textAlign: "center", textAlignVertical: "center"}]}
|
||||
style={[styles.multiline, {textAlign: 'center', textAlignVertical: 'center'}]}
|
||||
/>
|
||||
<TextInput
|
||||
autoCorrect={true}
|
||||
multiline={true}
|
||||
style={[styles.multiline, {color: 'blue'}, {textAlign: "right", textAlignVertical: "bottom"}]}>
|
||||
style={[styles.multiline, {color: 'blue'}, {textAlign: 'right', textAlignVertical: 'bottom'}]}>
|
||||
<Text style={styles.multiline}>multiline with children, aligned bottom-right</Text>
|
||||
</TextInput>
|
||||
</View>
|
||||
|
@ -623,4 +710,22 @@ exports.examples = [
|
|||
title: 'Toggle Default Padding',
|
||||
render: function(): ReactElement { return <ToggleDefaultPaddingExample />; },
|
||||
},
|
||||
{
|
||||
title: 'Text selection & cursor placement',
|
||||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<SelectionExample
|
||||
style={styles.default}
|
||||
value="text selection can be changed"
|
||||
/>
|
||||
<SelectionExample
|
||||
multiline
|
||||
style={styles.multiline}
|
||||
value={"multiline text selection\ncan also be changed"}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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 = <Text>{children}</Text>;
|
||||
}
|
||||
|
||||
if (props.selection && props.selection.end == null) {
|
||||
props.selection = {start: props.selection.start, end: props.selection.start};
|
||||
}
|
||||
|
||||
const textContainer =
|
||||
<AndroidTextInput
|
||||
ref={this._setNativeRef}
|
||||
|
|
|
@ -64,6 +64,7 @@ public class ReactEditText extends EditText {
|
|||
private int mDefaultGravityHorizontal;
|
||||
private int mDefaultGravityVertical;
|
||||
private int mNativeEventCount;
|
||||
private int mMostRecentEventCount;
|
||||
private @Nullable ArrayList<TextWatcher> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ReactEditText, Layout
|
|||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "selection")
|
||||
public void setSelection(ReactEditText view, @Nullable ReadableMap selection) {
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.hasKey("start") && selection.hasKey("end")) {
|
||||
view.setSelection(selection.getInt("start"), selection.getInt("end"));
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "onSelectionChange", defaultBoolean = false)
|
||||
public void setOnSelectionChange(final ReactEditText view, boolean onSelectionChange) {
|
||||
if (onSelectionChange) {
|
||||
|
|
|
@ -48,8 +48,8 @@ import com.facebook.react.uimanager.events.RCTEventEmitter;
|
|||
WritableMap eventData = Arguments.createMap();
|
||||
|
||||
WritableMap selectionData = Arguments.createMap();
|
||||
selectionData.putInt("start", mSelectionStart);
|
||||
selectionData.putInt("end", mSelectionEnd);
|
||||
selectionData.putInt("start", mSelectionStart);
|
||||
|
||||
eventData.putMap("selection", selectionData);
|
||||
return eventData;
|
||||
|
|
|
@ -331,7 +331,7 @@ public class ReactTextInputPropertyTest {
|
|||
buildStyles("textAlign", null, "textAlignVertical", null));
|
||||
assertThat(view.getGravity()).isEqualTo(defaultGravity);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMaxLength() {
|
||||
ReactEditText view = mManager.createViewInstance(mThemedContext);
|
||||
|
@ -340,4 +340,23 @@ public class ReactTextInputPropertyTest {
|
|||
mManager.setMaxLength(view, null);
|
||||
assertThat(view.getFilters()).isEqualTo(filters);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelection() {
|
||||
ReactEditText view = mManager.createViewInstance(mThemedContext);
|
||||
view.setText("Need some text to select something...");
|
||||
|
||||
mManager.updateProperties(view, buildStyles());
|
||||
assertThat(view.getSelectionStart()).isEqualTo(0);
|
||||
assertThat(view.getSelectionEnd()).isEqualTo(0);
|
||||
|
||||
JavaOnlyMap selection = JavaOnlyMap.of("start", 5, "end", 10);
|
||||
mManager.updateProperties(view, buildStyles("selection", selection));
|
||||
assertThat(view.getSelectionStart()).isEqualTo(5);
|
||||
assertThat(view.getSelectionEnd()).isEqualTo(10);
|
||||
|
||||
mManager.updateProperties(view, buildStyles("selection", null));
|
||||
assertThat(view.getSelectionStart()).isEqualTo(5);
|
||||
assertThat(view.getSelectionEnd()).isEqualTo(10);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue