Rolagem automática no chat sem arrastar quem subiu para ler o histórico
Faça o chat ou o log acompanhar a mensagem nova só quando a pessoa já está no fim, com aviso de "novas mensagens" e sem roubar o foco de quem digita.
O atendente do suporte sobe vinte mensagens para reler o número do pedido que o cliente mandou. Chega uma mensagem nova e a tela despenca até o fim. Ele sobe de novo, chega outra, despenca de novo. A resposta curta: meça se a pessoa estava perto do fim antes de inserir a mensagem. Se estava, role até o fim. Se não estava, não mexa na rolagem e mostre um botão "novas mensagens".
O mesmo vale para o painel que mostra o log de deploy em tempo real, para o feed de pedidos de uma cozinha e para qualquer lista que cresce embaixo. O exemplo deste guia é o chat de suporte de uma loja, com uma lista rolável e um campo de texto embaixo.
Por que o script prende a pessoa no fim
O código ingênuo faz a mesma coisa a cada mensagem: adiciona o elemento e define scrollTop igual a scrollHeight. Ele não pergunta onde a pessoa está. Para o script, toda novidade é uma ordem para acompanhar.
A informação que falta está em três números que todo elemento rolável tem. scrollHeight é a altura total do conteúdo. clientHeight é a altura visível. scrollTop é quanto já foi rolado a partir do topo. Quando scrollHeight - scrollTop - clientHeight é zero, a pessoa está exatamente no fim. Quando é 800, ela subiu 800 pixels para ler alguma coisa.
A ordem importa. Se você mede depois de inserir a mensagem, a conta já inclui a altura nova e qualquer pessoa parece "longe do fim". Meça antes, insira depois, role por último.
Como fazer a rolagem que respeita a leitura
- Marque a lista como região de log, com
role="log", para que o leitor de tela anuncie as mensagens novas sem que você precise mover o foco. - Crie uma função que diz se a pessoa está perto do fim, com uma folga de alguns pixels. Zoom e telas de alta densidade deixam
scrollTopcom casas decimais, e comparar com zero exato falha. - Ao receber mensagem, guarde o resultado dessa função, insira o elemento e só então role, se a pessoa estava no fim.
- Se ela não estava, conte as não lidas e mostre um botão com o número.
- Quando a pessoa voltar ao fim por conta própria, ou clicar no botão, zere o contador e esconda o aviso.
- No primeiro carregamento do histórico, role até o fim sem perguntar. É o único momento em que a decisão é sua.
O código do chat
<ol id="mensagens" role="log" aria-label="Conversa com o suporte"></ol>
<button type="button" id="novas" hidden></button>
<textarea id="texto" aria-label="Sua mensagem"></textarea>
const lista = document.getElementById('mensagens');
const aviso = document.getElementById('novas');
const FOLGA = 48;
let naoLidas = 0;
function pertoDoFim() {
return lista.scrollHeight - lista.scrollTop - lista.clientHeight <= FOLGA;
}
function irAoFim() {
const menosMovimento = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
lista.scrollTo({ top: lista.scrollHeight, behavior: menosMovimento ? 'auto' : 'smooth' });
}
function atualizarAviso() {
aviso.hidden = naoLidas === 0;
aviso.textContent = naoLidas === 1 ? '1 nova mensagem' : naoLidas + ' novas mensagens';
}
function adicionarMensagem(item) {
const estavaNoFim = pertoDoFim();
lista.append(item);
if (estavaNoFim) {
irAoFim();
} else {
naoLidas += 1;
atualizarAviso();
}
}
lista.addEventListener('scroll', function () {
if (pertoDoFim() && naoLidas > 0) {
naoLidas = 0;
atualizarAviso();
}
});
aviso.addEventListener('click', function () {
naoLidas = 0;
atualizarAviso();
irAoFim();
});
A folga de 48 pixels é mais ou menos a altura de uma linha de mensagem. Com ela, quem está "quase no fim" também é acompanhado, o que bate com a intenção de quem está lendo a conversa ao vivo. A animação suave respeita a preferência do sistema por menos movimento: quem pediu isso recebe o salto direto.
Imagem que carrega depois
Mensagem com foto chega com altura pequena e cresce quando a imagem termina de baixar. Se a pessoa estava no fim, ela fica alguns pixels acima dele e a próxima mensagem já não a acompanha. Resolva de dois jeitos: reserve a altura com width e height na tag da imagem, ou, no evento load dela, role de novo se a pessoa ainda estava perto do fim.
Histórico antigo carregado no topo
O caminho inverso também prende. Quando a pessoa sobe até o topo e você carrega mensagens antigas acima, o conteúdo empurra tudo para baixo e ela perde o lugar. Guarde o scrollHeight antes de inserir, insira e some a diferença ao scrollTop. Alguns navegadores já tentam compensar sozinhos com a ancoragem de rolagem; se você faz a conta à mão, desligue a automática na lista com overflow-anchor: none para a compensação não acontecer duas vezes.
Foco: a rolagem não pode mexer no cursor
Nunca chame focus() na lista ou na mensagem nova para "levar a pessoa até ela". Quem está digitando a resposta perde o cursor no meio da palavra. A região com role="log" já cuida do anúncio para leitor de tela, e a rolagem com scrollTo não toca no foco.
Se a lista é a própria página, e não uma caixa rolável, a mesma conta vale com document.scrollingElement no lugar de lista. Para saber quando um bloco específico entrou na tela, sem fazer conta a cada evento de rolagem, o caminho é outro, e está em saber quando um bloco entrou na tela.
Erros comuns
- Medir depois de inserir. A altura nova entra na conta e o script acha que ninguém está no fim. O chat para de acompanhar até para quem estava lendo ao vivo.
- Comparar com zero exato. Com zoom de 110% o resto da conta vira 0,5 e a condição nunca é verdadeira. Use uma folga.
- Rolar a cada mensagem sem condição. Quem subiu para ler é arrastado a cada segundo e desiste do histórico. É exatamente o bug do título.
- Mover o foco para a mensagem nova. O texto que a pessoa digitava perde o cursor e a frase sai cortada.
- Esconder a novidade de quem subiu. Sem o botão de "novas mensagens", o atendente não sabe que o cliente respondeu e deixa a conversa parada.
- Animação suave obrigatória. Quem configurou menos movimento no sistema recebe o deslize mesmo assim. Respeite
prefers-reduced-motion.
Checklist
- A posição é medida antes de inserir a mensagem.
- A comparação com o fim tem folga, não zero exato.
- Quem subiu não é puxado para baixo quando chega mensagem.
- Existe um botão com o número de mensagens novas, e ele desce ao clicar.
- O contador zera quando a pessoa volta ao fim sozinha.
- O campo de digitação mantém o foco quando chega mensagem.
- A lista tem
role="log"e um nome acessível. - Carregar histórico antigo no topo não tira a pessoa do lugar.
Abra o chat ou o log ao vivo do seu sistema, suba até a metade e espere uma mensagem nova chegar. A tela te arrastou? Se sim, quem decidiu que você queria acompanhar foi o script, não você.
Referência: http://forum.imasters.com.br/topic/514073-barra-de-rolagem-vai-para-o-fim-da-pagina/