47

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?

Existe Deja
  • 1,227
  • 3
  • 14
  • 36
  • 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! – sinawic Sep 12 '18 at 10:13
  • Yeah I was thinking about that but I can't find the scroll duration for the `scrollTo` method :/ – Existe Deja Sep 12 '18 at 10:17
  • You might want to try [this](https://github.com/malihu/malihu-custom-scrollbar-plugin/issues/339) before using your hacky approach. – Akaino Sep 17 '18 at 11:18
  • 1
    Possible duplicate of [How to know scroll to element is done in Javascript?](https://stackoverflow.com/questions/46795955/how-to-know-scroll-to-element-is-done-in-javascript) – StudioTime Sep 17 '18 at 11:18
  • @Akaino 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. – Existe Deja Sep 17 '18 at 12:36
  • @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! – Existe Deja Sep 17 '18 at 12:36

3 Answers3

47

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'
    })
}
Community
  • 1
  • 1
Fabian von Ellerts
  • 4,763
  • 40
  • 35
  • Great function. I think you don't need to call `onScroll()` before `window.scrollTo`, though. – Carlos Roso Jun 03 '19 at 23:01
  • 5
    @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. – Fabian von Ellerts Jun 04 '19 at 11:58
  • 1
    The offset needs to be a whole number for this function to work – btjakes Apr 12 '20 at 17:31
  • 2
    @btjakes 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) { ... }` – btjakes Apr 14 '20 at 18:27
  • 1
    @btjakes 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. – Fabian von Ellerts Apr 15 '20 at 09:15
  • Any ideas why the callback gets called severally after the scroll settles @FabianvonEllerts? Thanks. – idrisadetunmbi Jun 30 '20 at 09:46
  • @idrisadetunmbi not really, I never experienced the issue when testing. You can debounce the `onScroll` function to prevent that. – Fabian von Ellerts Aug 11 '20 at 16:05
  • https://stackoverflow.com/a/51142522/470749 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. – Ryan Sep 26 '20 at 02:14
  • 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) – RonaldPK Jun 03 '22 at 11:19
  • 1
    @Ryan Thanks for the link to the alternative question, I found my exact use case there! – Felipe Maia Aug 29 '23 at 12:30
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
  }
}
Existe Deja
  • 1,227
  • 3
  • 14
  • 36
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',
        })
    }
sebpiq
  • 7,540
  • 9
  • 52
  • 69