Safari en ios8 está desplazando la pantalla cuando los elementos fijos se enfocan

96

En IOS8 Safari hay un nuevo error con la posición corregida.

Si enfoca un área de texto que está en un panel fijo, safari lo desplazará hasta la parte inferior de la página.

Esto hace que sea imposible trabajar con todo tipo de IU, ya que no tiene forma de ingresar texto en áreas de texto sin desplazarse por la página hacia abajo y perder su lugar.

¿Hay alguna forma de solucionar este error de forma limpia?

#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>
Sam Saffron
fuente
1
¿Sería útil establecer un índice Z en #b?
Ben
1
z index no es de ayuda, tal vez alguna transformación elegante sin op css sería mucho con contextos de pila, no estoy seguro.
Sam Saffron
1
para el contexto aquí está la discusión sobre Discourse: meta.discourse.org/t/dealing-with-ios-8-ipad-mobile-safari-bugs/…
Sam Saffron
79
iOS Safari es el nuevo IE
geedubb
4
@geedubb estuvo de acuerdo. cualquier sistema operativo idiota que vincule su versión predeterminada del navegador al sistema operativo se verá afectado por los problemas que han plagado a IE durante los últimos 7 años.
rocío

Respuestas:

58

Basado en este buen análisis de este problema, he usado esto en htmly bodyelementos en css:

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

Creo que me está funcionando muy bien.

Mohammad AlBanna
fuente
2
funcionó para mí también. Esto arruinó muchas otras cosas ya que estoy manipulando mi DOM en carga, así que lo convierto en una clase y lo agrego al html, cuerpo después de que el DOM se ha estabilizado. Cosas como scrollTop no funcionan muy bien (estoy haciendo desplazamiento automático), pero nuevamente, puedes agregar / eliminar la clase mientras realizas operaciones de desplazamiento. Sin embargo, mal trabajo por parte del equipo de Safari.
Amarsh
1
Las personas que buscan esta opción también pueden querer probar transform: translateZ(0);desde stackoverflow.com/questions/7808110/…
lkraav
1
Esto resuelve el problema, pero si tiene animaciones, se verán muy entrecortadas. Podría ser mejor envolverlo en una consulta de medios.
mmla
Me funcionó en iOS 10.3.
quotesBro
No resuelve el problema.
Debe
36

La mejor solución que se me ocurrió es cambiar al uso position: absolute;de enfoque y calcular la posición en la que estaba cuando se estaba usando position: fixed;. El truco es que el focusevento se activa demasiado tarde, por lo que touchstartdebe utilizarse.

La solución en esta respuesta imita muy de cerca el comportamiento correcto que tuvimos en iOS 7.

Requisitos:

El bodyelemento debe tener un posicionamiento para asegurar un posicionamiento adecuado cuando el elemento cambia a posicionamiento absoluto.

body {
    position: relative;
}

El código ( ejemplo en vivo ):

El siguiente código es un ejemplo básico para el caso de prueba proporcionado y se puede adaptar para su caso de uso específico.

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

Aquí está el mismo código sin el truco para comparar .

Consideración:

Si el position: fixed;elemento tiene cualquier otro elemento padre con posicionamiento ademásbody , cambiar a position: absolute;puede tener un comportamiento inesperado. Debido a la naturaleza deposition: fixed; esto, probablemente no sea un problema importante, ya que anidar dichos elementos no es común.

Recomendaciones:

Si bien el uso del touchstartevento filtrará la mayoría de los entornos de escritorio, es probable que desee utilizar el rastreo de agentes de usuario para que este código solo se ejecute para el iOS 8 roto, y no para otros dispositivos como Android y versiones anteriores de iOS. Desafortunadamente, todavía no sabemos cuándo Apple solucionará este problema en iOS, pero me sorprendería que no se solucione en la próxima versión principal.

Alexander O'Mara
fuente
Me pregunto si la doble envoltura con un div y la configuración de la altura en% 100 en el div de envoltura transparente podría engañarlo para evitar esto ...
Sam Saffron
@SamSaffron ¿Podría aclarar cómo podría funcionar esta técnica? Probé algunas cosas como esta sin éxito. Dado que la altura del documento es ambigua, no estoy seguro de cómo podría funcionar.
Alexander O'Mara
Estaba pensando que simplemente tener una envoltura "fija" al 100% de altura podría solucionar esto, posiblemente no
Sam Saffron
@downvoter: ¿Me equivoqué en algo? Estoy de acuerdo en que es una solución terrible, pero no creo que haya ninguna mejor.
Alexander O'Mara
4
Esto no funcionó para mí, el campo de entrada aún se mueve.
Rodrigo Ruiz
8

¡Encontré un método que funciona sin la necesidad de cambiar a la posición absoluta!

Código completo sin comentarios

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

Rompiéndolo

Primero, debe tener el campo de entrada fijo hacia la parte superior de la página en el HTML (es un elemento fijo, por lo que debería tener sentido semánticamente tenerlo cerca de la parte superior de todos modos):

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

Luego, debe guardar la posición de desplazamiento actual en 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;

Entonces necesita una forma de detectar dispositivos iOS para que no afecte a las cosas que no necesitan la corrección (función tomada 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;
}

Ahora que tenemos todo lo que necesitamos, aquí está la solución :)

