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>
<div>
with acontentEditable
attribute was generally responding better than the <textarea>. But, of course, this it neither accessible nor semantic. – Flurried