Pular para o conteúdo

ADR-0024 Atalhos de teclado

Atalhos de teclado padrão da empresa — TanStack Hotkeys, fixado em versões exatas enquanto for pré-1.0, com regras de WCAG 2.1.4 e de descoberta para cada atalho.

Decisão
TanStack Hotkeys, fixadoConstruir a app
Estado
Aceite
Data
Âmbito
Predefinição da empresa para todas as novas apps

As interfaces densas — tabelas, editores, paletas de comandos, diálogos — pedem atalhos de teclado. Fazê-los bem é sobretudo uma questão de casos-limite: disposições de teclado, Command no macOS versus Control nos restantes, escrever em campos, widgets com foco que usam as mesmas teclas, composição IME, um diálogo e a página por trás a quererem ambos o Escape. Feitos sem cuidado, os atalhos são também um risco de acessibilidade: o WCAG 2.1.4 Character Key Shortcuts (Nível A) exige que um atalho feito apenas de teclas de letra, número, pontuação ou símbolo possa ser desativado, remapeado para incluir um modificador como Control ou Alt, ou esteja ativo apenas enquanto o seu componente tem foco.

As decisões à volta estão tomadas:

  • Componentes: shadcn/ui sobre Base UI, que é dono do foco e das teclas dentro de diálogos, menus e selects (ADR-0011)
  • Formulários: TanStack Form com submissão nativa de <form> (ADR-0013)
  • Renderização: o TanStack Start renderiza no servidor e depois hidrata (ADR-0002)
  • Testes: bun test com happy-dom para componentes, Playwright para o comportamento do teclado e end-to-end (ADR-0014)

O que está em aberto: que biblioteca regista os atalhos, como os atalhos convivem com o tratamento de teclado do próprio Base UI, e que regras de acessibilidade todos os atalhos têm de cumprir.

Restrições:

  • Um atalho nunca é o único caminho. As WAI-ARIA Authoring Practices dizem para não usar atalhos de teclado como substituto do acesso pela navegação normal
  • Pré-1.0, e a mover-se depressa. A visão geral do Hotkeys chama-lhe beta, com uma API “still subject to change”; os pacotes são @tanstack/hotkeys 0.17.0 e @tanstack/react-hotkeys 0.16.0 (ambos publicados em 2026-10-19). A política é a que o ADR-0008 definiu para o Drizzle v1 enquanto era release candidate: adotar, fixar versões exatas, atualizar de forma deliberada

Atalhos de teclado com tipos seguros, com scopes, sequências e remapeamento pelo utilizador.

Prós

  • Bindings tipados: Mod+S para chords, ['G', 'G'] para sequências; uma gralha é um erro de tipos. O Mod resolve para Command no macOS e Control no Windows e no Linux
  • Predefinições sensatas: as teclas simples são ignoradas enquanto o utilizador escreve num campo de texto, mas os chords com Control / Meta e o Escape continuam a disparar. Botões, menus, listboxes, separadores e sliders com foco mantêm as teclas que usam, seguindo as WAI-ARIA Authoring Practices. Disposições de teclado, AltGr e composição IME são tratados
  • Scopes: um hotkey está ativo enquanto o seu componente está montado; os scopes aninhados ganham; um scope exclusive — o de um diálogo, por exemplo — suspende todos os outros scopes, incluindo o global. Os scopes seguem a árvore de componentes, pelo que os portais funcionam
  • App hotkeys: declarar cada atalho uma só vez com defineAppHotkeys, para que diálogos de ajuda e ecrãs de definições listem atalhos de páginas que ainda não carregaram. Os utilizadores podem remapear ou remover atalhos através de um recorder, e os conflitos são verificados em todas as declarações. A biblioteca não guarda nada; a app decide onde
  • A sua documentação trata o WCAG 2.1.4 diretamente: enabled para uma definição do utilizador, app hotkeys para remapear, target para só com foco
  • Não liga listeners durante a renderização no servidor; MIT; intervalo de peer react >=16.8; o @tanstack/react-hotkeys depende exatamente do @tanstack/hotkeys 0.17.0 e reexporta-o

