Manter a posição de rolagem funciona apenas quando não está perto da parte inferior das mensagens div

10

Estou tentando imitar outros aplicativos de bate-papo para celular onde, quando você seleciona a send-messagecaixa de texto e ela abre o teclado virtual, a mensagem mais abaixo ainda está em exibição. Parece não haver uma maneira de fazer isso com o CSS de maneira surpreendente, então o JavaScript resize(única maneira de descobrir quando o teclado é aberto e fechado aparentemente) eventos e rolagem manual para o resgate.

Alguém forneceu essa solução e eu descobri essa solução , que parece funcionar.

Exceto em um caso. Por alguma razão, se você estiver dentro MOBILE_KEYBOARD_HEIGHT(250 pixels no meu caso) de pixels da parte inferior da div das mensagens, quando fechar o teclado do celular, algo estranho acontecerá. Com a solução anterior, ela rola para o fundo. E com a última solução, em vez disso, rola os MOBILE_KEYBOARD_HEIGHTpixels para baixo.

Se você for rolado acima dessa altura, as duas soluções fornecidas acima funcionarão perfeitamente. Somente quando você está perto do fundo é que eles têm esse problema menor.

Eu pensei que talvez fosse apenas o meu programa causando isso com algum código estranho, mas não, eu até reproduzi um violino e ele tem esse problema exato. Peço desculpas por tornar isso tão difícil de depurar, mas se você acessar https://jsfiddle.net/t596hy8d/6/show (o sufixo show fornece um modo de tela cheia) no seu telefone, você poderá ver o mesmo comportamento.

Sendo esse comportamento, se você rolar o suficiente, abrir e fechar o teclado mantém a posição. No entanto, se você fechar o teclado dentro dos MOBILE_KEYBOARD_HEIGHTpixels da parte inferior, verá que ele rola para a parte inferior.

O que está causando isso?

Reprodução de código aqui:

window.onload = function(e){ 
  document.querySelector(".messages").scrollTop = 10000;
  
  bottomScroller(document.querySelector(".messages"));
}
  

function bottomScroller(scroller) {
  let scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;

  scroller.addEventListener('scroll', () => { 
  scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
  });   

  window.addEventListener('resize', () => { 
  scroller.scrollTop = scroller.scrollHeight - scrollBottom - scroller.clientHeight;

  scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
  });
}
.container {
  width: 400px;
  height: 87vh;
  border: 1px solid #333;
  display: flex;
  flex-direction: column;
}

.messages {
  overflow-y: auto;
  height: 100%;
}

.send-message {
  width: 100%;
  display: flex;
  flex-direction: column;
}
<div class="container">
  <div class="messages">
  <div class="message">hello 1</div>
  <div class="message">hello 2</div>
  <div class="message">hello 3</div>
  <div class="message">hello 4</div>
  <div class="message">hello 5</div>
  <div class="message">hello 6 </div>
  <div class="message">hello 7</div>
  <div class="message">hello 8</div>
  <div class="message">hello 9</div>
  <div class="message">hello 10</div>
  <div class="message">hello 11</div>
  <div class="message">hello 12</div>
  <div class="message">hello 13</div>
  <div class="message">hello 14</div>
  <div class="message">hello 15</div>
  <div class="message">hello 16</div>
  <div class="message">hello 17</div>
  <div class="message">hello 18</div>
  <div class="message">hello 19</div>
  <div class="message">hello 20</div>
  <div class="message">hello 21</div>
  <div class="message">hello 22</div>
  <div class="message">hello 23</div>
  <div class="message">hello 24</div>
  <div class="message">hello 25</div>
  <div class="message">hello 26</div>
  <div class="message">hello 27</div>
  <div class="message">hello 28</div>
  <div class="message">hello 29</div>
  <div class="message">hello 30</div>
  <div class="message">hello 31</div>
  <div class="message">hello 32</div>
  <div class="message">hello 33</div>
  <div class="message">hello 34</div>
  <div class="message">hello 35</div>
  <div class="message">hello 36</div>
  <div class="message">hello 37</div>
  <div class="message">hello 38</div>
  <div class="message">hello 39</div>
  </div>
  <div class="send-message">
	<input />
  </div>
</div>

