auto show: Use textarea as the anchor element if available (#2407)

* enhance: use textarea as the anchor element if available

* correct only downwards vertical layout shifts
This commit is contained in:
soxa 2025-08-09 22:35:48 +02:00 committed by GitHub
parent dea8945e43
commit 1379c419df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -8,26 +8,57 @@ export default function preserveScroll (callback) {
return return
} }
// get a reference element at the center of the viewport to track if content is added above it // check if a ref element is in the viewport
const ref = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2) const isElementInViewport = (element) => {
const refTop = ref ? ref.getBoundingClientRect().top + scrollTop : scrollTop if (!element?.getBoundingClientRect) return false
const rect = element.getBoundingClientRect()
return (
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < window.innerHeight &&
rect.left < window.innerWidth
)
}
// pick a textarea element to use as anchor ref, if any
const selectTextarea = () => {
// pick the focused textarea, if any
const active = document.activeElement
if (active && active.tagName === 'TEXTAREA' && isElementInViewport(active)) {
return active
}
// if no textarea is focused, check if there are any in the viewport
const textareas = document.querySelectorAll('textarea')
for (const textarea of textareas) {
if (isElementInViewport(textarea)) {
return textarea
}
}
return null
}
// if no textarea is found, use the center of the viewport as fallback anchor
const anchorRef = selectTextarea() || document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2)
const refTop = anchorRef ? anchorRef.getBoundingClientRect().top + scrollTop : scrollTop
// observe the document for changes in height // observe the document for changes in height
const observer = new window.MutationObserver(() => { const observer = new window.MutationObserver(() => {
// request animation frame to ensure the DOM is updated
window.requestAnimationFrame(() => {
// we can't proceed if we couldn't find a traceable reference element
if (!ref) {
cleanup() cleanup()
return
}
// get the new position of the reference element along with the new scroll position // double rAF to ensure the DOM is updated - textareas are rendered on the next tick
const newRefTop = ref ? ref.getBoundingClientRect().top + window.scrollY : window.scrollY window.requestAnimationFrame(() => {
// has the reference element moved? window.requestAnimationFrame(() => {
if (!anchorRef) return
// get the new position of the anchor ref along with the new scroll position
const newRefTop = anchorRef ? anchorRef.getBoundingClientRect().top + window.scrollY : window.scrollY
// has the anchor ref moved?
const refMoved = newRefTop - refTop const refMoved = newRefTop - refTop
// if the reference element moved, we need to scroll to the new position // if the anchor ref moved, we need to scroll to the new position
if (refMoved > 0) { if (refMoved > 0) {
window.scrollTo({ window.scrollTo({
// some browsers don't respond well to fractional scroll position, so we round up the new position to the nearest integer // some browsers don't respond well to fractional scroll position, so we round up the new position to the nearest integer
@ -35,8 +66,7 @@ export default function preserveScroll (callback) {
behavior: 'instant' behavior: 'instant'
}) })
} }
})
cleanup()
}) })
}) })