O Safari no ios8 está rolando a tela quando os elementos fixos recebem o foco

96

No IOS8 Safari, há um novo bug com a posição corrigida.

Se você focar em uma área de texto que está em um painel fixo, o safari irá rolar você para o final da página.

Isso torna impossível trabalhar com todos os tipos de IUs, já que você não tem como inserir texto em áreas de texto sem rolar sua página totalmente para baixo e perder seu lugar.

Existe alguma maneira de contornar esse bug de forma limpa?

#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
fonte
1
Definir um z-index em #b ajudaria?
Ben
1
O índice z não ajuda em nada, talvez alguma transformação extravagante no op css faria muito com contextos de pilha, não tenho certeza.
Sam Saffron
1
para o contexto, aqui está a discussão sobre Discourse: meta.discourse.org/t/dealing-with-ios-8-ipad-mobile-safari-bugs/…
Sam Saffron
79
Safari para iOS é o novo IE
geedubb
4
@geedubb concordou. qualquer sistema operacional idiota que amarra sua versão de navegador padrão ao sistema operacional vai cair em conflito com os problemas que têm atormentado o IE nos últimos 7 anos.
dewd

Respostas:

58

Com base nessa boa análise desse problema, usei os elementos htmle bodyem css:

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

Acho que está funcionando muito bem para mim.

Mohammad AlBanna
fonte
2
funcionou para mim também. Isso atrapalhou muitas outras coisas, já que estou manipulando meu DOM no carregamento, então transformo isso em uma classe e adicionei ao html, corpo depois que o DOM se estabilizou. Coisas como scrollTop não funcionam muito bem (estou fazendo rolagem automática), mas, novamente, você pode adicionar / remover a classe ao fazer operações de rolagem. No entanto, mau trabalho por parte da equipe do Safari.
Amarsh
1
As pessoas que olham para essa opção também podem querer testar transform: translateZ(0);em stackoverflow.com/questions/7808110/…
lkraav
1
Isso resolve o problema, mas se você tiver animações, elas ficarão muito instáveis. Pode ser melhor envolvê-lo em uma consulta de mídia.
mmla
Funcionou para mim no iOS 10.3.
citaçõesBro de
Não resolve o problema. Você precisa interceptar a rolagem quando o teclado virtual é
exibido
36

A melhor solução que eu poderia encontrar é mudar para o uso position: absolute;de foco e calcular a posição em que estava quando estava usando position: fixed;. O truque é que o focusevento dispara tarde demais, então touchstartdeve ser usado.

A solução nesta resposta imita o comportamento correto que tínhamos no iOS 7 muito de perto.

Requisitos:

O bodyelemento deve ter posicionamento para garantir o posicionamento correto quando o elemento muda para o posicionamento absoluto.

body {
    position: relative;
}

O código ( exemplo ao vivo ):

O código a seguir é um exemplo básico para o caso de teste fornecido e pode ser adaptado para seu 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);
});

Aqui está o mesmo código sem o hack para comparação .

Embargo:

Se o position: fixed;elemento tiver qualquer outro elemento pai com posicionamento além disso body, alternar para position: absolute;pode ter um comportamento inesperado. Devido à natureza position: fixed;disso, provavelmente não é um problema importante, uma vez que o aninhamento de tais elementos não é comum.

Recomendações:

Embora o uso do touchstartevento filtre a maioria dos ambientes de desktop, você provavelmente desejará usar o sniffing de agente de usuário para que este código seja executado apenas no iOS 8 quebrado, e não em outros dispositivos, como Android e versões mais antigas do iOS. Infelizmente, ainda não sabemos quando a Apple irá corrigir esse problema no iOS, mas eu ficaria surpreso se não fosse corrigido na próxima versão principal.

Alexander O'Mara
fonte
Eu me pergunto se o envolvimento duplo com um div e definir a altura em% 100 no div de envolvimento transparente poderia enganá-lo para evitar isso ...
Sam Saffron
@SamSaffron Você poderia esclarecer como essa técnica pode funcionar? Tentei algumas coisas como essa sem sucesso. Como a altura do documento é ambígua, não tenho certeza de como isso funcionaria.
Alexander O'Mara
Eu estava pensando que simplesmente ter um wrapper "fixo" de 100% de altura pode resolver isso, possivelmente não
Sam Saffron
@downvoter: Eu entendi algo errado? Concordo que é uma solução terrível, mas não acho que haja outra melhor.
Alexander O'Mara
4
Isso não funcionou para mim, o campo de entrada ainda se move.
Rodrigo Ruiz
8

Encontrei um método que funciona sem a necessidade de mudar para a posição absoluta!

Código totalmente não comentado

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

Quebrando isso

Primeiro, você precisa ter o campo de entrada fixo no topo da página no HTML (é um elemento fixo, portanto, semanticamente deve fazer sentido tê-lo próximo ao topo de qualquer maneira):

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

Então você precisa salvar a posição de rolagem atual em variáveis ​​globais:

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

Então você precisa encontrar uma maneira de detectar dispositivos iOS para que não afete coisas que não precisam de correção (função retirada 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;
}

Agora que temos tudo de que precisamos, aqui está a correção :)