Ryan Peschel
fonte
Eu substituiria os manipuladores de eventos por IntersectionObserver e ResizeObserver. Eles têm uma sobrecarga de CPU muito menor do que os manipuladores de eventos. Se você estiver direcionando navegadores mais antigos, ambos têm polyfills.
bigless 11/01
Você já tentou isso no Firefox para dispositivos móveis? Parece não ter esse problema. No entanto, tentar isso no Chrome causa o problema que você mencionou.
Richard
Bem, ele tem que funcionar no Chrome de qualquer maneira. É legal que o Firefox não tenha esse problema.
Ryan Peschel 18/01
Meu mal por não transmitir o meu ponto de vista corretamente. Se um navegador tiver um problema e outro não, isso, IMO, pode significar que você pode precisar de uma implementação ligeiramente diferente para diferentes navegadores.
Richard
11
@halfer Tudo bem. Eu vejo. Obrigado pelo lembrete, levarei isso em consideração na próxima vez em que pedir a alguém para revisitar uma resposta.
Richard

Respostas:

3

Finalmente encontrei uma solução que realmente funciona. Embora possa não ser o ideal, ele realmente funciona em todos os casos. Aqui está o código:

bottomScroller(document.querySelector(".messages"));

bottomScroller = scroller => {
  let pxFromBottom = 0;

  let calcPxFromBottom = () => pxFromBottom = scroller.scrollHeight - (scroller.scrollTop + scroller.clientHeight);

  setInterval(calcPxFromBottom, 500);

  window.addEventListener('resize', () => { 
    scroller.scrollTop = scroller.scrollHeight - pxFromBottom - scroller.clientHeight;
  });
}

Algumas epifanias que tive ao longo do caminho:

  1. Ao fechar o teclado virtual, um scrollevento ocorre instantaneamente antes do resizeevento. Isso parece acontecer apenas ao fechar o teclado, não ao abri-lo. Esse é o motivo pelo qual você não pode usar o scrollevento para definir pxFromBottom, porque se você estiver próximo à parte inferior, ele será definido como 0 no scrollevento imediatamente antes do resizeevento, atrapalhando o cálculo.

  2. Outra razão pela qual todas as soluções tiveram dificuldade perto da parte inferior das mensagens div é um pouco difícil de entender. Por exemplo, na minha solução de redimensionamento, basta adicionar ou subtrair 250 (altura do teclado móvel) para scrollTopquando abrir ou fechar o teclado virtual. Isso funciona perfeitamente, exceto perto do fundo. Por quê? Porque digamos que você esteja a 50 pixels da parte inferior e feche o teclado. Subtrairá 250 de scrollTop(altura do teclado), mas subtrairá apenas 50! Portanto, ele sempre será redefinido para a posição fixa incorreta ao fechar o teclado próximo à parte inferior.

  3. Eu também acredito que você não pode usar onFocuse onBlureventos para esta solução, porque esses ocorrem apenas ao selecionar inicialmente a caixa de texto para abrir o teclado. Você é perfeitamente capaz de abrir e fechar o teclado móvel sem ativar esses eventos e, como tal, eles não podem ser usados ​​aqui.

Acredito que os pontos acima são importantes para o desenvolvimento de uma solução, porque no início não são óbvios, mas impedem o desenvolvimento de uma solução robusta.

Não gosto dessa solução (o intervalo é um pouco ineficiente e propenso a condições de corrida), mas não consigo encontrar nada melhor que sempre funcione.

Ryan Peschel
fonte
1

Eu acho que o que você quer é overflow-anchor

O suporte está aumentando, mas não total, ainda https://caniuse.com/#feat=css-overflow-anchor

De um artigo sobre CSS-Tricks:

A Ancoragem de rolagem impede que a experiência de "pular" bloqueie a posição do usuário na página enquanto ocorrem alterações no DOM acima do local atual. Isso permite que o usuário fique ancorado no local em que está na página, mesmo quando novos elementos são carregados no DOM.

A propriedade overflow-anchor nos permite desativar o recurso Scroll Anchoring no caso de ser preferível permitir que o conteúdo seja re-fluído à medida que os elementos são carregados.

Aqui está uma versão ligeiramente modificada de um de seus exemplos:

let scroller = document.querySelector('#scroller');
let anchor = document.querySelector('#anchor');

