Navegação espacial (foco no teclado direcional)

Recursos da Web: scrollIntoView()

A navegação de foco direcional (usando teclas de seta ou botões direcionais) permite que usuários que usam apenas o teclado ou entradas alternativas naveguem por itens interativos com base no layout visual 2D. É muito útil para interfaces do usuário espaciais (como barras de mídia, painéis de controle ou catálogos aninhados), mas precisa ser implementado com cuidado para não prejudicar os comportamentos nativos do navegador, como a rolagem da janela de visualização.

Este guia aborda como criar uma navegação direcional 2D acessível e de alto desempenho em widgets compostos.


Princípios básicos de arquitetura

Ao implementar o foco espacial personalizado com as teclas de seta, siga estas regras básicas:

  1. Limitar a widgets compostos: não substitua as teclas de seta no fluxo de texto normal do documento ou em layouts de lista vertical simples em que a navegação sequencial padrão com a tecla Tab é esperada. Consulte as Diretrizes de programação de acessibilidade para orientações gerais sobre o fluxo de documentos.
  2. Preservar a rolagem de limite nativa: se não houver um elemento focalizável na direção pressionada, não impeça o comportamento padrão do navegador. Deixe o evento se propagar para que o navegador possa rolar a janela de visualização de forma nativa. Substituir as teclas de seta incondicionalmente prejudica a acessibilidade (WCAG 1.4.10).
  3. Garanta a visibilidade e o alinhamento do foco: quando o foco mudar, verifique se o elemento recém-focado é rolado para a visualização (por exemplo, usando scrollIntoView({ block: 'nearest', inline: 'nearest' })) para que ele permaneça totalmente visível.
  4. Coordenar com o sistema de layout: determine as posições dos candidatos ao foco com base em geometrias de limite ativas (getBoundingClientRect()) ao processar cada pressionamento de tecla. Evite presumir que a ordem do DOM ou uma estrutura de grade fixa representa o layout visual. Consulte Layouts CSS e design responsivo para ver recomendações de layout moderno.

Gerenciamento de foco acessível

O contêiner espacial precisa expor uma única parada de tabulação para a navegação sequencial de guias de página. Dentro do widget, gerencie o foco do teclado usando o padrão de tabindex móvel:

  • O item ativo tem tabindex="0".
  • Todos os itens inativos têm tabindex="-1".
  • Ao navegar, mova o foco de forma programática mudando o atributo tabindex="0" e chamando focus().

Seleção de candidatos visuais

Em layouts assimétricos, agrupados ou de grade, o mapeamento baseado em índice falha. Em vez disso, derive candidatos da geometria visual ativa para o pressionamento de tecla atual:

  • Candidatos: use os descendentes focalizáveis do contêiner do widget que transmitem checkVisibility(). Os elementos ocultos podem ter um retângulo de tamanho zero em (0, 0) e não podem se tornar destinos acidentais de up ou left.
  • Geometria: leia getBoundingClientRect() para o elemento em foco e cada candidato ao processar o pressionamento de tecla. Não armazene esses retângulos em cache globalmente: o layout pode mudar por vários motivos, e um pequeno número de leituras de geometria é barato ao processar um evento de tecla no ritmo do usuário.
  • Filtro de direção: um candidato está na direção pressionada apenas quando a borda próxima está na borda distante do elemento atual ou além dela nesse eixo, permitindo uma pequena tolerância para arredondamento de subpixel. Por exemplo, um candidato para right precisa ter candidate.left >= current.right - tolerance. Não compare os centros para determinar se um candidato está na direção.
  • Candidatos alinhados primeiro: entre os candidatos que passam pelo filtro de direção, priorize aqueles cuja extensão no eixo perpendicular se sobrepõe à extensão do elemento atual. Escolha o menor intervalo no eixo de navegação, desempatando com o menor deslocamento central perpendicular. Isso mantém a navegação na linha ou coluna visual adjacente, mesmo quando um item é excepcionalmente largo.
  • Pontuação de fallback: se nenhum candidato se sobrepuser no eixo perpendicular, pontue os candidatos restantes como gap + w * perpendicularCentreOffset e escolha o mínimo. Use um peso w >= 2 como ponto de partida e ajuste-o para o layout. Um peso maior favorece os candidatos que estão mais alinhados.
  • Nenhum candidato: não retorna nada. O manipulador de eventos não pode chamar preventDefault(), permitindo que o navegador role nativamente.

A implementação exata pode variar, mas mantenha o teste de direção baseado em bordas e aplique a preferência de sobreposição antes do substituto ponderado. Esse é o ponto crucial do algoritmo. Evite apresentar um cache ou um peso fixo como universalmente correto.


Integração de pressionamento de tecla e cooperação de rolagem

O roteador de eventos precisa impedir a rolagem padrão da janela de visualização somente quando ocorre uma transição de foco espacial válida:

  • Não intercepte o evento quando uma tecla modificadora for pressionada, quando e.defaultPrevented já for verdadeiro ou quando o destino do evento mais interno consumir as teclas de seta. Use e.composedPath()[0] em vez de apenas e.target, porque e.target pode ser redirecionado para um host de sombra. Uma verificação prática de edição/controle é target.matches(':read-write, select, input[type="range"], [role="slider"]'). As raízes de sombra fechadas não podem ser inspecionadas.
  • Defina o escopo do listener para o contêiner do widget, de modo que vários widgets espaciais possam coexistir em uma página.
  • Aja apenas quando o elemento em foco for um candidato. Se os itens tiverem os próprios controles focalizáveis, torne-os candidatos também ou não faça nada. Não resolva um elemento pai com closest().
  • Quando um candidato for encontrado, chame preventDefault(), atualize o tabindex de roaming, chame focus() e use scrollIntoView({ block: 'nearest', inline: 'nearest' }).
  • Quando nenhum candidato for encontrado, retorne sem chamar preventDefault() para que a rolagem nativa continue.

Evite adicionar atalhos de um único caractere, como h/j/k/l ou W/A/S/D, para navegação espacial. As teclas de seta não são teclas de caracteres, e preservar a rolagem nativa quando não há candidatos evita o bloqueio desnecessário do movimento da janela de visualização.