Safari dans ios8 fait défiler l'écran lorsque les éléments fixes sont mis au point


96

Dans IOS8 Safari, il y a un nouveau bogue avec la position corrigée.

Si vous concentrez une zone de texte qui se trouve dans un panneau fixe, safari vous fera défiler vers le bas de la page.

Cela rend toutes sortes d'interfaces utilisateur impossibles à utiliser, car vous n'avez aucun moyen de saisir du texte dans des zones de texte sans faire défiler votre page jusqu'en bas et perdre votre place.

Existe-t-il un moyen de contourner proprement ce bogue?

#a {
  height: 10000px;
  background: linear-gradient(red, blue);
}
#b {
  position: fixed;
  bottom: 20px;
  left: 10%;
  width: 100%;
  height: 300px;
}

textarea {
   width: 80%;
   height: 300px;
}
<html>
   <body>
   <div id="a"></div>
   <div id="b"><textarea></textarea></div>
   </body>
</html>

1
La définition d'un z-index sur #b aiderait-elle?
Ben

1
z index n'est d'aucune aide, peut-être que certaines transformations css sans op feraient beaucoup avec des contextes de pile, pas sûr.
Sam Saffron

1
pour le contexte, voici la discussion sur le discours: meta.discourse.org/t/dealing-with-ios-8-ipad-mobile-safari-bugs
Sam Saffron

79
Safari iOS est le nouvel IE
geedubb

4
@geedubb était d'accord. tout système d'exploitation idiot qui lie sa version de navigateur par défaut au système d'exploitation va tomber sous le coup des problèmes qui affligent IE depuis 7 ans.
dewd le

Réponses:


58

Sur la base de cette bonne analyse de ce problème, j'ai utilisé ceci dans htmlet des bodyéléments en css:

html,body{
    -webkit-overflow-scrolling : touch !important;
    overflow: auto !important;
    height: 100% !important;
}

Je pense que cela fonctionne très bien pour moi.


2
a fonctionné pour moi aussi. Cela a gâché beaucoup d'autres choses depuis que je manipule mon DOM en charge, alors j'en fais une classe, et je l'ai ajouté au corps html après la stabilisation du DOM. Des choses comme scrollTop ne fonctionnent pas très bien (je fais le défilement automatique), mais encore une fois, vous pouvez ajouter / supprimer la classe tout en faisant des opérations de défilement. Mauvais travail de la part de l'équipe Safari.
Amarsh le

1
Les personnes qui recherchent cette option peuvent également vouloir tester à transform: translateZ(0);partir de stackoverflow.com/questions/7808110/…
lkraav

1
Cela résout le problème, mais si vous avez des animations, elles auront l'air très saccadées. Il vaudrait peut-être mieux l'envelopper dans une requête multimédia.
mmla

A travaillé pour moi sur iOS 10.3.
quotesBro

Cela ne résout pas le problème. Vous devez intercepter le défilement lorsque le clavier virtuel apparaît et changer la hauteur à une valeur spécifique: stackoverflow.com/a/46044341/84661
Brian Cannard

36

La meilleure solution que je pourrais trouver est de passer à l'utilisation position: absolute;de la mise au point et de calculer la position à laquelle il se trouvait lorsqu'il était utilisé position: fixed;. L'astuce est que l' focusévénement se déclenche trop tard, alorstouchstart doit être utilisé.

La solution dans cette réponse imite de très près le comportement correct que nous avions dans iOS 7.

Exigences:

le body élément doit avoir un positionnement afin d'assurer un bon positionnement lorsque l'élément passe en positionnement absolu.

body {
    position: relative;
}

Le code ( exemple en direct ):

Le code suivant est un exemple de base pour le cas de test fourni et peut être adapté à votre cas d'utilisation spécifique.

//Get the fixed element, and the input element it contains.
var fixed_el = document.getElementById('b');
var input_el = document.querySelector('textarea');
//Listen for touchstart, focus will fire too late.
input_el.addEventListener('touchstart', function() {
    //If using a non-px value, you will have to get clever, or just use 0 and live with the temporary jump.
    var bottom = parseFloat(window.getComputedStyle(fixed_el).bottom);
    //Switch to position absolute.
    fixed_el.style.position = 'absolute';
    fixed_el.style.bottom = (document.height - (window.scrollY + window.innerHeight) + bottom) + 'px';
    //Switch back when focus is lost.
    function blured() {
        fixed_el.style.position = '';
        fixed_el.style.bottom = '';
        input_el.removeEventListener('blur', blured);
    }
    input_el.addEventListener('blur', blured);
});

Voici le même code sans le hack pour comparaison .

