3

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.

Samuel Gfeller
  • 840
  • 9
  • 19
  • How do you mean "don't change the content"; where am I changing the content? I am changing the height of the textarea on each input or resize event. And knowing that there is a scrollbar on the textarea doesn't help, as the scrollbar that is problematic for me is the one for the entire page, caused by expanding the second textarea. If you look at the code, there is an `overflow-y: hidden;` so there is no scrollbar anyway on the textarea. You can test it out, when the page scrollbar appears because the text gets too long in the first textarea of my second example, there is no issue, it works. – Samuel Gfeller Aug 24 '22 at 15:19
  • I'm open for suggestions from those who read the whole question. – Samuel Gfeller Aug 24 '22 at 15:32
  • 1
    Maybe [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver). Though it may be tricky since you're resizing the element in response to the element being resized. – Ouroborus Aug 24 '22 at 19:45
  • Does this answer your question? [Javascript detect scrollbar in textarea](https://stackoverflow.com/questions/3238515/javascript-detect-scrollbar-in-textarea) – Dean Van Greunen Aug 25 '22 at 10:34
  • No not at all, as explained above. Please read through my whole question before suggesting closing it! – Samuel Gfeller Aug 25 '22 at 10:42
  • @Ouroborus thank you so much, this actually worked, I could detect the event and with the height reset it doesn't cause an infinite loop so that's nice, but unfortunately it didn't solve my issue. See my edit. – Samuel Gfeller Aug 25 '22 at 11:13
  • For this particular example, adding `body {margin: 0;}` and `textarea {box-sizing: border-box;}` seem like they help. `border-box` seem obvious in hindsight as to why it helps. `margin: 0`, not so much. – Ouroborus Aug 25 '22 at 16:00
  • @Ouroborus the issue persists https://i.imgur.com/mUrx3c3.mp4 – Samuel Gfeller Aug 25 '22 at 16:19
  • The problem being that the last line ("magna") is falling below the bottom of the text area? (I can't quiet tell from the video as, if it is, it appears exactly one line short.) If this is always the case, maybe add one line-height worth of pixels to the text area. – Ouroborus Aug 25 '22 at 17:34
  • Yes exactly, the problem is that when the last line wraps because of an appearing scrollbar, the height: auto "reset" makes a scrollHeight value on the textarea that is not correct. It is however correct when resizing the window (as long as no scrollbar appears) and when typing inside. And no its not always the case, only when a scrollbar appears and it makes the words wrap in such a way that a new line is created. Adding one line-height would make the textarea too hight at the bottom and I'm trying to build the "perfect" clean solution here so having to ressort to that would be quite a defeat – Samuel Gfeller Aug 25 '22 at 17:47
  • 1
    [This](https://replit.com/@ouroborus/ImperfectDimgrayVendor) is about as good as I could get it. (One of the interface buttons lets you open the web view in a new tab if you like.) It seems like `.scrollHeight` is "late" when something causes it to need an update. I'm not sure how well my strategy will work with multiple text area widgets on the page. It may need to be re-written so that multiple size changes in the same event are collected and processed as a batch rather than having individual calls for each element. – Ouroborus Aug 26 '22 at 05:26
  • Bleh. I guess replit works better if you have an account. – Ouroborus Aug 26 '22 at 07:24
  • This is nice well done! I didn't know the `requestAnimationFrame` callback, what does it exactly do in this situation? Feel free to make an answer out of it with some explanation as to how you fixed it. Do you think there is a way to remove the "jitter" on resize?https://i.imgur.com/RTI2gbR.gif – Samuel Gfeller Aug 26 '22 at 08:30
  • 1
    I made some further changes. Should fix the jitter. At this point, `requestAnimationFrame`, as it's used there, just lets it ignore additional resize requests for two frames. (One frame should've been enough, but there's an edge case where, if the page is a certain length, updates can cause the scrollbar to reverse state, causing the whole thing to rapidly flip between with-scrollbar and without-scrollbar. Waiting for a second frame fixes this.) – Ouroborus Aug 26 '22 at 15:56
  • When you manually resize the main textarea, you're fighting with the browser setting the element width and height. Jitter related to this may not be fixable unless there's some event to detect this form of change that you can respond to. – Ouroborus Aug 26 '22 at 16:03
  • Jitter is fixed indeed. So `requestAnimationFrame` sets the var `working` to `false` which makes that the next time the function `resizeTextarea` is called, the resize actually happens? And each time a resize happens, `working` is set to true. But `working` is outside the function scope so I'm wondering if it would work with multiple textareas? Because for me it breaks when there are multiple especially on window resizing https://i.imgur.com/9eUZxCB.mp4. And I was curious about the padding too, can you just get rid of the issue by adding `borderBottom + borderTop` ? – Samuel Gfeller Aug 30 '22 at 14:40
  • https://replit.com/@samuelgfeller/Auto-adjusting-text-areas#script.js – Samuel Gfeller Aug 30 '22 at 14:48

0 Answers0