Contras

  • Beta, e a API move-se depressa. Publicado pela primeira vez em 2026-02-10; da 0.11.0 (2026-10-04) à 0.17.0 (2026-10-19) saíram seis minors em 15 dias. A 0.12.0 (2026-10-14) removeu useHotkeySequence e useHotkeySequences, renomeou Hotkey para HotkeyChord e SequenceManager para HotkeyManager; a 0.9.0 (2026-09-21) fez os recorders usar por predefinição códigos de teclas físicas. A documentação pede aos early adopters que ajudem com “edge cases across multiple keyboard layouts, locales, and operating systems”
  • Jovem: o repositório foi criado em 2026-01, pelo que há muito menos experiência na comunidade do que com as alternativas
  • As predefinições tomam conta da tecla premida: preventDefault e stopPropagation estão ativos, pelo que um atalho registado esconde a ação do browser e outros listeners
  • A biblioteca oferece as ferramentas de acessibilidade mas não impõe o WCAG 2.1.4 — essa regra é nossa

5.3.3 (2026-06-26), MIT, mantido ativamente.

Prós: um major estável com um historial muito mais longo; useHotkeys com scopes (HotkeysProvider, toggleScope); hotkeys desligados em campos de formulário por predefinição (enableOnFormTags); sequências (sequenceTimeoutMs, 1000 ms por predefinição); useRecordHotkeys para ecrãs de definições; uma opção description para painéis de ajuda. Contras: a sua documentação não lista um registo declarado com remapeamento pelo utilizador e verificação de conflitos, nem helpers para cheat sheets — teríamos de os construir. O preventDefault vem desligado por predefinição, pelo que cada Mod+S precisa de o ter definido. É a escolha se o risco de pré-1.0 pesar mais do que essas funcionalidades, e o nosso plano B se o Hotkeys desiludir.

4.0.1 (2026-09-25), MIT, “~1KB” segundo o seu README.

Prós: minúsculo; $mod para o modificador da plataforma; sequências ("g i", timeout de 1000 ms); ignora por predefinição eventos de input, textarea, select e contenteditable; devolve uma função de unsubscribe. Contras: agnóstico de framework, pelo que escrevemos o useEffect e a limpeza a cada utilização. O seu README não tem scopes, recorder, remapeamento nem deteção de conflitos, e não tem noção de widgets com foco para além das tags de formulário.

1.6.5; Apache-2.0 com a exceção LLVM.

Prós: estabelecido há muito — há versões desde pelo menos 2015. Contras: a última versão no npm (1.6.5) é de 2020-01-23 e o último push para o seu repositório é de 2023-03-15. Na prática sem manutenção; excluído só por isso.

Prós: zero dependências. Contras: a lista de armadilhas do próprio Hotkeys é a lista que iríamos redescobrir — disposições de teclado, sistemas operativos, atalhos personalizados, scopes em conflito, ignorar inputs, repetição de teclas, composição IME e teclas que parecem presas depois de mudar de janela. As cheat sheets e as definições deixam de coincidir com os atalhos reais.

  • Chords com modificador e teclas que não são de caracteres por predefinição; atalhos de teclas de caracteres só atrás de um interruptor. Os chords com Mod, o Escape e as teclas de função estão isentos do WCAG 2.1.4; ?, / ou G H precisam de uma definição do utilizador. Uma só regra cumpre o critério
  • Vale tudo. O mais rápido para utilizadores avançados, mas falha o WCAG 2.1.4 e dispara por engano para utilizadores de entrada por voz, cujo ditado chega como teclas premidas

O TanStack Hotkeys, através do @tanstack/react-hotkeys fixado em versões exatas, é o padrão da empresa para atalhos de teclado em todas as novas apps. Todos os atalhos seguem as regras de acessibilidade abaixo.

