Search code examples
javascriptcarouseltouch-eventslidepreventdefault

When I touch my JavaScript slider on mobile, I can't scroll down the page


I've built a slider using pure JavaScript. On mobile screen when I touch the slider I can only change slides but can not scroll down the page.
Under the hood when the slider element is touched, a "touchstart" event fires and the corresponding event handler function, features event.preventDefault() to stop scrolling the page, then when "touchmove" event fires, the code uses difference between first and new horizontal coordinates and CSS left propery to move the slider.

I made a minimal code below. Also click to see code on online editor.

const slides = document.querySelector(".slides");
let posX1, posX2, dX;

slides.addEventListener("touchstart", dragStart);
slides.addEventListener("touchmove", dragAction);

function dragStart(e) {
  e.preventDefault();
  posX1 = e.touches[0].clientX;
}

function dragAction(e) {
  posX2 = e.touches[0].clientX;
  dX = posX2 - posX1;
  posX1 = e.touches[0].clientX;
  slides.style.left = (slides.offsetLeft + dX) + "px";
}
body {
  padding-bottom: 1000px;
}

.slider {
  width: 200px;
  height: 100px;
  margin: 0 auto;
  border: 1px solid black;
  position: relative;
  overflow: hidden;
}

.slides {
  width: 600px;
  height: 100px;
  display: flex;
  position: absolute;
}

.slide {
  width: 200px;
  height: 100px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.slide:nth-child(1) {
  background-color: rgb(200, 200, 200);
}

.slide:nth-child(2) {
  background-color: rgb(150, 150, 150);
}

.slide:nth-child(3) {
  background-color: rgb(100, 100, 100);
}
<!DOCTYPE html>
<html>

<body>
  <div class="slider">
    <div class="slides">
      <div class="slide">Slide 1</div>
      <div class="slide">Slide 2</div>
      <div class="slide">Slide 3</div>
    </div>
  </div>
</body>

</html>

Thanks for your time :)


