web-dev-qa-db-fra.com

iOS - css/js - Défilement en incrustation mais prévention du défilement du corps

Je sais que quelques questions similaires ont été posées, mais elles ne fonctionnent pas pour mon cas d'utilisation ou les réponses acceptées ont un défaut qui ne fonctionne pas pour moi. Alors...

J'ai une page avec une liste d'éléments. Un clic sur un élément de la liste ouvrira une superposition contenant des détails sur cet élément. J'ai besoin que cette superposition soit déroulable, mais je ne veux pas que le reste de la page située sous la superposition défile de sorte qu'une fois la superposition fermée, vous êtes dans la même position (la superposition est également légèrement transparente et gênante pour l'utilisateur pour voir la page défiler ci-dessous, pourquoi également je ne peux pas enregistrer le scrollY et réinitialiser à la fermeture).

En ce moment, je travaille partout sauf sur iOS. C'est fondamentalement ce que j'ai:

<html>
   <body>
      <ul id="list">
         <li>something 1</li>
         <li>something 2</li>
         <li>something 3</li>
         <li>something 4</li>
         <li>something 5</li>
      </ul>
      <div id="overlay"></div>
   </body>
</html>

CSS:

body.hidden {
   overflow: hidden;
}
#overlay {
   opacity: 0;
   top: -100vh;
}
#overlay.open {
   opacity: 1;
   overflow-y: scroll;
   overflow-x: hidden;
   top: 0;
}

Puis, dans mon clic clic, je bascule la classe hidden sur body, la classe open sur #overlay et renseigne l'élément #overlay avec mon contenu. Comme je l'ai dit, cela fonctionne bien partout sauf sur iOS.

Solutions J'ai vu d'autres endroits dire que j'ai besoin d'utiliser position:fixed et height:100% sur les balises body et/ou html. Le problème avec cette solution est que vous perdez votre position de défilement et lorsque vous fermez la superposition, vous revenez en haut de la page. Certaines de ces listes peuvent être très longues, alors ce n'est pas une option pour moi.

Je ne peux pas empêcher le défilement complet avec preventDefault sur le corps ou quelque chose parce que j'ai besoin que le contenu de superposition soit déroulable.

D'autres suggestions?

13
jcmitch

Il n'y a pas moyen de contourner cela pour l'instant. Depuis iOS 9.3, il n'y a toujours pas de bon moyen d'empêcher le parchemin sur le corps. La meilleure méthode que je mette actuellement en œuvre sur tous les sites qui le nécessitent est de verrouiller le HTML et la hauteur et le débordement du corps.

html, body {
  height: 100%;
  overflow: hidden;
}

C’est le meilleur moyen d’empêcher le défilement iOS de faire défiler le contenu derrière la superposition/le modal.

Ensuite, pour conserver la position de défilement, je déplace le contenu derrière vers le haut pour lui donner l’impression de le conserver, puis, lorsque le modal se ferme, restaure la position du corps.

Je le fais avec une fonction de verrouillage et de déverrouillage dans jQuery

var $docEl = $('html, body'),
  $wrap = $('.content'),
  $.scrollTop;

$.lockBody = function() {
  if(window.pageYOffset) {
    scrollTop = window.pageYOffset;

    $wrap.css({
      top: - (scrollTop)
    });
  }

  $docEl.css({
    height: "100%",
    overflow: "hidden"
  });
}

$.unlockBody = function() {
  $docEl.css({
    height: "",
    overflow: ""
  });

  $wrap.css({
    top: ''
  });

  window.scrollTo(0, scrollTop);
  window.setTimeout(function () {
    scrollTop = null;
  }, 0);
}

Lorsque vous assemblez tous ces éléments, vous obtenez http://codepen.io/jerrylow/pen/yJeyoG si vous souhaitez tester votre téléphone, voici le résultat: http://jerrylow.com/demo/ios-body-lock/

14
jerrylow

Pourquoi la page défile-t-elle lorsque je fais défiler le modal?  

Si la propriété css -webkit-overflow-scrolling: touch; est activée sur l'élément situé derrière le modal, certains kiosques de code natif semblent écouter les événements touchmove que nous ne pouvons pas capturer.

Alors quoi maintenant?

J'ai résolu ce problème pour mon application en ajoutant une classe pour annuler la propriété css lorsque le modal est visible. Ceci est un exemple pleinement fonctionnel.

let pageEl = document.querySelector(".page");
let modalEl = document.querySelector(".modal");

function openModal(e){
  e.preventDefault();
  pageEl.classList.add("page--has-modal");
  modalEl.classList.remove("hidden");
  window.addEventListener("wheel", preventScroll);
  window.addEventListener("touchmove", preventScroll);
}
function closeModal(e){
  e.preventDefault();
  pageEl.classList.remove("page--has-modal");
  modalEl.classList.add("hidden");
  
  window.removeEventListener("wheel", preventScroll);
  window.removeEventListener("touchmove", preventScroll);
}