Em concreto:

  • Pacote: @tanstack/react-hotkeys 0.16.0, instalado com bun add --exact. Reexporta o core e fixa ele próprio o @tanstack/hotkeys 0.17.0, pelo que o core não se instala à parte
  • Fixar versões exatas (sem ^) enquanto o Hotkeys for pré-1.0; atualizar de forma deliberada, como no ADR-0008. O Renovate (ADR-0016) abre o PR mas nunca o faz merge automático; uma pessoa lê primeiro as notas de lançamento — as da 0.12.0 trazem uma tabela de antes e depois de todas as APIs renomeadas. Reavaliar na 1.0
  • Registar no componente que é dono da ação com useHotkey ou useHotkeys; o atalho existe enquanto o componente estiver montado. Escrever os bindings com Mod, nunca Control+S nem Meta+S, para que cada plataforma tenha o seu modificador. Tipá-los como HotkeyChord ou Hotkey
  • Todo o atalho que um utilizador vê é um app hotkey. Declará-lo com defineAppHotkeys num módulo só de declarações, com meta.name e meta.group, e passá-lo ao HotkeysProvider da raiz. Os ids são um contrato estável — os hotkeys personalizados guardados são indexados por eles. Teclas locais pontuais, como o Escape num popup, podem ser um useHotkey simples
  • Os scopes seguem as regiões da UI. Os atalhos da página ficam em global. Cada diálogo, popover ou paleta envolve os seus hotkeys em HotkeysProvider scope="…" exclusive, com um nome de scope único
  • O Base UI é dono das suas teclas (ADR-0011):
    • Diálogos: o Base UI move o foco para dentro, faz o Tab circular e “Esc requests close”. Fechar com Escape através do onOpenChange do diálogo, não com um segundo hotkey Escape. Os atalhos só do diálogo vivem num scope exclusivo, para que os atalhos da página fiquem suspensos enquanto está aberto
    • Menus, selects, separadores e outros widgets mantêm setas, Home, End, Enter, Espaço e typeahead. O Hotkeys ignora as teclas que um widget ARIA com foco usa, mas nunca ignora chords com Control, Meta ou Alt. Logo: nenhum atalho de seta, Enter ou Space simples ao nível do documento; uma região que precise deles recebe um ref de target para o seu próprio elemento
    • Campos de texto: deixar o ignoreInputs na predefinição, para que as teclas simples nunca disparem enquanto se escreve. O Enter é a submissão nativa do formulário (ADR-0013), nunca um hotkey
    • Usar capture: true apenas com uma razão escrita — corre antes do handler do próprio widget com foco
  • Regras de acessibilidade (WCAG 2.1.4, Nível A, e as WAI-ARIA Authoring Practices):
    1. Os atalhos de teclas de caracteres precisam de um interruptor. Qualquer atalho feito de uma tecla de letra, número, pontuação ou símbolo — sozinha, com Shift ou numa sequência — tem uma de: uma definição do utilizador que o desativa (enabled), remapeamento pelo utilizador (app hotkeys com o recorder) ou um target para só funcionar com foco. A predefinição é uma só definição “Atalhos de tecla única” que os controla a todos. Os chords com Mod, Control ou Alt, e teclas como Escape e as de função, estão isentos
    2. Nunca o único caminho. Todas as ações continuam acessíveis pela ordem normal de foco e por controlos visíveis
    3. Evitar conflitos de teclas com o sistema operativo, o browser e as tecnologias de apoio. Nenhum modificador com Tab, Enter, Space ou Escape; nenhum Meta com uma só outra tecla; nenhum Alt com uma tecla de função; nada construído sobre Caps Lock, Insert ou Scroll Lock (os leitores de ecrã usam-nas como modificadores); no macOS, nenhuma combinação Control+Option; ter em atenção a barra de endereços do browser, o refresh, os marcadores e o histórico, e a pesquisa na página. Os browsers ficam com Mod+T, Mod+N e Mod+W de qualquer forma. Um conflito intencional, como Mod+S num editor, só é permitido para uma função da app muito usada quando a função do browser raramente é usada e tem outro caminho — o teste das próprias Practices
    4. Descobrível. Mostrar o atalho no próprio controlo — <kbd> (o Kbd do shadcn/ui) com formatForDisplay do mesmo binding que está registado — e ter um diálogo de ajuda construído a partir de useAppHotkeyList e aberto a partir de um controlo visível. O atalho do diálogo de ajuda é um extra, nunca a única entrada
    5. Ecrã de definições: o interruptor de tecla única, mais remapeamento com useHotkeyRecorder, findConflicts e useCustomHotkeys. Onde se guardam os hotkeys personalizados é uma escolha do produto, mas têm de estar carregados antes de o provider montar: ele lê customHotkeys uma só vez
  • Renderização no servidor: o Hotkeys não liga listeners no servidor, e o Mod é detetado a partir de navigator — o servidor não sabe qual é a plataforma do utilizador. Renderizar as etiquetas dos atalhos depois da hidratação (useHydrated() de @tanstack/react-router, como no ADR-0013), não no HTML do servidor, e registar só através de hooks, nunca ao nível do módulo
  • Testes (ADR-0014): o Playwright prime ControlOrMeta+K e semelhantes, pelo que um só teste cobre ambas as plataformas. Cada produto testa que desligar os atalhos de tecla única os desativa, e que um diálogo aberto suspende os atalhos da página
  • SOLID: os hooks são donos do comportamento; os componentes ficam apresentacionais. Um botão ou item de menu recebe onSelect e uma etiqueta shortcut, e renderiza ambos. A página ou o hook da funcionalidade regista o hotkey e chama o mesmo handler que o clique usa — um handler, dois gatilhos. O módulo de declarações é a fonte única para o registo, o diálogo de ajuda e o ecrã de definições. Não há wrapper à volta do useHotkey: a API da biblioteca já é a abstração, e um wrapper limitar-se-ia a copiar a sua superfície. Reavaliar se as atualizações começarem a tocar em muitos ficheiros

