Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mobile browser issue with textarea resize

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:

Working correctly in a desktop context

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>
like image 689
DanMad Avatar asked Jan 31 '19 00:01

DanMad


1 Answers

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.

like image 200
Michael Angstadt Avatar answered Nov 04 '22 23:11

Michael Angstadt