// https://ajaydsouza.com/42-phrases-a-lexophile-would-love/
let messages = [
  'I wondered why the baseball was getting bigger. Then it hit me.',
  'Police were called to a day care, where a three-year-old was resisting a rest.',
  'Did you hear about the guy whose whole left side was cut off? He’s all right now.',
  'The roundest knight at King Arthur’s round table was Sir Cumference.',
  'To write with a broken pencil is pointless.',
  'When fish are in schools they sometimes take debate.',
  'The short fortune teller who escaped from prison was a small medium at large.',
  'A thief who stole a calendar… got twelve months.',
  'A thief fell and broke his leg in wet cement. He became a hardened criminal.',
  'Thieves who steal corn from a garden could be charged with stalking.',
  'When the smog lifts in Los Angeles , U. C. L. A.',
  'The math professor went crazy with the blackboard. He did a number on it.',
  'The professor discovered that his theory of earthquakes was on shaky ground.',
  'The dead batteries were given out free of charge.',
  'If you take a laptop computer for a run you could jog your memory.',
  'A dentist and a manicurist fought tooth and nail.',
  'A bicycle can’t stand alone; it is two tired.',
  'A will is a dead giveaway.',
  'Time flies like an arrow; fruit flies like a banana.',
  'A backward poet writes inverse.',
  'In a democracy it’s your vote that counts; in feudalism, it’s your Count that votes.',
  'A chicken crossing the road: poultry in motion.',
  'If you don’t pay your exorcist you can get repossessed.',
  'With her marriage she got a new name and a dress.',
  'Show me a piano falling down a mine shaft and I’ll show you A-flat miner.',
  'When a clock is hungry it goes back four seconds.',
  'The guy who fell onto an upholstery machine was fully recovered.',
  'A grenade fell onto a kitchen floor in France and resulted in Linoleum Blownapart.',
  'You are stuck with your debt if you can’t budge it.',
  'Local Area Network in Australia : The LAN down under.',
  'He broke into song because he couldn’t find the key.',
  'A calendar’s days are numbered.',
];

function randomMessage() {
  return messages[(Math.random() * messages.length) | 0];
}

function appendChild() {
  let msg = document.createElement('div');
  msg.className = 'message';
  msg.innerText = randomMessage();
  scroller.insertBefore(msg, anchor);
}
setInterval(appendChild, 1000);
html {
  height: 100%;
  display: flex;
}

body {
  min-height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  padding: 0;
}

#scroller {
  flex: 2;
}

#scroller * {
  overflow-anchor: none;
}

.new-message {
  position: sticky;
  bottom: 0;
  background-color: blue;
  padding: .2rem;
}

#anchor {
  overflow-anchor: auto;
  height: 1px;
}

body {
  background-color: #7FDBFF;
}

.message {
  padding: 0.5em;
  border-radius: 1em;
  margin: 0.5em;
  background-color: white;
}
<div id="scroller">
  <div id="anchor"></div>
</div>

<div class="new-message">
  <input type="text" placeholder="New Message">
</div>

Abra isso no celular: https://cdpn.io/chasebank/debug/PowxdOR

O que está fazendo é basicamente desativar qualquer ancoragem padrão dos novos elementos da mensagem, com #scroller * { overflow-anchor: none }

E, em vez disso, ancore um elemento vazio #anchor { overflow-anchor: auto }que sempre virá após essas novas mensagens, pois as novas mensagens serão inseridas antes dele.

Tem que haver um pergaminho para notar uma mudança na ancoragem, o que eu acho que geralmente é um bom UX. Mas de qualquer maneira, a posição atual de rolagem deve ser mantida quando o teclado abrir.

correr atrás
fonte
0

Minha solução é igual à sua solução proposta com uma adição de verificação condicional. Aqui está uma descrição da minha solução:

  • Grave a última posição de rolagem scrollTope último clientHeightde .messagesa oldScrollTope oldHeight, respectivamente,
  • Atualizar oldScrollTope oldHeightsempre que resizeacontece windowe atualizar oldScrollToptoda vez que scrollacontece.messages
  • Quando windowé reduzido (quando o teclado virtual é exibido), a altura de .messagesretrai-se automaticamente. O comportamento pretendido é tornar o conteúdo mais baixo .messagesainda visível, mesmo quando .messages'height retrai. Isso exige que ajustemos manualmente a posição scrollTopde rolagem de .messages.
  • Quando os shows teclado virtual, atualização scrollTopde .messagester certeza de que a parte mais inferior de .messagesantes de sua retração altura acontece ainda é visível
  • Quando as peles teclado virtual, atualização scrollTopde .messagester certeza de que a parte mais inferior de .messagesrestos a parte mais inferior da .messagesapós a expansão de altura (a menos que a expansão não pode acontecer para cima, o que acontece quando você está quase no topo .messages)

O que causou o problema?

Meu pensamento lógico (possivelmente possivelmente defeituoso) é: resizeacontece, .messagesa altura muda, a atualização .messages scrollTopacontece dentro do nosso resizemanipulador de eventos. No entanto, após .messagesa expansão da altura, um scrollevento ocorre curiosamente antes de a resize! E ainda mais curioso, o scrollevento acontece quando ocultamos o teclado quando rolamos acima do scrollTopvalor máximo de quando .messagesnão é retraído. No meu caso, isso significa que, quando eu rolar abaixo 270.334px(o máximo scrollTopantes .messagesé retraído) e ocultar o teclado, esse evento estranho scrollantes do resizeacontecimento acontece e rola o seu .messagesexatamente 270.334px. Obviamente, isso atrapalha nossa solução acima.