Solution

  • I think nobody has checked my question but me, however, assuming there's a hypothetical person having the same problem as mine, I could fix that:
    I figured out how to make a JavaScript slider using this post in which the author has used preventDefault() inside "touchstart" EventListener and I stuck to it, but the solution is to simply call preventDefault() method on "touchmove" event itself not "touchstart", off course if you need (which is if the user's trying to change slides). and if user's trying to scroll the page then remove "touchend" EventListener.

    const slides = document.querySelector(".slides");
    let posX1,
        posX2,
        posY1,
        posY2,
        dX,
        dY,
        dirDetected = false;
        
      //feature detection
      //-------------Note 1-----------//
      let passiveIfSupported = false;
      try {
        window.addEventListener("test", null, Object.defineProperty({}, "passive", {
          get: function() {passiveIfSupported = {passive: false};}
        }));
      } catch(err) {}
    
    slides.addEventListener("touchstart", dragStart, passiveIfSupported);
    slides.addEventListener("touchmove", dragAction, passiveIfSupported);
    slides.addEventListener("touchend", dragEnd, false);
    
    function dragStart(e) {
      posX1 = e.touches[0].clientX;
      posY1 = e.touches[0].clientY;
    }
    
    function dragAction(e) {
      //-------------Note 2-----------//
      e.preventDefault();
      
      posX2 = e.touches[0].clientX;
      posY2 = e.touches[0].clientY;
      dX = posX2 - posX1;
      posX1 = e.touches[0].clientX;
      dY = posY2 - posY1;
      
      if (!dirDetected) {
        if (Math.abs(dY) > Math.abs(dX)) {
          slides.removeEventListener("touchmove", dragAction, passiveIfSupported);
          return;
        }
        dirDetected = true;
      }
        
      slides.style.left = (slides.offsetLeft + dX) + "px";
    }
    
      function dragEnd() {
        if (!dirDetected) {
          slides.addEventListener("touchmove", dragAction, passiveIfSupported);
        }
        dirDetected = false;
      }
    body {
      padding-bottom: 1000px;
    }
    
    .slider {
      width: 200px;
      height: 100px;
      margin: 0 auto;
      border: 1px solid black;
      position: relative;
      overflow: hidden;
    }
    
    .slides {
      width: 600px;
      height: 100px;
      display: flex;
      position: absolute;
    }
    
    .slide {
      width: 200px;
      height: 100px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .slide:nth-child(1) {
      background-color: rgb(200, 200, 200);
    }
    
    .slide:nth-child(2) {
      background-color: rgb(150, 150, 150);
    }
    
    .slide:nth-child(3) {
      background-color: rgb(100, 100, 100);
    }
    <!DOCTYPE html>
    <html>
    
    <body>
      <div class="slider">
        <div class="slides">
          <div class="slide">Slide 1</div>
          <div class="slide">Slide 2</div>
          <div class="slide">Slide 3</div>
        </div>
      </div>
    </body>
    
    </html>

    Note 1. (addEventListener's third parameter): note the third parameter of addEventListener() method. there's something I realized in my searches from this question. I'm going to use preventDefault() inside "touchmove" EventListener but in some versions of Chrome and Firefox for "touchstart" and "touchmove" events if you don't specify passive property of options object (the third parameter of addEventListener()) the default value would be set to true and this doesn't let the EventListener to call preventDefault() so you have to set the third parameter to {passive: false} but if the browser loading your script is an old one which requires a third boolean parameter for addEventListener() then you need to provide a boolean value not object. So you can use feature detection to provide an appropraite parameter.

    Note 2. Firefox and chrom android handle "touchmove" preventDefault() differently: Here's another point I found (from this question), if you run the above code on Chrome Android it works well, you can scroll the page or change slides but on Firefox Android you can't scroll. Actually I think it's a Chrome's fault because according to Specs for "touchmove" event if the first "touchmove" event is prevented the subsequent "touchmove" events bound to the same touch point are prevented and in the code above at the very first line of "touchmove" event handler function I used preventDefault() so I need to call it after the if block so ensure default behaviour is prevented when needed.

    const slides = document.querySelector(".slides");
    let posX1,
        posX2,
        posY1,
        posY2,
        dX,
        dY,
        dirDetected = false;
        
      //feature detection
      let passiveIfSupported = false;
      try {
        window.addEventListener("test", null, Object.defineProperty({}, "passive", {
          get: function() {passiveIfSupported = {passive: false};}
        }));
      } catch(err) {}
    
    slides.addEventListener("touchstart", dragStart, passiveIfSupported);
    slides.addEventListener("touchmove", dragAction, passiveIfSupported);
    slides.addEventListener("touchend", dragEnd, false);
    
    function dragStart(e) {
      posX1 = e.touches[0].clientX;
      posY1 = e.touches[0].clientY;
    }
    
    function dragAction(e) {
      posX2 = e.touches[0].clientX;
      posY2 = e.touches[0].clientY;
      dX = posX2 - posX1;
      posX1 = e.touches[0].clientX;
      dY = posY2 - posY1;
      
      if (!dirDetected) {
        if (Math.abs(dY) > Math.abs(dX)) {
          slides.removeEventListener("touchmove", dragAction, passiveIfSupported);
          return;
        }
        e.preventDefault();
      }
      
      dirDetected = true;
      slides.style.left = (slides.offsetLeft + dX) + "px";
    }
    
      function dragEnd() {
        if (!dirDetected) {
          slides.addEventListener("touchmove", dragAction, passiveIfSupported);
        }
        dirDetected = false;
      }
    body {
      padding-bottom: 1000px;
    }
    
    .slider {
      width: 200px;
      height: 100px;
      margin: 0 auto;
      border: 1px solid black;
      position: relative;
      overflow: hidden;
    }
    
    .slides {
      width: 600px;
      height: 100px;
      display: flex;
      position: absolute;
    }
    
    .slide {
      width: 200px;
      height: 100px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .slide:nth-child(1) {
      background-color: rgb(200, 200, 200);
    }
    
    .slide:nth-child(2) {
      background-color: rgb(150, 150, 150);
    }
    
    .slide:nth-child(3) {
      background-color: rgb(100, 100, 100);
    }
    <!DOCTYPE html>
    <html>
    
    <body>
      <div class="slider">
        <div class="slides">
          <div class="slide">Slide 1</div>
          <div class="slide">Slide 2</div>
          <div class="slide">Slide 3</div>
        </div>
      </div>
    </body>
    
    </html>