How to make textarea scrollHeight take into account line wrapping caused by external scrollbar after height reset

Viewed 61

Please see edit for a better wording of the real issue.
I'm creating a text-area that auto-adapts its height to the content, but it breaks when a scrollbar appears on the page (not caused by the textarea itself, there can't even be a scrollbar on the textarea as I have overflow-y: hidden; on the second example. Javascript detect scrollbar in textarea is not related to my issue).

Below is a snippet that shows what I mean. Expand the lower textarea downwards and observe the upper textarea. When the scrollbar appears on the right side, it shrinks the upper textarea a bit, and if you have the same screen width than me, it creates a new line in the textarea. For the example to work, a word has to be close to the right and wrap when the scrollbar appears and the last line has to be "full" and wrap so that it wraps on a new line.

<textarea style="font-size: 25px;width: 100%;min-height: 100px">
Lorem ipsum dolor sit amet, consetetur aaaaa sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna </textarea>
<br>
<textarea placeholder="Expand me"></textarea>

I want to somehow detect that this happens and use it to compensate by giving it more height.

Below is the full context of what I'm trying to do:

let textareas = document.getElementsByClassName("auto-resize-textarea");
// Loop through textareas and add event listeners as well as other needed css attributes
for (const textarea of textareas) {
    // Initially set height as otherwise the textarea is not high enough on load
    textarea.style.height = textarea.scrollHeight.toString();
    // Hide scrollbar
    textarea.style.overflowY = 'hidden';
    // Call resize function with "this" context once during initialisation as it's too high otherwise
    resizeTextarea.call(textarea);
    // Add event listener to resize textarea on input
    textarea.addEventListener('input', resizeTextarea, false);
    // Also resize textarea on window resize event binding textarea to be "this"
    window.addEventListener('resize', resizeTextarea.bind(textarea), false);
}
function resizeTextarea() {
    // Textareas have default 2px padding and if not set it returns 0px
    let padding = window.getComputedStyle(this).getPropertyValue('padding-bottom');
    // getPropertyValue('padding-bottom') returns "px" at the end it needs to be removed to be added to scrollHeight
    padding = parseInt(padding.replace('px',''));
    this.style.height = "auto";
    this.style.height = (this.scrollHeight + padding) + "px";
}
// Source: https://stackoverflow.com/a/25621277/9013718
<textarea style="font-size: 25px;width: 100%;" class="auto-resize-textarea">
Lorem ipsum dolor sit amet, consetetur aaaaa sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna </textarea>
<br>
<textarea placeholder="Expand me"></textarea>

Because I'm not sure if you can reproduce the examples like I want to show them, I made a screen recording:

At first, I show that on normal window resize, the textarea adapts its height to fit the content. Then, when writing and creating a new line, the textarea also grows its height. But if the scrollbar causes the line wrap it doesn't detect it as "window resize" and thus the textarea doesn't grow, and the bottom text disappears below.

illustration

What do I have to change in the above JS function for it to account for the extra needed space for line breaks caused by the scrollbar?

Edit - ResizeObserver works but doesn't fix my issue

Thanks a lot @Ouroborus for the suggestion. I implemented an observer for the textarea, and it does detect the width change when a scrollbar appears; but the scrollHeight is still not correct.
Here is the code with the observer calling resizeTextarea() when it observes a change.

let textareas = document.getElementsByClassName("auto-resize-textarea");
// Init observer to call resizeTextarea when the dimensions of the textareas change
let observer = new ResizeObserver(entries => {
    for (let entry of entries) {
        let element = entry.target;
        const cr = entry.contentRect;
        console.log(`Size change observed. New width: ${cr.width}px height: ${cr.height}px`);
        resizeTextarea.call(element);
    }
});

// Loop through textareas and add event listeners as well as other needed css attributes
for (const textarea of textareas) {
    // Initially set height as otherwise the textarea is not high enough on load
    textarea.style.height = textarea.scrollHeight.toString();
    // Hide scrollbar
    textarea.style.overflowY = 'hidden';
    
    // Call resize function with "this" context once during initialisation as it's too high otherwise
    resizeTextarea.call(textarea);
    
    // Add event listener to resize textarea on input
    textarea.addEventListener('input', resizeTextarea, false);
    
    // Also resize textarea on window resize event binding textarea to be "this"
    observer.observe(textarea);
}

function resizeTextarea() {
    // Textareas have default 2px padding and if not set it returns 0px
    let padding = window.getComputedStyle(this).getPropertyValue('padding-bottom');
    // getPropertyValue('padding-bottom') returns "px" at the end it needs to be removed to be added to scrollHeight
    padding = parseInt(padding.replace('px',''));
    
    // Reset textarea height to (supposedly) have correct scrollHeight afterwards 
    const scrollHeightBefore = this.scrollHeight;
    this.style.height = "auto";
    console.log('scrollHeight before height reset:' + scrollHeightBefore + '; after: ' + this.scrollHeight);
    // Adapt height of textarea to new scrollHeight and padding
    this.style.height = (this.scrollHeight + padding) + "px";
}

// Clear console on load
setTimeout(() => {
console.clear();
}, 800);
// Clear console every 7 sec
window.setInterval(function(){
  console.clear();
}, 7000);

// Source: https://stackoverflow.com/a/25621277/9013718
<textarea style="font-size: 25px;width: 100%;" class="auto-resize-textarea">
Lorem ipsum dolor sit amet, consetetur aaaaa sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna </textarea>
<br>
<textarea placeholder="Expand me"></textarea>

When a size change is observed by the ResizeObserver, the function resizeTextarea() is correctly called.
I tried illustrating the issue with the snippet above, but basically what I noticed is the following:
The textarea.scrollHeight does increase and change when the scrollbar appears but for the height calculation to work a height "reset" is needed which is done with height: auto and after this reset the scrollHeight (which will become the textarea height) changes again to the same old value NOT taking into account the now new extra line from the wrapping caused by the shrunk space because of the scrollbar.
Here is an example with real numbers from my environment:
Assuming the textarea scrollHeight is 221 on initial load, after the height reset it may be 214, so the textarea height will be set to 214 (+ padding, but let's forget that for a sec).
Now, when a scrollbar appears (for example by expanding the second textarea) the scrollHeight becomes 237 before the reset but after the reset it becomes the same 214 as before when it should be a bit more.
My deduction is that it's the height reset height: auto that does not not taking into account the extra line and changes the scrollHeight back to the wrong value (the same as the old one) like this wrapped line doesn't exist.

The question: How can I make the textarea scrollHeight take into account line wrapping caused by an external scrollbar after height reset; or how to make a height reset on a textarea that takes into account wrapped line caused by a page scrollbar ?

I kind of think it should be working, might it be a bug ? I'd love an explanation of this behaviour.

0 Answers
Related