window.addEventListener("click", function(){
  console.log(modalEl.scrollHeight);
  console.log(modalEl.clientHeight);
});

function preventScroll(e){
  if (!isDescendant(modalEl, e.target)){
    e.preventDefault();
    return false;
  }
  
  let modalTop = modalEl.scrollTop === 0;
  let modalBottom = modalEl.scrollTop === (modalEl.scrollHeight -      modalEl.clientHeight);
  
  if (modalTop && e.deltaY < 0){
    e.preventDefault();
  } else if (modalBottom && e.deltaY > 0){
    e.preventDefault();
  }
}

function isDescendant(parent, child) {
     var node = child.parentNode;
     while (node != null) {
         if (node == parent) {
             return true;
         }
         node = node.parentNode;
     }
     return false;
}
.page { 
  -webkit-overflow-scrolling: touch; 
}
.page--has-modal { 
  -webkit-overflow-scrolling: auto;  
}

.modal {
  position: absolute;
  top: 50px;
  left: 50px;
  right: 50px;
  bottom: 50px;
  background: #c0c0c0;
  padding: 50px;
  text-align: center;
  overflow: auto;
  -webkit-overflow-scrolling: auto; 
}
.hidden {
  display: none;
}
<div class="page">
<button onclick="openModal(event);">Open modal</button>
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Integer consequat sapien a lectus gravida euismod. Sed vitae nisl non odio viverra accumsan. Curabitur nisi neque, egestas sed, vulputate sit amet, luctus vitae, dolor. Cras lacus massa, sagittis ut, volutpat consequat, interdum a, nulla. Vivamus rhoncus molestie nulla. Ut porttitor turpis sit amet turpis. Nam suscipit, justo quis ullamcorper sagittis, mauris diam dictum elit, suscipit blandit ligula ante sit amet mauris. Integer id arcu. Aenean scelerisque. Sed a purus. Pellentesque nec nisl eget metus varius tempor. Curabitur tincidunt iaculis lectus. Aliquam molestie velit id urna. Suspendisse in ante ac nunc commodo placerat.</p>

<p>Morbi gravida posuere est. Fusce id augue. Sed facilisis, felis quis ornare consequat, neque risus faucibus dui, quis ullamcorper tellus lacus vitae felis. Phasellus ac dolor. Integer ante diam, consectetuer in, tempor vitae, volutpat in, enim. Integer diam felis, semper at, iaculis ut, suscipit quis, dolor. Vestibulum semper, velit et tincidunt vehicula, nisl risus eleifend ipsum, vel consectetuer enim dolor id magna. Praesent hendrerit urna ac lacus. Maecenas porttitor ipsum sed orci. In ac odio vel lorem tincidunt pellentesque. Nam tempor pulvinar turpis. Nunc in leo in libero ultricies interdum. Proin ut urna. Donec ultricies nunc dapibus justo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent vulputate, lectus pulvinar nonummy eleifend, sapien urna posuere metus, vel auctor risus odio eu augue. Cras vitae dolor. Phasellus dolor. Etiam enim. Donec erat felis, tincidunt quis, luctus in, faucibus at, est.</p>
<div class="modal hidden">
Hi there!
<button onclick="closeModal(event);">Close me</button>
<p>Morbi gravida posuere est. Fusce id augue. Sed facilisis, felis quis ornare consequat, neque risus faucibus dui, quis ullamcorper tellus lacus vitae felis. Phasellus ac dolor. Integer ante diam, consectetuer in, tempor vitae, volutpat in, enim. Integer diam felis, semper at, iaculis ut, suscipit quis, dolor. Vestibulum semper, velit et tincidunt vehicula, nisl risus eleifend ipsum, vel consectetuer enim dolor id magna. Praesent hendrerit urna ac lacus. Maecenas porttitor ipsum sed orci. In ac odio vel lorem tincidunt pellentesque. Nam tempor pulvinar turpis. Nunc in leo in libero ultricies interdum. Proin ut urna. Donec ultricies nunc dapibus justo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent vulputate, lectus pulvinar nonummy eleifend, sapien urna posuere metus, vel auctor risus odio eu augue. Cras vitae dolor. Phasellus dolor. Etiam enim. Donec erat felis, tincidunt quis, luctus in, faucibus at, est.</p>
</div>
</div>

6
Justus Romijn

Nous avons fait face à ce problème - et l'avons finalement résolu en utilisant:

https://github.com/lazd/iNoBounce

