11

Recently my version of chrome has been doing something strange (74.0.3729.131 on ubuntu 18.04) more and more often. I have a small editor script which has a textarea which displays code. The textarea has a fixed size and a vertical scroll bar. Beyond that nothing fancy.

Usually, when I insert a newline (normal behaviour of textarea), the scroll bar doesn't move. Now for some reason about 80% of the times it scrolls the textarea down till the position of the caret is at the top of the textarea. Strangely if I delete and enter the newline in the same position, it usually does not scroll.

I'm not sure if this is some new issue in Chrome. I usen't have this issue with previous versions with the identical editor.

Here is a codepen which demonstrates the issue, scroll to some line, press enter and the textarea should scroll down. Try this a few times to see the unpredictable behaviour (adding the code just to be able to add the link, as you can see it's just a textarea).

https://codepen.io/anon/pen/rgKqMb

<textarea style="width:90%;height:300px"></textarea>

The only solution that occurs to me to avoid this is to stop the normal behaviour of the enter key and add the newline to the text. Any other ideas/insights very much welcome.

  • Do you have that textarea script? I'd like to see it – Mauricio Cárdenas May 27 '19 at 16:43
  • @MauricioCárdenas see the codepen –  May 27 '19 at 17:06
  • There's no script there. It's just the standard textarea. "I have a small editor script which has a textarea which displays code. The textarea has a fixed size and a vertical scroll bar. Beyond that nothing fancy". I wanted to see that script lol – Mauricio Cárdenas May 27 '19 at 17:07
  • 1
    @MauricioCárdenas the bug doesn't involve any script, the problematic behaviour is present in this codepen –  May 27 '19 at 17:19
  • Try what I just posted. That isn't a bug, it's the "normal" behaviour. Most likely you'll need to "force" the scroll position to stay where it is. – Mauricio Cárdenas May 27 '19 at 17:24
  • 1
    I have the same issue. @user10275798 Did you find a solution? – Jason O. Jul 12 '20 at 15:08
  • Maybe someone can report it via https://support.google.com/chrome/answer/95315?hl=en&co=GENIE.Platform%3DDesktop (I'm using Vivaldi, and they say the behavior is expected, which is because Chrome shows the same behavior). – steffen Jul 26 '23 at 19:53

3 Answers3

6

It's almost the end of 2020, Chrome version 86 and this issue still exists? What's more, I am surprised I have not found more information (complaints) on this matter (this post is the only thing I've found which speaks of this issue specifically.) I have observed that this behavior occurs not only in typing, but pasting any text containing a newline. I also observed that if I execute an undo action after this occurs, another random scroll happens, taking me even farther up the page, and nowhere near where the caret is.

I experimented and examined this behavior at much length, and was not able to find any repeatable circumstances which might give a clue as to how to predict when this would occur. It truly just seems "random". Nonetheless, I've had to work around this issue for an NWJS editor app I'm creating (NWJS uses Chrome for UI.)

This is what seems to be working for me:

First all, let me start simple in order to introduce the principle. We attach an "input" listener and "scroll" listener to the textarea. This works because, from my observation anyway, the "input"[1] listener gets fired before the random scroll action occurs.

The scroll listener records each scrolling action and saves it in a global prevScrollPos. It also checks for a global flag scrollCorrection.

The "input" listener sets the scrollCorrection flag everytime text is input into the textarea. Remember, this has happened before the random scroll occurs.

So the next scroll to occur, which may be the nuisance random action, the scroll listener will clear scrollCorrection, then scroll the textarea to the previous scroll position, ie, scrolling it back to where it was before the "random" scroll. But the issue is unpredictable, what if there is no random scroll and the next scroll to occur is intentional? That is not a big deal. It just means that if the user scrolls manually, the first scroll event is basically nullified, but then after that (with scrollCorrection cleared) everything will scroll normally. Since during normal scrolling, events are spit out so rapidly, it is unlikely there will be any noticeable effect.

Here is the code:

let textarea;
let prevScrollPos = 0;
let scrollCorrection = false;

function onScroll(evt) {
  if (scrollCorrection) {
    // Reset this right off so it doesn't get retriggered by the corrction.
    scrollCorrection = false;
    textarea.scrollTop = prevScrollPos;
  }
  prevScrollPos = textarea.scrollTop;
}

function onInput(evt) {
  scrollCorrection = true;
}

window.addEventListener("load", () => {
  textarea = document.getElementById("example_textarea");
  textarea.addEventListener("scroll", onScroll);
  textarea.addEventListener("input", onInput);
})

Now let's expand on it:

There is another consideration. What if the typing or pasting action puts the end of the typed or pasted text (and thus the caret) outside the view of the textarea viewport? When normal scrolling is in play, most browsers will scroll the page[2] so the caret will remain in view. However now that we've taken over scrolling action, we'll need to implement that ourselves.

In the psuedo-code below, on input to the textarea, besides setting scrollCorrection, we call a function which will:

  1. determine the xy position of caret relative to textarea viewport
  2. determine if it is scrolled out of view
  3. if so:
  4. determine the amount to scroll to bring it in view
  5. determine if the random scroll has already occurred by testing the state of scrollCorrection
  6. if it hasn't, set flag scrollCorrection2 containing the amount to scroll
  7. if it has, explicitly do the additional scrolling to bring it back into view

Finding the xy position of the caret in a textarea is not a trivial matter and is outside the scope of this answer, but there are plenty of methods to be found in searching the web. Most involve replicating the textarea contents in a non-form element, eg div block, with similar font, font-size, text wrapping etc, then using getBoundingClientRect on the resultant containing block and such. In my situation, I was already doing most of this for my editor, so it wasn't much of an additional expense. But I've included some psuedo-code to show how this can be implemented in the scroll correction mechanism. setCaretCorrection basically does steps 1 - 7 above.

let textarea;
let prevScrollPos = 0;
let scrollCorrection = false;
let caretCorrection = 0;

function onScroll(evt) {
  if (scrollCorrection) {
    // Reset this right off so it doesn't get retriggered by the correction.
    scrollCorrection = false;
    textarea.scrollTop = prevScrollPos + caretCorrection;
    caretCorrection = 0;
  }

  prevScrollPos = textarea.scrollTop;
}

function onTextareaInput() {
  scrollCorrection = true;
  setCaretCorrection();
}

function setCaretCorrection(evt) {
  let caretPos = textarea.selectionStart;
  let scrollingNeeded;
  let amountToScroll;

  /* ... Some code to determine xy position of caret relative to
         textarea viewport, if it is scrolled out of view, and if
         so, how much to scroll to bring it in view. ... */

  if (scrollingNeeded) {
    if (scrollCorrection) {
      // scrollCorrection is true meaning random scroll has not occurred yet,
      // so flag the scroll listener to add additional correction. This method
      // won't cause a flicker which could happen if we scrollBy() explicitly.
      caretCorrection = amountToScroll;
    } else {
      // Random scroll has already occurred and been corrected, so we are
      // forced to do the additional "out of viewport" correction explicitly.
      // Note, in my situation I never saw this condition happen.
      textarea.scrollBy(0, amountToScroll);
    }
  }
}

One could go further and use the experimental event, "beforeinput"[3], to optimize this a little bit so fewer unnecessary calls to setCaretCorrection are made. If one examines event.data from "beforeinput" event, in certain cases it will report the data to be input. If it does not, then it outputs null. Unfortunately, when a newline is typed, event.data is null. However it will report newlines if they are pasted. So at least one can see if event.data contains a string, and if the string does not contain newlines, skip the whole correction action. (Also, see [1] below.)

[1] I also don't see any reason you couldn't do in the "beforeinput"[3] listener, what what we're doing in the "input" listener. That may also give more insurance that we set scrollCorrection before the random scroll occurs. Although note that "beforeinput" is experimental.

[2] I suspect it is broken implementation of this feature which is causing this issue.

[3] https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event (Available on Chrome and all major browsers except Firefox according to this link.)

KevinHJ
  • 1,014
  • 11
  • 24
1

You can try avoiding the events on the textarea with css and js, then force the scroll to it's current position:

css:

textarea { 
    overflow:auto; 
    resize:none; 
    width:90%; 
    height:300px; 
} 

js: You'll need to insert the first answer from this question at A

function preventMoving(e) {
    var key = (e.keyCode ? e.keyCode : e.which);
    if(key == 13) {
        e.preventDefault();
        // A
    }
}

Then on your HTML:

<textarea onkeyup="preventMoving(event);"></textarea>
  • 2
    (1) This relates to the page scroll not the textarea scroll as I noted is the issue (2) the unwanted scrolling behaviour occurs at the keydown event rather than keyup. (3) The bug does not look like normal behaviour given that it is unpredictable. –  May 27 '19 at 17:33
  • Give me a couple minutes and I'll update the answer, I'm a bit busy right now, but thanks for clearing that up. – Mauricio Cárdenas May 27 '19 at 17:35
  • PS: It's not unpredictable really, it is normal behaviour due to focus (apparently). I was able to constantly reproduce the issue in the codepen you posted. Anyhow, I'll look deeper into it in a couple minutes. – Mauricio Cárdenas May 27 '19 at 17:36
  • I don't know if you're seeing the same issue as here it doesn't happen all the time, and it happens on/off without changing focus on the textarea element. –  May 27 '19 at 17:47
  • Probably I'm seeing something different and that's why I'm understanding something different. What I'm able to see, is: whenever you scroll down within the textarea (And also the whole page), then you write something and press enter, the textarea automatically scrolls so whatever you just wrote ends up being on top of the textarea. – Mauricio Cárdenas May 27 '19 at 17:53
  • Ok, this is similar. But even if you don't type, just scroll down the lines and choose one, press enter, and then you get an unpredictable scroll behaviour. In any case, aside from preventing the default behaviour of the enter key and adding the newline manually I can't see any other solution. I don't think this particular scrolling behaviour can be isolated and stopped. –  May 27 '19 at 17:56
  • Try the solution to this question: https://stackoverflow.com/questions/11076975/insert-text-into-textarea-at-cursor-position-javascript – Mauricio Cárdenas May 27 '19 at 18:01
  • I'll edit my answer to include what you need to catch in order to insert that – Mauricio Cárdenas May 27 '19 at 18:03
0

i just run into this bug and decided to prevent the default 'Enter' behavior and emulate it in a function.

The function adds the line breaks to the value and adjusts the carrot position and finally Y scroll bar.

<html>
<head>
    <script>
function onEnter(id,evt) {
    if(evt.key!='Enter')return;
    evt.preventDefault();
    let s=id.selectionStart,e=id.selectionEnd,st=id.scrollTop,ch=id.clientHeight;
    let lh=17; /* Adjust the lh value to the line heigh in PX of your text area. */
    id.setRangeText('\n',s,e,'end');
    let p=id.value.slice(0,s+1).split('\n').length*lh
    if(p<st)id.scrollTop=p-2*lh;
    else if(p>st+ch)id.scrollTop=p-ch;
};
    </script>
</head>
<body>
    <textarea style="line-height:17px;" onkeydown="onEnter(this,event);"></textarea>
</body>
</html>

Cheers, Kai