//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);
    }
});
Daniel Tonon
fuente
+1. Esta es una solución significativamente menos complicada que la respuesta aceptada, si tiene una jerarquía DOM no trivial. Esto debería tener más votos positivos
Anson Kao
¿Podría proporcionar esto también en JS nativo? ¡Muchas gracias!
mesqueeb
@SamSaffron, ¿esta respuesta realmente funcionó para ti? ¿Puedo tener un ejemplo aquí? no funcionó para mí?
Ganesh Putta
@ SamSaffron, ¿esta respuesta realmente resolvió su problema? ¿Puede enviar algún ejemplo que funcione para usted? Estoy trabajando en lo mismo, pero no funcionó para mí.
Ganesh Putta
@GaneshPutta Es posible que una actualización más reciente de iOS haya hecho que esto ya no funcione. Publiqué esto hace 2.5 años. Sin embargo, aún debería funcionar si siguió todas las instrucciones exactamente: /
Daniel Tonon
4

Pude solucionar esto para las entradas seleccionadas agregando un detector de eventos a los elementos seleccionados necesarios, luego desplazándome por un desplazamiento de un píxel cuando la selección en cuestión gana el foco.

Esta no es necesariamente una buena solución, pero es mucho más simple y confiable que las otras respuestas que he visto aquí. El navegador parece volver a renderizar / volver a calcular la posición: fija; atributo basado en el desplazamiento proporcionado en la función window.scrollBy ().

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

Al igual que Mark Ryan Sallee sugirió, descubrí que cambiar dinámicamente la altura y el desbordamiento de mi elemento de fondo es la clave; esto no le da a Safari nada para desplazarse.

Entonces, después de que finalice la animación de apertura del modal, cambie el estilo del fondo:

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

Cuando cierras el modal, cámbialo de nuevo:

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

Si bien otras respuestas son útiles en contextos más simples, mi DOM era demasiado complicado (gracias a SharePoint) para usar el intercambio de posición absoluta / fija.

Matthew Levy
fuente
1

¿Limpiamente? No.

Recientemente tuve este problema con un campo de búsqueda fijo en un encabezado fijo, lo mejor que puede hacer en este momento es mantener la posición de desplazamiento en una variable en todo momento y, al realizar la selección, hacer que la posición del elemento fijo sea absoluta en lugar de fija con una parte superior posición basada en la posición de desplazamiento del documento.

Sin embargo, esto es muy feo y todavía resulta en un extraño desplazamiento hacia adelante y hacia atrás antes de aterrizar en el lugar correcto, pero es lo más cercano que pude conseguir.

Cualquier otra solución implicaría anular la mecánica de desplazamiento predeterminada del navegador.

Samuel
fuente
1

¡Esto ahora está arreglado en iOS 10.3!

Los hacks ya no deberían ser necesarios.

Sam Saffron
fuente
1
¿Puede señalar alguna nota de la versión que indique que esto se ha solucionado?
bluepnume
Apple es muy reservado, cerraron mi informe de error. Confirmé que ahora funciona correctamente, eso es todo lo que tengo :)
Sam Saffron
Todavía tengo este problema en iOS 11
zekia
0

No me he ocupado de este error en particular, pero tal vez haya puesto un overflow: hidden; en el cuerpo cuando el área de texto está visible (o simplemente activa, según su diseño). Esto puede tener el efecto de no hacer que el navegador esté "hacia abajo" para desplazarse.

Mark Ryan Sallee
fuente
1
Parece que ni siquiera puedo hacer que el inicio táctil se active lo suficientemente temprano como para siquiera considerar ese truco :(
Sam Saffron
0

Una posible solución sería reemplazar el campo de entrada.

  • Supervisar eventos de clic en un div
  • enfocar un campo de entrada oculto para representar el teclado
  • replicar el contenido del campo de entrada oculto en el campo de entrada falso

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

Davidcondrey
fuente
0

Ninguna de estas soluciones funcionó para mí porque mi DOM es complicado y tengo páginas de desplazamiento infinitas dinámicas, así que tuve que crear la mía propia.

Fondo: estoy usando un encabezado fijo y un elemento más abajo que se pega debajo de él una vez que el usuario se desplaza hacia abajo. Este elemento tiene un campo de entrada de búsqueda. Además, tengo páginas dinámicas agregadas durante el desplazamiento hacia adelante y hacia atrás.

Problema: en iOS, cada vez que el usuario hacía clic en la entrada en el elemento fijo, el navegador se desplazaba hasta la parte superior de la página. Esto no solo provocó un comportamiento no deseado, sino que también provocó que mi página dinámica se agregara en la parte superior de la página.

Solución esperada: sin desplazamiento en iOS (ninguno) cuando el usuario hace clic en la entrada en el elemento adhesivo.

Solución:

     /*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));

Requisitos: JQuery mobile es necesario para que funcionen las funciones startsroll y stopscroll.

Se incluye antirrebote para suavizar cualquier retraso creado por el elemento adhesivo.

Probado en iOS10.

Dima
fuente
0

Ayer salté algo como esto estableciendo la altura de #a a la altura máxima visible (la altura del cuerpo era en mi caso) cuando #b es visible

ex:

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

ps: perdón por el ejemplo tardío, solo noté que era necesario.

Onur Uyar
fuente
14
Incluya un ejemplo de código para dejar en claro cómo su corrección puede ayudar
roo2
@EruPenkman lo siento, acabo de notar tu comentario, espero que te ayude.
Onur Uyar
0

Tuve el problema, las siguientes líneas de código lo resolvieron por mí:

html{

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

}
Manoj Gorasya
fuente