Caveat:

Si la position: fixed; élément a d'autres éléments parents avec un positionnement en plus body, le passage à position: absolute;peut avoir un comportement inattendu. En raison de la nature de position: fixed;cela, ce n'est probablement pas un problème majeur, car l'imbrication de tels éléments n'est pas courante.

Recommandations:

Bien que l'utilisation de l' touchstartévénement filtrera la plupart des environnements de bureau, vous souhaiterez probablement utiliser le reniflage de l'agent utilisateur afin que ce code ne s'exécute que pour l'iOS 8 cassé, et non pour d'autres appareils tels qu'Android et les anciennes versions d'iOS. Malheureusement, nous ne savons pas encore quand Apple résoudra ce problème dans iOS, mais je serais surpris qu'il ne soit pas résolu dans la prochaine version majeure.


Je me demande si le double emballage avec un div et le réglage de la hauteur à 100% sur un div d'emballage transparent pourrait le tromper en évitant cela ...
Sam Saffron

@SamSaffron Pourriez-vous clarifier comment une telle technique pourrait fonctionner? J'ai essayé quelques trucs comme ça sans succès. Comme la hauteur du document est ambiguë, je ne sais pas comment cela pourrait fonctionner.
Alexander O'Mara

Je pensais simplement avoir une enveloppe "fixe" à 100% de hauteur peut contourner ce problème, peut-être pas
Sam Saffron

@downvoter: Est-ce que je me suis trompé? Je conviens que c'est une solution terrible, mais je ne pense pas qu'il y en ait de meilleure.
Alexander O'Mara le

4
Cela n'a pas fonctionné pour moi, le champ de saisie bouge toujours.
Rodrigo Ruiz

8

J'ai trouvé une méthode qui fonctionne sans avoir besoin de changer de position absolue!

Code complet non commenté

var scrollPos = $(document).scrollTop();
$(window).scroll(function(){
    scrollPos = $(document).scrollTop();
});
var savedScrollPos = scrollPos;

function is_iOS() {
  var iDevices = [
    'iPad Simulator',
    'iPhone Simulator',
    'iPod Simulator',
    'iPad',
    'iPhone',
    'iPod'
  ];
  while (iDevices.length) {
    if (navigator.platform === iDevices.pop()){ return true; }
  }
  return false;
}

$('input[type=text]').on('touchstart', function(){
    if (is_iOS()){
        savedScrollPos = scrollPos;
        $('body').css({
            position: 'relative',
            top: -scrollPos
        });
        $('html').css('overflow','hidden');
    }
})
.blur(function(){
    if (is_iOS()){
        $('body, html').removeAttr('style');
        $(document).scrollTop(savedScrollPos);
    }
});

Le décomposer

Vous devez d'abord avoir le champ de saisie fixe vers le haut de la page dans le HTML (c'est un élément fixe, il devrait donc être sémantiquement logique de l'avoir près du haut de toute façon):

<!DOCTYPE HTML>

<html>

    <head>
      <title>Untitled</title>
    </head>

    <body>
        <form class="fixed-element">
            <input class="thing-causing-the-issue" type="text" />
        </form>

        <div class="everything-else">(content)</div>

    </body>

</html>

Ensuite, vous devez enregistrer la position de défilement actuelle dans des variables globales:

//Always know the current scroll position
var scrollPos = $(document).scrollTop();
$(window).scroll(function(){
    scrollPos = $(document).scrollTop();
});

//need to be able to save current scroll pos while keeping actual scroll pos up to date
var savedScrollPos = scrollPos;

Ensuite, vous avez besoin d'un moyen de détecter les appareils iOS afin que cela n'affecte pas les choses qui n'ont pas besoin du correctif (fonction tirée de https://stackoverflow.com/a/9039885/1611058 )

//function for testing if it is an iOS device
function is_iOS() {
  var iDevices = [
    'iPad Simulator',
    'iPhone Simulator',
    'iPod Simulator',
    'iPad',
    'iPhone',
    'iPod'
  ];

  while (iDevices.length) {
    if (navigator.platform === iDevices.pop()){ return true; }
  }

  return false;
}

Maintenant que nous avons tout ce dont nous avons besoin, voici le correctif :)

//when user touches the input
$('input[type=text]').on('touchstart', function(){

    //only fire code if it's an iOS device
    if (is_iOS()){

        //set savedScrollPos to the current scroll position
        savedScrollPos = scrollPos;

        //shift the body up a number of pixels equal to the current scroll position
        $('body').css({
            position: 'relative',
            top: -scrollPos
        });

        //Hide all content outside of the top of the visible area
        //this essentially chops off the body at the position you are scrolled to so the browser can't scroll up any higher
        $('html').css('overflow','hidden');
    }
})

