Is there a callback for window.scrollTo?
Asked Answered
W

5

53

I want to call focus() on an input after the widow scrolled. I'm using the smooth behavior for the scrollTo() method. The problem is the focus method cut the smooth behavior. The solution is to call the focus function just after the scroll end.

But I can't find any doc or threads speaking about how to detect the end of scrollTo method.

let el = document.getElementById('input')
let elScrollOffset = el.getBoundingClientRect().top
let scrollOffset = window.pageYOffset || document.documentElement.scrollTop
let padding = 12
window.scrollTo({
  top: elScrollOffset + scrollOffset - padding,
  behavior: 'smooth'
})
// wait for the end of scrolling and then
el.focus()

Any ideas?

Wyne answered 12/9, 2018 at 10:5 Comment(6)
I don't really know if a call back exists for scrollTo() but I think you can try setTimeout() only if you know the duration of scrolling!Joyann
Yeah I was thinking about that but I can't find the scroll duration for the scrollTo method :/Wyne
You might want to try this before using your hacky approach.Pride
Possible duplicate of How to know scroll to element is done in Javascript?Cohobate
@Pride Thank you for the tip, in vanilla I would just work inside a onscroll listener and watch the page offset. Not so far of what I'm doing.Wyne
@Darren Sweeney, you're right I wasn't looking for scrollIntoView because it isn't well supported yet. I will answer to it too, thanks!Wyne
P
55

I wrote a generic function based on the solution of George Abitbol, without overwriting window.onscroll:

/**
 * Native scrollTo with callback
 * @param offset - offset to scroll to
 * @param callback - callback function
 */
function scrollTo(offset, callback) {
    const fixedOffset = offset.toFixed();
    const onScroll = function () {
            if (window.pageYOffset.toFixed() === fixedOffset) {
                window.removeEventListener('scroll', onScroll)
                callback()
            }
        }

    window.addEventListener('scroll', onScroll)
    onScroll()
    window.scrollTo({
        top: offset,
        behavior: 'smooth'
    })
}
Padrone answered 15/4, 2019 at 9:59 Comment(12)
Great function. I think you don't need to call onScroll() before window.scrollTo, though.Cornia
@caroso1222 Thank you! I added that after some testing, because if the scroll position is already correct the callback won't fire without that call.Padrone
The offset needs to be a whole number for this function to workGar
@Gar To clarify: the offset needs to perfectly match the window.pageYOffset. In MS Edge window.pageYOffset is a float, unlike other browsers. With precision mismatching across browsers, this approach will fail in one or the other unless you're careful with rounding and possibly making a buffer of completion: if (roundedPageYOffset + 2 >= roundedOffset && roundedOffset >= roundedPageYOffset - 2) { ... }, rather than just if (window.pageYOffset === offset) { ... }Gar
@Gar I fixed the number so floats should not be a problem any more. Rounding is a good idea, but might not be precise enough depending on the use case.Padrone
Any ideas why the callback gets called severally after the scroll settles @FabianvonEllerts? Thanks.Penutian
@Penutian not really, I never experienced the issue when testing. You can debounce the onScroll function to prevent that.Padrone
https://mcmap.net/q/238741/-how-to-know-scroll-to-element-is-done-in-javascript was also helpful for me since I'm triggering scrolling on an infinite page until there are no more results to load, so I think setTimeout is useful.Attalanta
Thank you for the excellent function. It noticed that when the offset point is close to the end of the document, window.pageYOffset and offset will never match. Adding offset = Math.min(offset, documentHeight - viewportHeight); to the beginning of the script helps. (determining documentHeight and viewportHeight is another discussion)Lowdown
@Attalanta Thanks for the link to the alternative question, I found my exact use case there!Sucre
Need to update pageYOffset to scrollY because the first one is depreciated.Blowzy
This doesn't work if the user starts scrolling also manually, and might miss the equality check.Loralyn
W
4

I found a way to achieve what I want but I think it's a bit hacky, isn't it?

let el = document.getElementById('input')
let elScrollOffset = el.getBoundingClientRect().top
let scrollOffset = window.pageYOffset || document.documentElement.scrollTop
let padding = 12
let target = elScrollOffset + scrollOffset - padding
window.scrollTo({
  top: target,
  behavior: 'smooth'
})
window.onscroll = e => {
  let currentScrollOffset = window.pageYOffset || document.documentElement.scrollTop
  // Scroll reach the target
  if (currentScrollOffset === target) {
    el.focus()
    window.onscroll = null // remove listener
  }
}
Wyne answered 12/9, 2018 at 10:17 Comment(0)
F
1

Other answers didn't fully work for me, therefore based on @Fabian von Ellerts answer, I wrote my own solution.

My problems were that :

  • The element I was scrolling (and all its parents along the hierarchy) always had a offsetTop of 0, so it was not working.

  • I needed to scroll a nested element.

Using getBoundingClientRect and a container element as reference works :

    const smoothScrollTo = (
        scrollContainer,
        scrolledContent,
        offset,
        callback
    ) => {
        const fixedOffset = (
            scrollContainer.getBoundingClientRect().top + offset
        ).toFixed()
        const onScroll = () => {
            if (
                scrolledContent.getBoundingClientRect().top.toFixed() ===
                fixedOffset
            ) {
                scrollContainer.removeEventListener('scroll', onScroll)
                callback()
            }
        }
        scrollContainer.addEventListener('scroll', onScroll)
        onScroll()
        scrollContainer.scrollTo({
            top: offset,
            behavior: 'smooth',
        })
    }
Friedrich answered 23/10, 2020 at 17:54 Comment(0)
P
1

There is now a scrollend event, supported everywhere but in Safari, but for what you want you shouldn't even need it, since we're supposed to be able to set the scrolling behavior through CSS, with the scroll-behavior rule, this way even .focus() should trigger a smooth scroll. Unfortunately, and weirdly enough, only Chrome seems to support this...

document.querySelector("button").onclick = (evt) => document.querySelector("input").focus();
.obstacle {
  background: repeating-linear-gradient(black, black 20px, white 20px, white 40px);
  height: 800vh;
}
html { /* The scrolling element */
  scroll-behavior: smooth
}
<button>focus target</button>
<div class=obstacle></div>
<input>
Putupon answered 19/2 at 23:36 Comment(0)
B
0

I just wanted to make an async version of the @Fabian von Ellerts function, replaced the depreciated pageYOffset, and added semicolons :)

/**
 * Async scrollTo
 * @param offset - offset to scroll to
 */
async function scrollTo(offset) {
    return new Promise((resolve, reject) => {
        const fixedOffset = offset.toFixed();
        const onScroll = function () {
                if (window.scrollY.toFixed() === fixedOffset) {
                    window.removeEventListener('scroll', onScroll);
                    resolve();
                }
            }

        window.addEventListener('scroll', onScroll);
        onScroll();
        window.scrollTo({
            top: offset,
            behavior: 'smooth'
        });
    });
}
Blowzy answered 16/2 at 13:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.