Skip to content

Instantly share code, notes, and snippets.

@catchin
Last active July 31, 2018 04:07
Show Gist options
  • Save catchin/47afe706256604959c13dc25e7bb9383 to your computer and use it in GitHub Desktop.
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.
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');
});
});
@codewithpassion
Copy link

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.

@codewithpassion
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment