Existe Deja
Existe Deja

Reputation: 1379

Is there a callback for window.scrollTo?

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?

Upvotes: 56

Views: 34728

Answers (5)

Kaiido
Kaiido

Reputation: 137006

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>

Upvotes: 1

Eugene
Eugene

Reputation: 1170

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'
        });
    });
}

Upvotes: 0

Fabian von Ellerts
Fabian von Ellerts

Reputation: 5211

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'
    })
}

Upvotes: 56

sebpiq
sebpiq

Reputation: 7812

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',
        })
    }

Upvotes: 1

Existe Deja
Existe Deja

Reputation: 1379

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
  }
}

Upvotes: 4

Related Questions