Dropdown
Em andamentoMenu de ações que abre de um botão — o padrão Menu Button do WAI-ARIA: um ponto de Tab para o menu inteiro, setas para andar, digitação para buscar e Esc para fechar.
- Função
- Formulário e ação
- Também chamado de
- menu, menu de ações, mais opções, kebab, menu suspenso
- Mora em
- app/components/molecules/Dropdown.tsx
- Última mudança
- 2026-09-30
Prévia
A peça ao vivo, vinda da ficha, na largura escolhida. Em larguras fixas, ela roda num quadro do tamanho de uma tela de verdade.
Na largura da página: 0px
Variantes na ficha
Lidas da ficha no build, na ordem do palco. Cada uma abre sozinha, sem o styleguide em volta — é o endereço que o palco carrega.
Quando usar
Para agrupar ações de um item ou de uma tela que não cabem como botões soltos — editar, duplicar, excluir.
Faça
- Dê um
triggerLabelque diga o que abre: "Ações do curso", não "Opções". - Ponha ícone no item destrutivo (
tone="destructive") — a cor sozinha não basta. - Com mais de seis ações, organize em grupos com subtítulo:
{ id, group: "Compartilhar", items }(E63). O separador{ id, separator: true }só divide para quem vê. - Use
placementquando o botão estiver no fim da linha (bottom-end); perto do pé da janela ele inverte sozinho (E63). - Para o "⋯" de cartão e de linha,
triggerIconcom umtriggerLabelque diga de quê: "Mais ações do curso" (E63).
Evite
- Usar como seletor de valor: o item não fica marcado — não há
menuitemradio. - Esconder ação principal dentro do menu.
- Item desabilitado sem motivo à vista: ele continua focável e anunciado, mas não diz por que não funciona.
Quando não usar. Para escolher um valor num formulário, use o Select. Para navegar entre páginas, links (a Navbar tem os seus). Para duas ou três ações, botões à vista (Button).
Nota de uso
E63: acionador só-ícone (triggerIcon, com o triggerLabel como nome), grupos com subtítulo (role="group" nomeado) e inversão quando não cabe na janela. Declara role="menu" com foco roving: um único ponto de tabulação no menu inteiro, setas circulando, Home/End, e digitação buscando por prefixo (com acento — a alcança "Ações"). Isso não contradiz a decisão da Navbar: lá o papel foi removido porque prometia um item focável por vez e entregava doze links tabuláveis; aqui o contrato vem inteiro. Para escolher um valor, use o Select — este é menu de ações. Não implementado, e dito em voz alta: item de marcação (menuitemradio), que filtro rápido e ordenação pediriam. A lista entra esmaecendo e descendo 4px em 150ms, só com movimento liberado.
Anatomia
As partes, na ordem em que o olho — e o teclado — passam.
- Botão — Um Button (`triggerVariant`) com o rótulo e uma seta — ou, com `triggerIcon`, só o ícone em 44×44, com o `triggerLabel` como `aria-label`; anuncia `aria-haspopup="menu"` e `aria-expanded`.
- Painel — `role="menu"`, a partir de 14rem de largura, posicionado pelo `placement`.
- Item — `role="menuitem"`, 44px de altura, com ícone opcional.
- Separador — `role="separator"` entre grupos.
- Grupo — `role="group"` nomeado pelo subtítulo (12px, negrito, caixa alta, `--muted-foreground`), com os itens dentro (E63).
Estados
Os estados que a peça tem no código, e o que muda em cada um.
| Estado | O que muda |
|---|---|
| Fechado | Só o botão; aria-expanded="false". |
| Aberto | Painel à mostra, aria-expanded="true", e o foco no item ativo. Entra esmaecendo e descendo 4px em 150ms, só com movimento liberado. |
| Item em foco | Anel de foco por dentro do item (ring-inset). |
| Item destrutivo | tone="destructive": texto vermelho de interface. |
| Item inativo | disabled: 50% de opacidade e aria-disabled, mas continua focável. |
| Desabilitado | disabled no Dropdown desabilita o botão. |
| Invertido | Sem espaço embaixo e com espaço em cima, o painel abre para cima (data-posicao); vale também ao contrário (E63). |
Comportamento
| Situação | O que acontece |
|---|---|
| Item escolhido | Chama o onSelect do item, fecha o menu e devolve o foco ao botão. |
| Clique fora | Fecha o menu — pelo mousedown fora e pelo foco que sai; testado na ficha (E142). |
| Abre perto do pé da janela | Mede o painel antes da pintura e, se ele sairia da tela, abre do outro lado — conferido no teste da ficha: pedido bottom-start, efetivo top-start, dentro da janela. Um ancestral com overflow: hidden ainda recorta: a medida é contra a janela, e o painel não usa portal. |
| Setas com grupos | Andam pelos itens de todos os grupos, na ordem, como se fosse uma lista só — conferido no teste da ficha. |
| Tab com o menu aberto | Fecha o menu e segue para o próximo elemento, sem prender o foco. |
| Modo controlado | Com open, quem manda é o consumidor; onOpenChange dispara em todo caminho de fechamento, nos dois modos. |
Relacionados
Como usar no código
A importação e o menor uso, lidos do arquivo. O … marca as props obrigatórias.
import { Dropdown } from '@/app/components/molecules/Dropdown';
<Dropdown items={…} triggerLabel={…}>…</Dropdown>Propriedades
Lidas do código da peça no build — não escritas à mão.
| Prop | Tipo | Padrão | Obrigatória | Descrição |
|---|---|---|---|---|
placement | 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'bottom-start' | não | Variante do cva. |
items | DropdownEntry[] | — | sim | — |
triggerLabel | string | — | sim | Obrigatório de propósito, pelo mesmo motivo que o title do Modal. Um acionador sem nome é anunciado apenas como "botão" — a pessoa sabe que algo abre e não sabe o quê. Isso é defeito de conteúdo, não opção de API, e o tipo é o lugar certo de dizer: o erro aparece ao escrever a chamada, não numa auditoria seis meses depois. |
triggerIcon | React.ReactNode | — | não | Acionador só-ícone (E63), para o menu "⋯" de cartão e de linha. O ícone é decorativo, o triggerLabel vira o aria-label — e a seta some, porque o ícone já diz que ali há mais. |
open | boolean | — | não | Presente = modo controlado. Ausente = o componente segura o estado. |
defaultOpen | boolean | false | não | — |
onOpenChange | (aberto: boolean) => void | — | não | Dispara em todo caminho de fechamento, nos dois modos. |
disabled | boolean | false | não | — |
triggerVariant | ButtonProps['variant'] | 'outline' | não | — |
Aceita também: Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'>.
Tokens de cor
Os tokens semânticos que o código da peça usa, com o valor de hoje em app/globals.css.
| Token | Valor | Usado como |
|---|---|---|
--border | #E5E5E5 | bg-borderborder-border |
--card | #FFFFFF | bg-card |
--destructive | #BA0517 | text-destructive |
--foreground | #1A1A1A | text-ink |
--muted | #F4F6F9 | bg-muted |
--muted-foreground | #595959 | text-muted-foreground |
--ring | var(--footer-red)), decisão da revisão 2. Se outra peça precisar
* dele como superfície, o nome vira de papel (ex.: --surface-brand-strong). */
--footer-red: #7D1B1B | ring-ring |
Teclado
| Tecla | Faz |
|---|---|
| EnterEspaço↓ | Com o foco no botão, abre o menu no primeiro item. |
| ↑ | Com o foco no botão, abre o menu no último item. |
| ↓↑ | Com o menu aberto, anda entre os itens — do último volta ao primeiro. |
| HomeEnd | Vai direto ao primeiro ou ao último item. |
| Letra | Vai ao item que começa com ela. Acento não importa: “a” encontra “Ações”. |
| EnterEspaço | Ativa o item e fecha o menu. |
| Esc | Fecha o menu e devolve o foco ao botão. |
| Tab | Sai do menu e o fecha, indo ao próximo elemento da página. É o comportamento de todo menu acessível: dentro dele se navega com as setas. |
Leitor de tela
O que é anunciado, e por quê.
- O nome do menu vem do botão (
aria-labelledby), e otriggerLabelé obrigatório no tipo — um botão sem nome seria anunciado só como "botão". - Um só item do menu recebe Tab por vez (foco roving): o menu inteiro é uma parada.
- Cada grupo é anunciado pelo nome ao entrar nele ("Compartilhar, grupo"); o subtítulo é
presentation, e não vira item.
O que sustenta o estado
Esta peça usa menu, divulgacao. O estado dela é o de quem já foi ouvido com leitor de tela nesses padrões — e não uma passada sobre esta peça em específico.
Falta ouvir: menu, divulgacao — ver docs/a11y-screen-reader-checklist.md.
Status e recursos
| Tipo | Recurso | Status |
|---|---|---|
| Código | app/components/molecules/Dropdown.tsx | Disponível |
| Ficha | Storybook (abre em outra aba) | Disponível |
| Teste de acessibilidade | axe em npm run test | No gate de todo commit |
| Leitor de tela | Passada da E33 | Falta ouvir |
| Design | Figma | Desativado até a Fase 2 |
Histórico
Os últimos commits que tocaram o arquivo, lidos do git no build.
0298af82026-09-30E147: fechamento da Entrega 11 — REPORT.md, verificação cross-agent e o gate estático estrito
O histórico completo está no GitHub (abre em outra aba).