I am working in React.js and have textarea
elements that dynamically expand and contract based on the size of the user's input. The intended functionality is as follows:
This works correctly in a desktop context. However, on any mobile or tablet in a modern browser (tested Safari, Chrome and Firefox) the textarea
element only expands, it does not contract when content is deleted.
At first I thought it might have something to do with the onChange
handler I was employing, however, the same issue remains when swapping it out with an onInput
handler. So I believe the issue resides in the resize()
method.
Does anyone have an idea of why I'm experiencing this issue?
I have created a style-free fiddle to share with you the basic functionality. Interestingly, the bug doesn't occur in the JSFiddle simulator on a mobile device, but if you take the same code and put it in another react environment, the bug occurs on a mobile device in modern browsers.
class Application extends React.Component {
render() {
return (
<div>
<Textarea value="This is a test" maxLength={500}/>
</div>
);
}
}
class Textarea extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value
? this.props.maxLength && this.props.maxLength > 0
? this.props.value.length < this.props.maxLength
? this.props.value
: this.props.value.substring(0, this.props.maxLength)
: this.props.value
: '',
remaining: this.props.value
? this.props.value.length < this.props.maxLength
? this.props.maxLength - this.props.value.length
: 0
: this.props.maxLength
};
this.textAreaRef = React.createRef();
this.textAreaHeight = null;
this.textAreaoffSetHeight = null;
}
componentDidMount() {
window.addEventListener('resize', this.resize);
this.resize();
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
handleChange = event => {
const target = event.target || event.srcElement;
this.setState({
value: target.value,
remaining: target.value
? target.value.length < this.props.maxLength
? this.props.maxLength - target.value.length
: 0
: this.props.maxLength
});
this.resize();
};
resize = () => {
const node = this.textAreaRef.current;
node.style.height = '';
const style = window.getComputedStyle(node, null);
let heightOffset =
parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
this.textAreaoffSetHeight = node.offsetTop;
this.textAreaHeight = node.scrollHeight + heightOffset;
node.style.height = this.textAreaHeight + 'px';
this.resizeBorder();
this.resizeParentNode();
};
resizeBorder = () => {
const textAreaSize = this.textAreaHeight;
const node = this.textAreaRef.current;
const borderNode = node.parentNode.querySelector(
'.textarea__border'
);
if (borderNode !== null) {
borderNode.style.top =
this.textAreaoffSetHeight + textAreaSize - 1 + 'px';
}
};
resizeParentNode = () => {
const node = this.textAreaRef.current;
const parentNode = node.parentNode;
if (parentNode !== null) {
parentNode.style.height = this.textAreaHeight + 40 + 'px';
}
};
render() {
return (
<div className={'textarea'}>
<textarea
ref={this.textAreaRef}
className={
!this.state.value
? 'textarea__input'
: 'textarea__input active'
}
value={this.state.value}
maxLength={
this.props.maxLength && this.props.maxLength > 0 ? this.props.maxLength : null
}
onChange={this.handleChange}
/>
<div className={'textarea__message'}>
{this.state.remaining <= 0
? `You've reached ${this.props.maxLength} characters`
: `${this.state.remaining} characters remaining`}
</div>
</div>
);
}
}
ReactDOM.render(
<Application />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main id="app">
<!-- This element's contents will be replaced with your component. -->
</main>
The issue is that you're modifying the DOM directly (or trying to) instead of modifying state and allowing React to flow properly. You modify the DOM elements properties in resize()
then any input change will immediate call handleChange(e)
and re-flow your DOM overwriting the modifications.
NEVER MIX REACT WITH DOM TOUCHING!!!
Change your resize
function to behave like your handleChange(e)
function and set variables within the state which control those properties during the render()
of the mark-up.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With