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:
- Troque o
<span>por um<button type="button">com um nome que diga o que faz e sobre o quê. - Mude a ação de
mousedownparaclick. - Deixe as ações visíveis por padrão. Esconda até o hover só em dispositivos que têm hover de verdade.
- Mostre as ações também quando qualquer coisa dentro do card receber foco.
- 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>comclick. 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: noneaté o hover. O botão some da ordem do Tab e só existe para o mouse. - Menu que abre no
mouseentere fecha nomouseleave. No celular abre e não fecha, no teclado não abre. - Tratar
touchstartemousedownjuntos 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 comclick. - Nenhuma ação destrutiva dispara no
mousedownou nopointerdown. - O que aparece no hover também aparece com
:focus-withinou:focus-visible. - Elementos escondidos até o hover usam
opacity, nãodisplay: 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?
Referência: http://www.linhadecodigo.com.br/artigo/3491/tratando-os-eventos-do-mouse-com-jquery.aspx