vaadin / web-components

A set of high-quality standards based web components for enterprise web applications. Part of Vaadin 20+
https://vaadin.com/docs/latest/components
466 stars 83 forks source link

[text-area] Infinite ResizeObserver loop #7828

Open robrez opened 1 month ago

robrez commented 1 month ago

Description

In certain situations, text-area's resize observer causes an infinite loop

Expected outcome

Loop should be terminated

Minimal reproducible example

I've had some difficultly creating a very minimalist reproduction. Here is a gif and stackblitz

https://stackblitz.com/edit/vitejs-vite-wmooik?file=main.js

text-area-resize-loop

Steps to reproduce

Place text-area(s) in the dom, provide values, and slowly make the window / dom parent narrower until you see "jittery" behavior

Environment

Vaadin version(s): 23, 24 OS: windows

Browsers

Chrome

robrez commented 1 month ago

Potential general solutions:

  _onResize() {
    const width = this.getBoundingClientRect()?.width;
    const value = this.value;
    if (width === this.__prevWidth && value === this.__prevValue) {
      return;
    }
    this.__prevWidth = width;
    this.__prevValue = value;
    this._updateHeight();
    this.__scrollPositionUpdated();
  }

Memoize width and value onResize. Don't re-try resize whenever width and value match the previous effort. This would misbehave in a lot of circumstances, some examples:

After typing that, I realize checking the width of the input element would be better versus "this". The only other idea I have for a solution involves some sort of debouncing of resize observers or maybe saying, "ignore the next one" whenever we cause the resize ourselves

robrez commented 1 month ago

I think that doing some sort of "skipNextResize" is the better strategy.. probably still not perfect:

  _onResize() {
    if (this.__resizing) {
      return;
    }
    if (this.__skipNextResize) {
      this.__skipNextResize = false;
      return;
    }
    this.__resizing = true;
    this._updateHeight();
    this.__resizing = false;
    this.__scrollPositionUpdated();
  }

  _updateHeight() {
    // ...

    const inputHeight = input.scrollHeight;
    if (inputHeight > input.clientHeight) {
      // add these 3 lines.. not sure it really makes sense to have an "if"... we'd probably
      // want to just _always_ skip the next resize observer when we are the ones who caused it
      if (this.__resizing) {
          this.__skipNextResize = true;
      }
      input.style.height = `${inputHeight}px`;
    }

   // ...
 }