Last active
July 31, 2018 04:07
-
-
Save catchin/47afe706256604959c13dc25e7bb9383 to your computer and use it in GitHub Desktop.
This is a workaround for the buggy react-native TextInput multiline on Android. Inspired by the comments on https://github.com/facebook/react-native/issues/12717.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {PropTypes, PureComponent} from 'react'; | |
import {TextInput} from 'react-native'; | |
import debounce from 'debounce'; | |
/** | |
* This is a workaround for the buggy react-native TextInput multiline on Android. | |
* | |
* Can be removed once https://github.com/facebook/react-native/issues/12717 | |
* is fixed. | |
* | |
* Example for usage: | |
* <MultilineTextInput value={this.state.text} onChangeText={text => setState({text})} /> | |
*/ | |
export default class MultilineTextInput extends PureComponent { | |
constructor(props) { | |
super(props); | |
this.state = {selection: {start: 0, end: 0}}; | |
// Prevent 2 newlines for some Android versions, because they dispatch onSubmitEditing twice | |
this.onSubmitEditing = debounce(this.onSubmitEditing.bind(this), 100, true); | |
} | |
onSubmitEditing() { | |
const {selection} = this.state; | |
const {value} = this.props; | |
const newText = `${value.slice(0, selection.start)}\n${value.slice(selection.end)}`; | |
// move cursor only for this case, because in other cases a change of the selection is not allowed by Android | |
if (selection.start !== this.props.value.length && selection.start === selection.end) { | |
this.setState({ | |
selection: { | |
start: selection.start + 1, | |
end: selection.end + 1, | |
}, | |
}); | |
} | |
this.props.onChangeText(newText); | |
} | |
render() { | |
return ( | |
<TextInput | |
multiline | |
blurOnSubmit={false} | |
selection={this.state.selection} | |
value={this.props.value} | |
onSelectionChange={event => this.setState({selection: event.nativeEvent.selection})} | |
onChangeText={this.props.onChangeText} | |
onSubmitEditing={this.onSubmitEditing} | |
{...this.props} | |
/> | |
); | |
} | |
} | |
MultilineTextInput.propTypes = { | |
value: PropTypes.string.isRequired, | |
onChangeText: PropTypes.func.isRequired, | |
}; | |
////////////////// Tests ////////////////// | |
import 'react-native'; | |
// Require after react-native | |
import renderer from 'react-test-renderer'; | |
import React from 'react'; | |
import {shallow} from 'enzyme'; | |
import MultilineTextInput from '../MultilineTextInput'; | |
describe('MultilineTextInput', () => { | |
const text = 'some value'; | |
const onChangeText = jest.fn(); | |
const defaultProps = { | |
value: text, | |
onChangeText, | |
}; | |
it('renders correctly', () => { | |
const tree = renderer.create(<MultilineTextInput {...defaultProps} />).toJSON(); | |
expect(tree).toMatchSnapshot(); | |
}); | |
it('inserts a new line at the end if the "enter" soft key is pressed', () => { | |
const component = shallow(<MultilineTextInput {...defaultProps} />); | |
component.simulate('selectionChange', {nativeEvent: {selection: {start: text.length, end: text.length}}}); | |
component.simulate('submitEditing'); | |
expect(onChangeText).toBeCalledWith('some value\n'); | |
}); | |
it('inserts a new line in the middle if the cursor is in the middle and the "enter" soft key is pressed', () => { | |
const component = shallow(<MultilineTextInput {...defaultProps} />); | |
component.simulate('selectionChange', {nativeEvent: {selection: {start: 5, end: 5}}}); | |
component.simulate('submitEditing'); | |
expect(onChangeText).toBeCalledWith('some \nvalue'); | |
}); | |
it('inserts a new line in the middle if text is selected and the "enter" soft key is pressed', () => { | |
const component = shallow(<MultilineTextInput {...defaultProps} />); | |
component.simulate('selectionChange', {nativeEvent: {selection: {start: 4, end: 7}}}); | |
component.simulate('submitEditing'); | |
expect(onChangeText).toBeCalledWith('some\nlue'); | |
}); | |
}); |
Hey, thanks for this work!
I ran into a little issue with it when I'm using it while handling selection changes myself.
To fix those I had to add:
componentWillReceiveProps(props) {
if (props.selection) {
this.setState({ selection: props.selection });
}
}
focus() {
this.refs.text.focus();
}
And change onSubmitEditting to:
onSubmitEditing() {
const { selection } = this.state;
const { value } = this.props;
const newText = `${value.slice(0, selection.start)}\n${value.slice(selection.end)}`;
this.props.onChangeText(newText);
// move cursor only for this case, because in other cases a change of the selection is not allowed by Android
if (selection.start !== this.props.value.length && selection.start === selection.end) {
const newSelection = {
selection: {
start: selection.start + 1,
end: selection.end + 1,
},
};
if (this.props.onSelectionChange) {
this.props.onSelectionChange({ nativeEvent: newSelection });
} else {
this.setState(newSelection);
}
}
}
Essentially, I'm allowing the container to manage the selection state themselves.
Here's the updated code
https://gist.github.com/codewithpassion/d2d1a3e4d94c547e6b343ea5e9fe9845
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
We use this greate component in our app.
Thanks @catchin !