Os fatores decisivos: os casos-limite de disposição de teclado, campos de entrada, widgets com foco e scopes tratados por nós; um registo declarado que alimenta atalhos, ajuda e remapeamento; e a sua documentação a tratar o WCAG 2.1.4. Aceitamos o risco de pré-1.0, que é superior ao do Pacer, e uma comunidade mais pequena.

Mais fácil:

  • Bindings tipados; Mod e as disposições de teclado funcionam entre plataformas sem código por sistema operativo
  • Escrever em campos, usar menus e listboxes e empilhar diálogos não dispara atalhos da página por engano
  • Uma só declaração alimenta o atalho, a sua etiqueta no ecrã, o diálogo de ajuda e o ecrã de remapeamento, pelo que não conseguem divergir
  • O WCAG 2.1.4 passa a ser uma definição e um teste por produto, e não uma discussão por atalho

Mais difícil:

  • Risco de pré-1.0, dito às claras. A documentação do Hotkeys chama-lhe beta, e ele move-se depressa: seis minors em 15 dias, com renomeações que quebram a API na 0.12.0 (2026-10-14) e na 0.9.0 (2026-09-21). Versões fixadas significam sem correções automáticas: cada atualização é um PR deliberado, verificado contra as notas de lançamento, e os tutoriais mais antigos, e as ferramentas de IA, vão mostrar APIs que já não existem (useHotkeySequence)
  • Gatilhos de reavaliação: o Hotkeys chega à 1.0 (planear a janela de atualização e rever a política de fixação); uma atualização quebra algo que não conseguimos corrigir dentro do próprio PR de atualização; ou o projeto deixa de lançar versões. A saída custa mais do que a do Pacer: sair significa reconstruir app hotkeys, scopes e remapeamento sobre o react-hotkeys-hook (5.x estável) ou o tinykeys. É por isso que as declarações vivem num só módulo
  • A acessibilidade é regra nossa, não da biblioteca. Rever à procura de atalhos de tecla simples sem o interruptor, e de ações acessíveis só por atalho
  • O Hotkeys decide o que um widget com foco mantém pelo papel ARIA. Não verificámos cada componente do Base UI contra essa tabela, pelo que se testa antes de lançar um atalho de tecla simples numa página com menus ou listboxes
  • Os ids dos app hotkeys são um contrato: renomear um deixa cair o hotkey personalizado guardado pelos utilizadores para esse id, e o Hotkeys ignora ids desconhecidos com um aviso
  • Os atalhos do browser e do sistema operativo não se podem sobrepor; algumas combinações desejadas vão estar ocupadas
  • As etiquetas dos atalhos dependem da plataforma do utilizador; impressas no HTML do servidor podem discordar do cliente, pelo que se renderizam depois da hidratação

Do que abdicamos: do major estável e do historial do react-hotkeys-hook, da dimensão mínima do tinykeys e da simplicidade sem dependências de um listener escrito à mão. Aceite — um registo declarado com scopes, remapeamento e orientação de acessibilidade justifica uma dependência fixada em pré-1.0 e uma atualização planeada na 1.0.