Mobile browser issue with textarea resize
Asked Answered
P

1

18

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>
Polder answered 31/1, 2019 at 0:14 Comment(12)
I have now added a more appropriate fiddle, and a reasonable bountyPolder
The jsfiddle you provided seems unnecessarily complicated. Here's a GitHub repo of mine, can you try this one and see if you also see the same bug or not? github.com/MartinDawson/react-fluid-textareaAmara
@danMad I have reproduced you code from JsFiddle in a React Context here and it seems to work without any weird behaviour. Can you confirm ?Chose
Cannot reproduce the problem anywaysHa
All works fine in my mobile, I guessIdell
Share your github repository.Announcer
Can you add more about your test identification browser?Tevere
it works fine on my mobile browserPaulitapaulk
It works fine in my mobile. deployed version - check hereMuley
@danMad as it seems to be working for everyone, I would suggest upgrading your other plugins, assessing what you can potentially remove, and also use the highest possible version of react. It's a funny one alright (although I couldn't reproduce it).. Maybe it's a sign that you deserve a new phone for all your hard work! :)Turbulent
Using Laura's Fiddle I also could not reproduce the bug, and had the textarea resizing with the content. But in a codepen I made, where I was just messing with a textarea element, I found a <div> with a contentEditable attribute was generally responding better than the <textarea>. But, of course, this it neither accessible nor semantic.Flurried
Since you mentioned Safari, I assume you checked it on your IOS device. Since all browsers on IOS actually work with the same "webkit" engine, you did not have 3 seperate failures, just one.Puffer
E
1

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.

Elizabetelizabeth answered 3/6, 2019 at 21:51 Comment(1)
I liked when you said: NEVER MIX REACT WITH DOM TOUCHING!!!Thwack

© 2022 - 2024 — McMap. All rights reserved.