//when the user is done and removes focus from the input field
.blur(function(){

    //checks if it is an iOS device
    if (is_iOS()){

        //Removes the custom styling from the body and html attribute
        $('body, html').removeAttr('style');

        //instantly scrolls the page back down to where you were when you clicked on input field
        $(document).scrollTop(savedScrollPos);
    }
});

+1. C'est une solution nettement moins compliquée que la réponse acceptée, si vous avez une hiérarchie DOM non triviale. Cela devrait avoir plus de votes positifs
Anson Kao

Pourriez-vous également fournir cela en JS natif? Merci beaucoup!
mesqueeb

@SamSaffron, cette réponse a-t-elle vraiment fonctionné pour vous? puis-je avoir un exemple ici. ça a marché pour moi?
Ganesh Putta

@ SamSaffron, est-ce que cette réponse a vraiment résolu votre problème, pouvez-vous envoyer un exemple qui a fonctionné pour vous, je travaille sur le même, mais cela a fonctionné pour moi.
Ganesh Putta

@GaneshPutta Il est possible qu'une mise à jour iOS plus récente ait empêché cela de fonctionner. J'ai posté ceci il y a 2,5 ans. Cela devrait quand même fonctionner si vous avez suivi toutes les instructions à la lettre: /
Daniel Tonon

4

J'ai pu résoudre ce problème pour les entrées sélectionnées en ajoutant un écouteur d'événement aux éléments de sélection nécessaires, puis en faisant défiler d'un décalage d'un pixel lorsque la sélection en question gagne le focus.

Ce n'est pas nécessairement une bonne solution, mais c'est beaucoup plus simple et plus fiable que les autres réponses que j'ai vues ici. Le navigateur semble re-rendre / recalculer la position: fixe; attribut basé sur le décalage fourni dans la fonction window.scrollBy ().

document.querySelector(".someSelect select").on("focus", function() {window.scrollBy(0, 1)});

2

Tout comme Mark Ryan Sallee l'a suggéré, j'ai trouvé que changer dynamiquement la hauteur et le débordement de mon élément d'arrière - plan est la clé - cela ne donne à Safari rien sur quoi faire défiler.

Donc, une fois l'animation d'ouverture du modal terminée, changez le style de l'arrière-plan:

$('body > #your-background-element').css({
  'overflow': 'hidden',
  'height': 0
});

Lorsque vous fermez le modal, changez-le de nouveau:

$('body > #your-background-element').css({
  'overflow': 'auto',
  'height': 'auto'
});

Alors que d'autres réponses sont utiles dans des contextes plus simples, mon DOM était trop compliqué (grâce à SharePoint) pour utiliser l'échange de position absolue / fixe.


1

Propre? non.

J'ai récemment eu ce problème moi-même avec un champ de recherche fixe dans un en-tête collant, le mieux que vous puissiez faire pour le moment est de garder la position de défilement dans une variable à tout moment et, lors de la sélection, de rendre la position de l'élément fixe absolue au lieu de fixe avec un haut position basée sur la position de défilement du document.

C'est cependant très moche et entraîne toujours d'étranges allers-retours avant d'atterrir au bon endroit, mais c'est le plus proche que je puisse obtenir.

Toute autre solution impliquerait de remplacer les mécanismes de défilement par défaut du navigateur.


1

Ceci est maintenant corrigé dans iOS 10.3!

Les piratages ne devraient plus être nécessaires.


1
Pouvez-vous indiquer des notes de publication indiquant que cela est corrigé?
bluepnume

Apple est très secret, ils ont fermé mon rapport de bogue, j'ai confirmé que cela fonctionne maintenant correctement, c'est tout ce que j'ai :)
Sam Saffron

J'ai toujours ce problème sur iOS 11
zekia

0

Je n'ai pas traité de ce bogue particulier, mais peut-être mettre un débordement: hidden; sur le corps lorsque la zone de texte est visible (ou simplement active, selon votre conception). Cela peut avoir pour effet de ne pas donner au navigateur quelque part "en bas" pour faire défiler.


1
Je n'arrive même pas à obtenir le touchstart pour déclencher assez tôt pour même envisager ce hack :(
Sam Saffron

0

Une solution possible serait de remplacer le champ de saisie.

  • Surveiller les événements de clic sur un div
  • concentrer un champ de saisie masqué pour rendre le clavier
  • répliquer le contenu du champ de saisie masqué dans le faux champ de saisie

function focus() {
  $('#hiddeninput').focus();
}

$(document.body).load(focus);