//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
fonte
+1. Esta é uma correção significativamente menos complicada do que a resposta aceita, se você tiver uma hierarquia DOM não trivial. Isso deve ter mais votos positivos
Anson Kao
Você poderia fornecer isso em JS nativo também? Muito obrigado!
mesqueeb
@ SamSaffron, essa resposta realmente funcionou para você? posso ter algum exemplo aqui. não funcionou para mim?
Ganesh Putta
@ SamSaffron, esta resposta realmente resolveu seu problema, pode enviar algum exemplo que funcionou para você, estou trabalhando no mesmo, mas não funcionou para mim.
Ganesh Putta
@GaneshPutta É possível que uma atualização mais recente do iOS tenha feito isso não funcionar mais. Eu postei isso há 2,5 anos. Ainda deve funcionar se você seguiu todas as instruções exatamente: /
Daniel Tonon
4

Consegui consertar isso para entradas selecionadas adicionando um ouvinte de evento aos elementos de seleção necessários e, em seguida, rolando por um deslocamento de um pixel quando a seleção em questão ganha o foco.

Esta não é necessariamente uma boa solução, mas é muito mais simples e confiável do que as outras respostas que vi aqui. O navegador parece renderizar / recalcular a posição: fixa; atributo baseado no deslocamento fornecido na função window.scrollBy ().

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

Assim como Mark Ryan Sallee sugeriu, descobri que alterar dinamicamente a altura e o estouro do meu elemento de fundo é a chave - isso não dá ao Safari nada para percorrer.

Assim, após o término da animação de abertura do modal, altere o estilo do fundo:

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

Ao fechar o modal, altere-o de volta:

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

Embora outras respostas sejam úteis em contextos mais simples, meu DOM era muito complicado (graças ao SharePoint) para usar a troca de posição absoluta / fixa.

Matthew Levy
fonte
1

De forma limpa? não.

Recentemente, tive esse problema com um campo de pesquisa fixo em um cabeçalho fixo, o melhor que você pode fazer no momento é manter a posição de rolagem em uma variável o tempo todo e, após a seleção, tornar a posição do elemento fixo absoluta em vez de fixa com um topo posição com base na posição de rolagem do documento.

No entanto, isso é muito feio e ainda resulta em algumas rolagens estranhas para frente e para trás antes de pousar no lugar certo, mas é o mais próximo que pude chegar.

Qualquer outra solução envolveria substituir a mecânica de rolagem padrão do navegador.

Samuel
fonte
1

Isso agora está corrigido no iOS 10.3!

Hacks não devem mais ser necessários.

Sam Saffron
fonte
1
Você pode apontar as notas de lançamento que apontam que isso foi corrigido?
bluepnume
A Apple é muito sigilosa, eles fecharam meu relatório de bug Eu confirmei que agora funciona bem, isso é tudo que eu tenho :)
Sam Saffron
Ainda tenho esse problema no iOS 11
zekia
0

Não lidei com esse bug em particular, mas talvez coloque um estouro: oculto; no corpo quando a área de texto está visível (ou apenas ativa, dependendo do seu design). Isso pode ter o efeito de não deixar o navegador "para baixo" para onde rolar.

Mark Ryan Sallee
fonte
1
Eu não posso sequer parecem ter touchstart para gatilho cedo o suficiente para sequer considerar que corte :(
Sam Saffron
0

Uma possível solução seria substituir o campo de entrada.

  • Monitore eventos de clique em um div
  • focalize um campo de entrada oculto para renderizar o teclado
  • replicar o conteúdo do campo de entrada oculto no 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
fonte
0

Nenhuma dessas soluções funcionou para mim porque meu DOM é complicado e eu tenho páginas dinâmicas de rolagem infinita, então tive que criar minhas próprias.

Plano de fundo: estou usando um cabeçalho fixo e um elemento mais abaixo que fica abaixo dele quando o usuário rola para baixo. Este elemento possui um campo de entrada de pesquisa. Além disso, adicionei páginas dinâmicas durante a rolagem para frente e para trás.

Problema: No iOS, sempre que o usuário clicava na entrada no elemento fixo, o navegador rolava até o topo da página. Isso não apenas causou um comportamento indesejado, mas também acionou minha adição dinâmica de página no topo da página.

Solução esperada: nenhuma rolagem no iOS (nenhuma) quando o usuário clica na entrada no elemento aderente.

Solução:

     /*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 é necessário para que as funções startsroll e stopscroll funcionem.

Debounce está incluído para suavizar qualquer lag criado pelo elemento aderente.

Testado em iOS10.

Dima
fonte
0

Acabei de pular sobre algo assim ontem, definindo a altura de #a para a altura máxima visível (a altura do corpo era no meu caso) quando #b está visível

ex:

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

ps: desculpe pelo último exemplo, só percebi que era necessário.

Onur Uyar
fonte
14
Inclua um exemplo de código para deixar claro como sua correção pode ajudar
roo2
@EruPenkman, desculpe, acabei de notar seu comentário, espero que ajude.
Onur Uyar
0

Tive o problema, as linhas de código abaixo resolveram para mim -

html{

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

}
Manoj Gorasya
fonte