fix: prevent page jumps due to textarea auto resizing (#7569)

The change prevents unnecessary resizing of the text field.

## Testing

- Compose some text e.g. in an issue
- Enter a lot of lines, until the compose field is as big as possible
- Move the screen up, the top of the compose field should leave the screen, but the last e.g. 5 lines should still be visible on the screen
- Append some text
- Make sure the textfield just gets bigger, but doesn't jump to the bottom of the page

Fixes #7522

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7569
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Beowulf <beowulf@beocode.eu>
Co-committed-by: Beowulf <beowulf@beocode.eu>
This commit is contained in:
Beowulf 2025-10-01 03:47:19 +02:00 committed by Earl Warren
commit 1cd0c5e99b
2 changed files with 18 additions and 17 deletions

View file

@ -84,7 +84,7 @@
}
text-expander {
display: block;
display: flex;
position: relative;
}

View file

@ -117,9 +117,16 @@ export function isDocumentFragmentOrElementNode(el) {
// included in all copies or substantial portions of the Software.
// ---------------------------------------------------------------------
export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
const textareaParent = textarea.parentElement;
function createJumpPreventer() {
const el = document.createElement('div');
textareaParent.prepend(el, textareaParent.firstChild);
return el;
}
const jumpPreventer = textareaParent.querySelector('div') || createJumpPreventer();
let isUserResized = false;
// lastStyleHeight and initialStyleHeight are CSS values like '100px'
let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight;
let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight, lastLines;
function onUserResize(event) {
if (isUserResized) return;
@ -157,6 +164,12 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
const {top, bottom} = overflowOffset();
const isOutOfViewport = top < 0 || bottom < 0;
const currLines = textarea.value.split('\n').length;
const shouldResize = !isOutOfViewport || currLines < lastLines;
if (currLines < lastLines) jumpPreventer.style.height = '0';
lastLines = currLines;
if (!shouldResize) return;
const computedStyle = getComputedStyle(textarea);
const topBorderWidth = parseFloat(computedStyle.borderTopWidth);
const bottomBorderWidth = parseFloat(computedStyle.borderBottomWidth);
@ -168,23 +181,10 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
const maxHeight = curHeight + bottom - adjustedViewportMarginBottom;
textarea.style.height = 'auto';
let newHeight = textarea.scrollHeight + borderAddOn;
if (isOutOfViewport) {
// it is already out of the viewport:
// * if the textarea is expanding: do not resize it
if (newHeight > curHeight) {
newHeight = curHeight;
}
// * if the textarea is shrinking, shrink line by line (just use the
// scrollHeight). do not apply max-height limit, otherwise the page
// flickers and the textarea jumps
} else {
// * if it is in the viewport, apply the max-height limit
newHeight = Math.min(maxHeight, newHeight);
}
const newHeight = Math.min(maxHeight, textarea.scrollHeight + borderAddOn);
textarea.style.height = `${newHeight}px`;
jumpPreventer.style.height = textarea.style.height;
lastStyleHeight = textarea.style.height;
} finally {
// ensure that the textarea is fully scrolled to the end, when the cursor
@ -209,6 +209,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
textarea.addEventListener('input', resizeToFit);
textarea.form?.addEventListener('reset', onFormReset);
initialStyleHeight = textarea.style.height ?? undefined;
lastLines = textarea.value.split('\n').length;
if (textarea.value) resizeToFit();
return {