Programação Por Equipe DevBras

Capa do artigo: Tooltip que aparece no foco, fecha no Esc e não esconde o essencial

Tooltip que aparece no foco, fecha no Esc e não esconde o essencial

Faça a dica flutuante abrir no hover e no foco, fechar no Esc e continuar visível quando o ponteiro vai até ela. E veja o que nunca deve morar só na dica.

O painel de vendas tem uma barra de ferramentas com três ícones: uma seta para baixo, um funil e duas setas em círculo. Passando o mouse, aparecem as dicas "Exportar CSV", "Filtrar" e "Atualizar". Com Tab, o foco anda pelos três ícones e nada aparece. No celular, não há como passar o mouse, e a pessoa toca no funil para descobrir o que ele faz. A dica existia, mas só para um tipo de usuário.

A resposta curta: a dica abre no hover e no foco, fecha no Esc, e fica aberta enquanto o ponteiro está sobre ela. O nome do controle não depende da dica: o botão tem nome acessível de qualquer jeito. E o que é essencial, como instrução de preenchimento ou mensagem de erro, fica escrito na página, não num balão.

Por que a dica de hover não chega a todo mundo

Bibliotecas de tooltip só com CSS, como a do link original, mostram o texto num pseudo-elemento quando o ponteiro está em cima (:hover). Se o seletor não inclui o foco, o teclado nunca vê a dica. E como o texto mora num atributo e num ::after, ele não pode ser ligado ao botão por aria-describedby, que precisa apontar para um elemento com id.

Há três exigências que as diretrizes de acessibilidade (critério 1.4.13 das WCAG, "conteúdo em hover ou foco") resumem bem. A dica precisa poder ser dispensada sem mover o ponteiro ou o foco, normalmente com Esc. Precisa ser alcançável: se a pessoa move o ponteiro do ícone para a dica, ela não pode sumir. E precisa ser persistente: fica até a pessoa sair, dispensar ou a informação deixar de valer, sem sumir por um tempo fixo.

No celular, nada disso resolve sozinho, porque não há hover. Ali, ou o controle tem rótulo visível, ou a explicação vira um botão que abre e fecha a informação com um toque.

Como montar a dica do jeito certo

  1. Dê nome ao controle independente da dica. Num botão só com ícone, o nome pode vir do próprio texto da dica, via aria-labelledby.
  2. Coloque o texto da dica num elemento real, com role="tooltip" e id, dentro do mesmo contêiner do controle.
  3. Mostre a dica com :hover e :focus-within no contêiner.
  4. Encoste a dica no controle, sem espaço entre eles, para o ponteiro poder ir de um ao outro sem perder o hover.
  5. Ponha um pequeno atraso para aparecer, assim ela não pisca quando o ponteiro só atravessa a barra.
  6. Feche no Esc com algumas linhas de JavaScript, e reabra quando o foco ou o ponteiro sair e voltar.

O HTML da barra de ferramentas

<div class="com-dica">
  <button type="button" aria-labelledby="dica-exportar" data-acao="exportar">
    <img src="/icones/exportar.svg" alt="" width="20" height="20">
  </button>
  <span role="tooltip" id="dica-exportar">Exportar CSV</span>
</div>

O botão usa o texto da dica como nome. O leitor de tela anuncia "Exportar CSV, botão" mesmo com a dica invisível, porque aria-labelledby lê o texto do elemento apontado ainda que ele esteja escondido visualmente. A imagem tem alt="" porque o nome já está resolvido.

Se a dica complementa um controle que já tem nome ("Taxa", com uma dica explicando como ela é calculada), use aria-describedby no lugar de aria-labelledby. Nome é o que o controle é; descrição é informação extra.

O CSS

.com-dica { position: relative; display: inline-block; }

.com-dica [role="tooltip"] {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  padding: .25rem .5rem;
  border-radius: .25rem;
  background: #111827;
  color: #ffffff;
  font-size: .875rem;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .1s;
}

.com-dica:hover [role="tooltip"],
.com-dica:focus-within [role="tooltip"] {
  opacity: 1;
  pointer-events: auto;
  transition-delay: .3s;
}

