Programação Por Equipe DevBras

Capa do artigo: Rolagem automática no chat sem arrastar quem subiu para ler o histórico

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

  1. 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.
  2. 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 scrollTop com casas decimais, e comparar com zero exato falha.
  3. Ao receber mensagem, guarde o resultado dessa função, insira o elemento e só então role, se a pessoa estava no fim.
  4. Se ela não estava, conte as não lidas e mostre um botão com o número.
  5. Quando a pessoa voltar ao fim por conta própria, ou clicar no botão, zere o contador e esconda o aviso.
  6. 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ê.

Avalie este artigo

Referência: http://forum.imasters.com.br/topic/514073-barra-de-rolagem-vai-para-o-fim-da-pagina/