Programação Por Equipe DevBras

Capa do artigo: Interface que só responde ao mouse: como cobrir teclado e toque

Interface que só responde ao mouse: como cobrir teclado e toque

Ação presa em hover ou mousedown some para quem usa Tab ou o dedo. Veja como usar click, foco e media queries de ponteiro para a mesma ação funcionar em todo lugar.

Na página do carrinho, cada produto mostra o botão "Remover" quando o ponteiro passa por cima do card. No notebook do desenvolvedor, perfeito. No celular, não existe "passar por cima", e o botão só aparece depois de um toque que às vezes abre o produto. Com Tab, o foco anda por um link invisível que ninguém sabe o que faz. Três jeitos de usar a mesma tela, e só um deles funciona.

A resposta curta: a ação mora no evento click de um <button> ou <a> de verdade, que já responde a mouse, toque, Enter e espaço. Hover pode adiantar uma dica ou realçar o card, mas nunca ser o único caminho até a ação. E nada destrutivo dispara no mousedown.

Por que o evento de mouse deixa gente de fora

Os eventos mouseenter, mouseover e mousedown descrevem o que um mouse faz. O teclado não entra em lugar nenhum, e o toque só gera eventos de mouse emulados, depois do fato, para não quebrar sites antigos. Por isso o menu que abre no mouseenter não existe para quem navega por Tab, e no celular costuma abrir e "grudar" aberto depois do toque.

O click é diferente. Apesar do nome, num <button> ele dispara com o clique do mouse, com o toque, com Enter e com a barra de espaço. É um evento de ativação, não de mouse. O problema aparece quando o click está numa <div> ou num <span>: esses elementos não recebem foco nem respondem a tecla nenhuma, e a ação volta a ser exclusiva do ponteiro.

Há ainda a questão do momento. O mousedown dispara assim que o botão do mouse desce. O click só dispara se o botão desceu e subiu sobre o mesmo elemento. Essa diferença é a chance de desistir: quem apertou em cima de "Excluir" por engano pode arrastar o ponteiro para fora antes de soltar, e nada acontece. Com a ação no mousedown, essa saída não existe.

Como refazer o card do carrinho

O card original tinha este formato, com a ação escondida até o hover e presa num <span>:

<div class="card" onmouseenter="mostrarAcoes(this)" onmouseleave="esconderAcoes(this)">
  <span class="nome">Tênis Corrida 42</span>
  <div class="acoes" hidden>
    <span onmousedown="remover(42)">Remover</span>
  </div>
</div>

O passo a passo para consertar:

  1. Troque o <span> por um <button type="button"> com um nome que diga o que faz e sobre o quê.
  2. Mude a ação de mousedown para click.
  3. Deixe as ações visíveis por padrão. Esconda até o hover só em dispositivos que têm hover de verdade.
  4. Mostre as ações também quando qualquer coisa dentro do card receber foco.
  5. Teste com mouse, com Tab e com o celular, nessa ordem.
<article class="card">
  <h3>Tênis Corrida 42</h3>
  <div class="acoes">
    <button type="button" data-remover="42">Remover Tênis Corrida 42</button>
  </div>
</article>

O CSS esconde as ações só quando o aparelho tem ponteiro fino e hover, e mostra de novo no hover e no foco:

.card .acoes { opacity: 1; }

@media (hover: hover) and (pointer: fine) {
  .card .acoes { opacity: 0; transition: opacity .15s; }
  .card:hover .acoes,
  .card:focus-within .acoes { opacity: 1; }
}

Usar opacity em vez de display: none é proposital. O botão continua no fluxo do Tab; quando ele recebe foco, o :focus-within do card acende as ações e a pessoa vê onde está. Com display: none, o botão sairia da ordem do teclado. No celular, a media query não casa e as ações ficam sempre visíveis, que é o que o dedo precisa.

O JavaScript escuta o click uma vez, no documento, e descobre qual botão foi ativado:

document.addEventListener('click', (evento) => {
  const botao = evento.target.closest('[data-remover]');
  if (!botao) return;
  removerDoCarrinho(botao.dataset.remover);
});

Esse padrão de delegação, com closest, está explicado em detalhe em saber qual elemento foi clicado. Aqui o ponto é outro: o mesmo handler atende mouse, toque e teclado sem uma linha a mais.

Quando você precisa mesmo do ponteiro

Pointer Events no lugar de mouse e touch separados

Algumas interações são de ponteiro por natureza: desenhar uma assinatura, arrastar um controle deslizante próprio, recortar uma foto. Para elas, use Pointer Events (pointerdown, pointermove, pointerup, pointercancel). Um único conjunto de eventos cobre mouse, caneta e dedo, e evento.pointerType diz qual deles está em uso. Com setPointerCapture, o elemento continua recebendo o movimento mesmo se o dedo escorregar para fora dele.

Mesmo nesses casos, pense na alternativa sem ponteiro. O controle deslizante aceita setas do teclado? A lista arrastável tem botões "subir" e "descer"? O post sobre arrastar e soltar com teclado mostra como oferecer esse segundo caminho.

Hover como bônus, não como porta

Hover é ótimo para adiantar informação: realçar a linha da tabela, mostrar uma prévia, exibir o nome de um ícone. A regra é que tudo o que aparece no hover também apareça no foco, e que nada essencial fique só ali. Para dicas flutuantes, o detalhe está em dica que aparece no foco.

O tamanho do alvo

O mouse acerta um ícone de 16 pixels; o dedo, não. Se a ação ficou visível no celular, ela também precisa ter área de toque suficiente e distância do botão vizinho. Isso está em alvo de toque maior que o mouse.

Erros comuns

  • Ação em <div> ou <span> com click. Não recebe foco nem responde a Enter, e quem usa teclado não consegue executar a ação.
  • Excluir no mousedown. Tira a chance de desistir arrastando para fora, e o clique acidental vira dado perdido.
  • Esconder com display: none até o hover. O botão some da ordem do Tab e só existe para o mouse.
  • Menu que abre no mouseenter e fecha no mouseleave. No celular abre e não fecha, no teclado não abre.
  • Tratar touchstart e mousedown juntos sem cuidado. O toque dispara os dois e a ação roda duas vezes.
  • Instrução que só aparece no hover. "Passe o mouse para ver as opções" não ajuda quem não tem mouse.

Checklist

  • Toda ação está num <button> ou num <a href>, nunca num elemento genérico com click.
  • Nenhuma ação destrutiva dispara no mousedown ou no pointerdown.
  • O que aparece no hover também aparece com :focus-within ou :focus-visible.
  • Elementos escondidos até o hover usam opacity, não display: none.
  • Esconder até o hover só acontece dentro de @media (hover: hover).
  • Interações de arrastar usam Pointer Events e têm alternativa por teclado.
  • A tela foi testada com mouse, com Tab e no celular.

Desconecte o mouse e percorra a sua página principal só com Tab, Enter e espaço. Você consegue chegar em todas as ações que o mouse alcança, ou alguma delas só existe para quem passa o ponteiro por cima?

Avalie este artigo

Referência: http://www.linhadecodigo.com.br/artigo/3491/tratando-os-eventos-do-mouse-com-jquery.aspx