.com-dica[data-dispensada] [role="tooltip"] {
  opacity: 0;
  pointer-events: none;
}

O top: 100% encosta a dica embaixo do botão. Como ela é filha do contêiner, mover o ponteiro do ícone para a dica mantém o :hover, e ela não some. O atraso de 300 milissegundos vale só para aparecer; para sumir, a transição é imediata. A última regra vem depois das outras para vencer em caso de empate de especificidade.

O Esc em JavaScript

const dicas = document.querySelectorAll('.com-dica');

document.addEventListener('keydown', (evento) => {
  if (evento.key !== 'Escape') return;
  dicas.forEach((dica) => {
    if (dica.matches(':hover, :focus-within')) dica.setAttribute('data-dispensada', '');
  });
});

dicas.forEach((dica) => {
  dica.addEventListener('mouseleave', () => dica.removeAttribute('data-dispensada'));
  dica.addEventListener('focusout', () => dica.removeAttribute('data-dispensada'));
});

O Esc só esconde a dica que está visível naquele momento. O foco continua no botão, como a regra pede. Quando a pessoa sai e volta, a dica funciona de novo. Se a barra estiver dentro de um modal que também fecha no Esc, decida qual dos dois tem prioridade; o usual é o primeiro Esc fechar a dica e o segundo, o modal.

A dica no celular: botão que mostra a explicação

Para a explicação de um indicador do painel ("Como calculamos a taxa de conversão"), o padrão que funciona no toque é um botão "?" que abre e fecha um pequeno painel. O atributo popover do HTML resolve a parte trabalhosa: o navegador cuida de abrir, fechar no Esc e fechar ao tocar fora.

<span>Taxa de conversão: 3,2%</span>
<button type="button" popovertarget="ajuda-conversao" aria-label="Como a taxa é calculada">?</button>
<div popover id="ajuda-conversao">
  Pedidos pagos divididos por visitas únicas no período. 64 pedidos em 2.000 visitas dão 3,2%.
</div>

O popover está disponível nos navegadores atuais. Se você precisa atender versões antigas, o mesmo comportamento sai com um botão, aria-expanded e o atributo hidden, como no menu retrátil que fecha. Cuide também do contraste do balão e do anel de foco, que costuma sumir em fundo escuro, como mostra foco visível no escuro.

O que nunca mora só na dica

Instrução de preenchimento ("use só números"), formato esperado ("DD/MM/AAAA"), mensagem de erro e aviso de consequência ("esta ação não pode ser desfeita") são parte da tarefa. Se eles só aparecem no balão, quem não descobre o balão erra. Escreva na página, perto do campo. A dica é para o que a pessoa experiente dispensa e a iniciante agradece. Para ícones, o post ícone com nome no botão mostra quando vale trocar a dica por rótulo visível.

Erros comuns

  • Dica só no :hover. Quem navega por teclado nunca lê, e no celular ela não existe.
  • Dica como único nome do ícone, em pseudo-elemento. O botão fica sem nome acessível e o leitor de tela anuncia só "botão".
  • Espaço entre o ícone e o balão. O ponteiro sai do hover no caminho e a dica some antes de ser alcançada.
  • Sem como fechar. O balão cobre o campo de baixo e a pessoa precisa mover o mouse para longe para continuar.
  • Sumir depois de alguns segundos. Quem lê devagar perde o texto no meio.
  • Erro ou instrução só na dica. A pessoa erra o formulário sem nunca ter visto a regra.

Checklist

  • Todo botão com ícone tem nome acessível, com ou sem dica.
  • A dica aparece no hover e no foco.
  • Esc esconde a dica sem tirar o foco do controle.
  • O ponteiro consegue ir do controle até a dica sem ela sumir.
  • A dica não some por tempo.
  • No celular, a explicação abre com um toque num botão.
  • Nenhuma instrução, formato ou erro existe só dentro de uma dica.

Aperte Tab até o primeiro ícone sem texto da sua interface. A dica apareceu e o leitor de tela anunciou um nome? Se nada aconteceu, ela é enfeite de mouse, e o ícone continua sem explicação para todo o resto.

Avalie este artigo

Referência: https://github.com/chinchang/hint.css