Felizmente, podemos solucionar isso. Minha dedução pessoal de por que isso scrollantes do resizeevento acontecer é porque .messagesnão é possível manter sua scrollTopposição acima 270.334pxquando se expande em altura (é por isso que mencionei que meu pensamento lógico inicial é defeituoso; simplesmente porque não há como .messagesmanter sua scrollTopposição acima de seu máximo valor) . Portanto, ele define seu scrollTopvalor imediatamente para o valor máximo que pode fornecer (que é, sem surpresa 270.334px).

O que podemos fazer?

Como atualizamos apenas oldHeightno redimensionamento, podemos verificar se esse deslocamento forçado (ou mais corretamente resize) acontece e se ocorrer, não é atualizado oldScrollTop(porque já lidamos com isso resize!) Simplesmente precisamos comparar oldHeighte a altura atual em scrollpara ver se essa rolagem forçada acontece. Isso funciona porque a condição de oldHeightnão ser igual à altura atual ativada scrollsomente será verdadeira quando resizeacontecer (o que coincidentemente ocorre quando a rolagem forçada acontece).

Aqui está o código (no JSFiddle) abaixo:

window.onload = function(e) {
  let messages = document.querySelector('.messages')
  messages.scrollTop = messages.scrollHeight - messages.clientHeight
  bottomScroller(messages);
}


function bottomScroller(scroller) {
  let oldScrollTop = scroller.scrollTop
  let oldHeight = scroller.clientHeight

  scroller.addEventListener('scroll', e => {
    console.log(`Scroll detected:
      old scroll top = ${oldScrollTop},
      old height = ${oldHeight},
      new height = ${scroller.clientHeight},
      new scroll top = ${scroller.scrollTop}`)
    if (oldHeight === scroller.clientHeight)
      oldScrollTop = scroller.scrollTop
  });

  window.addEventListener('resize', e => {
    let newScrollTop = oldScrollTop + oldHeight - scroller.clientHeight

    console.log(`Resize detected:
      old scroll top = ${oldScrollTop},
      old height = ${oldHeight},
      new height = ${scroller.clientHeight},
      new scroll top = ${newScrollTop}`)
    scroller.scrollTop = newScrollTop
    oldScrollTop = newScrollTop
    oldHeight = scroller.clientHeight
  });
}
.container {
  width: 400px;
  height: 87vh;
  border: 1px solid #333;
  display: flex;
  flex-direction: column;
}

.messages {
  overflow-y: auto;
  height: 100%;
}

.send-message {
  width: 100%;
  display: flex;
  flex-direction: column;
}
<div class="container">
  <div class="messages">
    <div class="message">hello 1</div>
    <div class="message">hello 2</div>
    <div class="message">hello 3</div>
    <div class="message">hello 4</div>
    <div class="message">hello 5</div>
    <div class="message">hello 6 </div>
    <div class="message">hello 7</div>
    <div class="message">hello 8</div>
    <div class="message">hello 9</div>
    <div class="message">hello 10</div>
    <div class="message">hello 11</div>
    <div class="message">hello 12</div>
    <div class="message">hello 13</div>
    <div class="message">hello 14</div>
    <div class="message">hello 15</div>
    <div class="message">hello 16</div>
    <div class="message">hello 17</div>
    <div class="message">hello 18</div>
    <div class="message">hello 19</div>
    <div class="message">hello 20</div>
    <div class="message">hello 21</div>
    <div class="message">hello 22</div>
    <div class="message">hello 23</div>
    <div class="message">hello 24</div>
    <div class="message">hello 25</div>
    <div class="message">hello 26</div>
    <div class="message">hello 27</div>
    <div class="message">hello 28</div>
    <div class="message">hello 29</div>
    <div class="message">hello 30</div>
    <div class="message">hello 31</div>
    <div class="message">hello 32</div>
    <div class="message">hello 33</div>
    <div class="message">hello 34</div>
    <div class="message">hello 35</div>
    <div class="message">hello 36</div>
    <div class="message">hello 37</div>
    <div class="message">hello 38</div>
    <div class="message">hello 39</div>
  </div>
  <div class="send-message">
    <input />
  </div>
</div>

Testado no Firefox e Chrome para celular e funciona nos dois navegadores.

Richard
fonte