Dès que le script a été chargé, nous avons dû appeler iNoBounce.disable() lorsqu’il était activé au démarrage, empêchant ainsi tout comportement de défilement.

3
Stuart Johnston

La meilleure solution que j'ai trouvée, qui empêche également le défilement en arrière-plan lorsque vous faites défiler le haut ou le bas de la superposition (élément fixe) à l'aide de javascript dans Vanilla:

// "fixed-element" is the class of the overlay (fixed element) what has "position: fixed"
// Call disableScroll() and enableScroll() to toggle

var freeze = function(e) {
  if (!document.getElementsByClassName("fixed-element")[0].contains(e.target)) {
    e.preventDefault();
  }
}

var disableScroll = function() {
  document.body.style.overflow = "hidden"; // Or toggle using class: document.body.className += "overflow-hidden-class";

  // Only accept touchmove from fixed-element
  document.addEventListener('touchmove', freeze, false);

  // Prevent background scrolling
  document.getElementsByClassName("fixed-element")[0].addEventListener("touchmove", function(e) {
    var top = this.scrollTop,
      totalScroll = this.scrollHeight,
      currentScroll = top + this.offsetHeight;

    if (top === 0 && currentScroll === totalScroll) {
      e.preventDefault();
    } else if (top === 0) {
      this.scrollTop = 1;
    } else if (currentScroll === totalScroll) {
      this.scrollTop = top - 1;
    }
  });
}

var enableScroll = function() {
  document.removeEventListener("touchmove", freeze);
  document.body.style.overflow = "";
}

Avantages:
1. Ne fait pas du corps "fixe" lors de la superposition ouverte (élément fixe), donc la page ne défile pas vers le haut.
2. Empêche le défilement en arrière-plan avec l'élément fixe.

Voir Gist

2
Tony Tang

il semble que iOS ne fera défiler le corps que lorsque la superposition atteint le défilement min ou max. Donc, définissez le scrollTop de la superposition sur 1 au lieu de zéro et détectez l'événement onscroll (qui est déclenché sur iOS après la fin du défilement) et si au maximum (app.scrollHeight - app.scrollTop - app.clientHeight <1), définissez-le. à un pixel plus court. Par exemple

    var overlay = document.getElementById('overlay');

    function onScroll() {
        if (overlay.scrollTop < 1) {
            overlay.scrollTop = 1;
        } else if (overlay.scrollHeight - overlay.scrollTop - overlay.clientHeight < 1)                         {
            overlay.scrollTop = overlay.scrollTop - 1;
        }
    }


    overlay.addEventListener('scroll', onScroll);

Vous voudrez peut-être ajouter une vérification et attacher uniquement l'événement s'il est exécuté dans iOS. 

1
user3025020

J'ai trouvé cette question tout en cherchant une solution à mon problème très similaire. Peut-être que la solution que j'ai trouvée à la mienne apportera un peu de lumière ici.

Dans mon cas, le problème était de savoir comment empêcher un conteneur défilable de défiler lors de l'utilisation d'un widget défilable dans ce conteneur (par exemple, un curseur HTML5 pivoté verticalement à l'aide de la transformation css). Le conteneur à défilement est défini comme "overflow-y: scroll" à l'aide d'un sélecteur d'identifiant en CSS.

J'ai d'abord essayé d'utiliser une classe .scroll-lock qui avait 'overflow-y: hidden' et je l'ai activée/désactivée sur le conteneur à défilement avec les écouteurs touchstart et touchend sur tous les widgets défilables. Ça n'a pas marché. Les tentatives d'utilisation des widgets ont entraîné le défilement des conteneurs. Ensuite, j'ai essayé toutes sortes de solutions javascript jusqu'à ce que je les trouve ici et que je m'approche de la solution. 

Dans mon cas, le problème était les règles de spécificité. Les sélecteurs de classe sont ignorés par les sélecteurs id. Par conséquent, ma classe basculée ne peut pas remplacer le paramètre de dépassement de capacité par défaut sur le conteneur appliqué par id. Quand j'ai appliqué 'overflow: hidden' en utilisant l'attribut style directement sur le conteneur div, tout a bien fonctionné.

const $scrollableWidget = $('.vertical-slider-container');
$scrollableWidget.on('touchstart',function (e) {
    console.log("Disabling scroll on content area");
    $(`#content`).css("overflow-y", "hidden");
});
$scrollableWidget.on('touchend',function (e) {
    console.log("Re-enabling scroll on content area");
    $(`#content`).removeAttr("style");
});

Pour plus de détails, voir ce violon. (Utilisez un navigateur mobile pour l'essayer.)

https://jsfiddle.net/daffinm/fwgnm7hs/12/

J'espère que ça aide quelqu'un. (J'ai passé beaucoup trop de temps à résoudre ce problème.)

0
daffinm