$('.fakeinput').bind("click",function() {
    focus();
});

$("#hiddeninput").bind("keyup blur", function (){
  $('.fakeinput .placeholder').html(this.value);
});
#hiddeninput {
  position:fixed;
  top:0;left:-100vw;
  opacity:0;
  height:0px;
  width:0;
}
#hiddeninput:focus{
  outline:none;
}
.fakeinput {
  width:80vw;
  margin:15px auto;
  height:38px;
  border:1px solid #000;
  color:#000;
  font-size:18px;
  padding:12px 15px 10px;
  display:block;
  overflow:hidden;
}
.placeholder {
  opacity:0.6;
  vertical-align:middle;
}
<input type="text" id="hiddeninput"></input>

<div class="fakeinput">
    <span class="placeholder">First Name</span>
</div> 


codepen


0

Aucune de ces solutions n'a fonctionné pour moi car mon DOM est compliqué et j'ai des pages de défilement infinies dynamiques, j'ai donc dû créer les miennes.

Contexte: J'utilise un en-tête fixe et un élément plus bas qui reste en dessous une fois que l'utilisateur fait défiler aussi loin vers le bas. Cet élément a un champ d'entrée de recherche. De plus, j'ai des pages dynamiques ajoutées lors du défilement vers l'avant et vers l'arrière.

Problème: sous iOS, chaque fois que l'utilisateur cliquait sur l'entrée dans l'élément fixe, le navigateur défilait jusqu'en haut de la page. Cela a non seulement provoqué un comportement indésirable, mais également déclenché l'ajout de ma page dynamique en haut de la page.

Solution attendue: pas de défilement dans iOS (aucun du tout) lorsque l'utilisateur clique sur l'entrée dans l'élément collant.

Solution:

     /*Returns a function, that, as long as it continues to be invoked, will not
    be triggered. The function will be called after it stops being called for
    N milliseconds. If `immediate` is passed, trigger the function on the
    leading edge, instead of the trailing.*/
    function debounce(func, wait, immediate) {
        var timeout;
        return function () {
            var context = this, args = arguments;
            var later = function () {
                timeout = null;
                if (!immediate) func.apply(context, args);
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) func.apply(context, args);
        };
    };

     function is_iOS() {
        var iDevices = [
          'iPad Simulator',
          'iPhone Simulator',
          'iPod Simulator',
          'iPad',
          'iPhone',
          'iPod'
        ];
        while (iDevices.length) {
            if (navigator.platform === iDevices.pop()) { return true; }
        }
        return false;
    }

    $(document).on("scrollstop", debounce(function () {
        //console.log("Stopped scrolling!");
        if (is_iOS()) {
            var yScrollPos = $(document).scrollTop();
            if (yScrollPos > 200) { //200 here to offset my fixed header (50px) and top banner (150px)
                $('#searchBarDiv').css('position', 'absolute');
                $('#searchBarDiv').css('top', yScrollPos + 50 + 'px'); //50 for fixed header
            }
            else {
                $('#searchBarDiv').css('position', 'inherit');
            }
        }
    },250,true));

    $(document).on("scrollstart", debounce(function () {
        //console.log("Started scrolling!");
        if (is_iOS()) {
            var yScrollPos = $(document).scrollTop();
            if (yScrollPos > 200) { //200 here to offset my fixed header (50px) and top banner (150px)
                $('#searchBarDiv').css('position', 'fixed');
                $('#searchBarDiv').css('width', '100%');
                $('#searchBarDiv').css('top', '50px'); //50 for fixed header
            }
        }
    },250,true));

Exigences: JQuery mobile est requis pour que les fonctions startroll et stopscroll fonctionnent.

La fonction anti-rebond est incluse pour atténuer tout décalage créé par l'élément collant.

Testé sous iOS10.


0

Je viens de sauter par-dessus quelque chose comme ça hier en définissant la hauteur de #a sur la hauteur visible maximale (la hauteur du corps était dans mon cas) lorsque #b est visible

ex:

    <script>
    document.querySelector('#b').addEventListener('focus', function () {
      document.querySelector('#a').style.height = document.body.clientHeight;
    })
    </script>

ps: désolé pour l'exemple tardif, vient de remarquer qu'il était nécessaire.


14
Veuillez inclure un exemple de code pour expliquer clairement comment votre correctif peut aider
roo2

@EruPenkman désolé vient de remarquer votre commentaire, j'espère que cela vous aidera.
Onur Uyar

0

J'ai eu le problème, ci-dessous les lignes de code l'ont résolu pour moi -

html{

 overflow: scroll; 
-webkit-overflow-scrolling: touch;

}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.