1

The Project: I have setup a scroll function that should scroll to the next item within a container div when clicked "up" or "down." It is leveraging the window.requestAnimationFrame() method for the loop and calling element.scrollTo on each iteration.

The reason I made this is to allow for custom duration, as the "behavior: 'smooth'" setting is a set speed, and I would like to keep the css scroll-snap functionality so it needs to be within a container(So window.scrollTo() is not possible, although that seems to work across browsers).

The Problem: This works perfectly in Safari, but for some reason the .scrollTo() function is not moving the scroll view in Chrome and Firefox.

What I've Tried:

  1. I have tried setting element.scrollTop instead of calling element.scrollTo() with the same outcome.
  2. I read somewhere that Chrome's performance is too quick for certain loops, so I surrounded the element.scrollTo() with a setTimeout(()=>{},1) with varying durations. This did not work either.

My Code: The code is viewable in Codepen.io here

HTML:

<div class="scroll-wrap">
  <div id="item-1" class="scroll-item blue">
    <h1>1</h1>
    <div class="scroll-up"></div>
    <div class="scroll-down"></div>
  </div>
  <div id="item-2" class="scroll-item green">
    <h1>2</h1>
    <div class="scroll-up"></div>
    <div class="scroll-down"></div>
  </div>
  <div id="item-3" class="scroll-item red">
    <h1>3</h1>
    <div class="scroll-up"></div>
    <div class="scroll-down"></div>
  </div>
</div>

JS:

const scrollUps = document.querySelectorAll('.scroll-up')
const scrollDowns = document.querySelectorAll('.scroll-down')

// options...
options = {
  scrollWrap: document.querySelector('.scroll-wrap'), // scroll container.
  duration: 300, // Pixels per second. The smaller the number the longer the duration.
  easing: 'easeInOutSine', // can be easeOutSine, easeInOutSine, easeInOutQuint.
}

// RAF shim from ... http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          function( callback ){
            window.setTimeout(callback, 1000 / 60);
          };
})();

// Click Scroll function
function scrollToY(currentPosition, scrollTargetY) {
    // currentPostion: the current scrollY property of the view within the scroll container
    // scrollTargetY: the target scrollY property of the window
    // speed: time in pixels per second
    // easing: easing equation to use

var scrollY = currentPosition,
    scrollTargetY = scrollTargetY || 0,
    speed = options.duration || 2000,
    easing = options.easing || 'easeOutSine',
    currentTime = 0;

 // *For speed in pixels per second... 
 // min time .1, max time 5 seconds
var time = Math.max(.1, Math.min(Math.abs(scrollY - scrollTargetY) / speed, 5));


// easing equations from https://github.com/danro/easing-js/blob/master/easing.js
const PI_D2 = Math.PI / 2,
    easingEquations = {
        easeOutSine: function (pos) {
            return Math.sin(pos * (Math.PI / 2));
        },
        easeInOutSine: function (pos) {
            return (-0.5 * (Math.cos(Math.PI * pos) - 1));
        },
        easeInOutQuint: function (pos) {
            if ((pos /= 0.5) < 1) {
                return 0.5 * Math.pow(pos, 5);
            }
            return 0.5 * (Math.pow((pos - 2), 5) + 2);
        }
    };

  // animation loop
  function tick() {
      currentTime += 1 / 60;

      const p = currentTime / time;
      const t = easingEquations[easing](p);

      if (p < 1) {
        requestAnimFrame(tick);
        const newPosition = scrollY + ((scrollTargetY - scrollY) * t)
        setTimeout(()=>{
          options.scrollWrap.scrollTo(0, newPosition);
          // console.log('scroll:', options.scrollWrap.scrollTop);
        },1)
        
        
      } else {
        // console.log('scroll done');
        options.scrollWrap.scrollTo(0, scrollTargetY);
      }
  }

  // call it once to get started
  tick();
}

// set clickable areas...
Array.from(scrollUps).forEach(btn => {
  btn.addEventListener('click', (e) => {
    // Get Parent of button...
    const parent = e.target.parentElement
    // Get destination element...
    const destId = `item-${Number(parent.id.split('-')[1]) - 1}`
    const dest = document.getElementById(destId)
    // call scroll function if destination exists...
    if(dest) {
      const destDistanceToTop = dest.offsetTop
      scrollToY(parent.offsetTop, destDistanceToTop)
    }
  })
})

Array.from(scrollDowns).forEach(btn => {
  btn.addEventListener('click', (e) => {
    const parent = e.target.parentElement
    const destId = `item-${Number(parent.id.split('-')[1]) + 1}`
    const dest = document.getElementById(destId)
    if(dest) {
      const destDistanceToTop = dest.offsetTop
      // console.log(destDistanceToTop, parent.offsetTop)
      scrollToY(parent.offsetTop, destDistanceToTop)
  
    }
  })
})
  • I can confirm I've seen the same problem in Chrome, both on desktop & mobile. I could only get around it by setting `scrollTo` inside a separate function invoked by `setInterval`. – shark8me Mar 09 '21 at 09:34

1 Answers1

0

This is due to the scroll-snap in CSS. It won't work along with requestAnimationFrame (for some reason).

I removed the scroll-snap from your CSS and it seems to work just fine without it.

glneto
  • 527
  • 3
  • 10