Componentes
35. Menu / Dropdown
seed-componentes.md v1.43 · §35seção 37 de 9835-menu-dropdown-estavel-v0-38-mn1-mn8-bloco-5-5b.md · MD5 86a1b775Título completo no canon: Menu / Dropdown — estável (v0.38) · MN1–MN8 · Bloco 5/5B · suite suite-navegacao.mjs 58/58 ✓ · contraste-navegacao.py ✅ · regressão 349 verdes · gate visual aprovado 2026-08-06
Base (3 rodadas encadeadas, 2026-08-06, ~24 fontes): R1 — NN/g Menu-Design Checklist (hover-open frustra; Fitts; 1 nível; "gray out em vez de remover + explique o porquê"), NN/g Dropdowns (comando ≠ seleção de atributo; Mac HIG proíbe dropdown-box p/ comandos), NN/g listbox×dropdown, NN/g expandable menus, Carbon Menu (menu = ações; form/filtro = dropdown; destrutivo separado no FIM; atalhos exibidos), Carbon Overflow Menu usage+a11y, Polaris ActionList (dentro de popover; verbo+substantivo; destructive; seções; help text). R2 — Radix DropdownMenu (menu button APG 1:1: roving, typeahead, checkable, Esc — shadcn herda), Wootonn (setas do menu × cursor do input = conflito estrutural de ARIA; saída é dialog/popover), Ariakit. R3 — corpus Adrian Roselli 2017–2026 ("Don't Use ARIA Menu Roles for Site Nav" / "Be Careful Using 'Menu'" / "Link + Popover Navigation") + APG Disclosure Navigation Menu (Higley): navegação usa link+disclosure, nunca roles de menu — posição hoje do próprio APG; Terrill Thompson (1ª regra do ARIA); Piccalilli; Fiori Menu Button + Action Sheet (menu button aposentou o action sheet; 1 ação = botão; nunca ícone-só; importância top-down). Consolidado aprovado pelo Rafael em 2026-08-06.
35.1 Decisões MN1–MN8#
| # | Decisão | Porquê | Descartado |
|---|---|---|---|
| MN1 | Fronteira semântica de 4 saídas: itens executam AÇÃO = menu §35 (role="menu", APG menu button) · itens NAVEGAM = disclosure de links (botão aria-expanded + popover §24 com <a>, zero roles de menu — o shell F6 consome isto; Tab navega E ↓↑ opcionais do APG sem wrap) · itens selecionam VALOR = select/combobox (B2) · conteúdo rico/inputs = popover §24. PROIBIDO input dentro de role="menu" |
Roselli→APG disclosure nav; NN/g/Mac HIG; Carbon; Wootonn (conflito de setas) | role=menu p/ navegação (anti-padrão testado); componente "faz-tudo" |
| MN2 | Trigger: botão §1 + aria-haspopup="menu" + aria-expanded + caret; caso nomeado overflow "⋯" (icon-button nomeado). 1 ação = botão simples. Split button fora da v1 (fronteira) |
Carbon/Fiori; sem demanda p/ split | Split na v1; abrir por hover (NN/g) |
| MN3 | Teclado APG integral: Enter/Espaço/↓ abrem focando o 1º; ↑ abre no último; setas com wrap; Home/End; typeahead; Esc fecha→trigger; ativar AÇÃO executa, fecha e devolve o foco ao trigger | APG; Radix 1:1 | Subconjunto sem typeahead |
| MN4 | Itens: menuitem verbo+substantivo; ícone sempre com rótulo; atalho no trailing (JetBrains Mono + aria-keyshortcuts); menuitemcheckbox/menuitemradio p/ opções de exibição — alternáveis PERMANECEM abertos ao alternar (opção ≠ ação; ajustar 2+ opções sem reabrir) |
Polaris; Fiori; Carbon; Radix (padrão preventDefault em checkable é prática da stack) | Checkable fora da v1; fechar ao alternar (pune o segundo ajuste — Fitts) |
| MN5 | Organização: grupos role="group" nomeados; separadores (divider §27); destrutivo SEMPRE no fim, após separador, vermelho interface; importância top-down; máx ~7 itens, 1 nível — SUBMENU PROIBIDO na v1 (fronteira: demanda real → pesquisa própria) |
Carbon/Polaris; NN/g (2 níveis frustram; Fitts); vermelho interface-only | Submenu; destrutivo no topo |
| MN6 | Item indisponível — EXCEÇÃO DOCUMENTADA da linha "sem disabled": visível, aria-disabled="true", navegável pelas setas (descobrível por AT), texto text-muted deliberadamente legível (4.74 — não usamos a isenção da WCAG), motivo em help text. Critério da exceção: menu é INVENTÁRIO de comandos — remover muda o mapa mental; a aba (TB8) não é inventário |
NN/g literal; APG aria-disabled focável | Remover o item (NN/g); disabled nativo fora das setas (invisível p/ AT — divergência declarada com Carbon) |
| MN7 | Superfície: popover §24 (z 75, light-dismiss, flip); largura mín. 200px; itens ≥44px; <640px tray por herança Y4, sem regra nova; ativou ação → fecha; feedback (toast) é do consumidor | Carbon; §24 resolveu; Fiori (superfície plena no phone) | Action sheet como componente (Fiori o aposentou) |
| MN8 | Sem live region própria — estado vive nos roles; anúncio do resultado pertence ao consumidor | Regra do um no canal sonoro (linha CN10/CN12) | Live region de abre/fecha (dupla fala) |
35.2 Anatomia, tokens e fronteiras#
Trigger: botão §1 (qualquer variante) + caret rotativo. Painel: overlay branco/#273137, radius 10, sombra overlay, padding 6. Item: 44px, radius 6, ícone 18px decorativo, rótulo 600, atalho mono ci-600/ci-300, check action-primary (alternáveis). Destrutivo: --seed-danger-item ve-700 light / ve-200 dark. Pares novos medidos (contraste-navegacao.py ✅): 7.18 · 6.61 · 8.67 · 4.74 · 6.94. Fora do DS: split button (fronteira MN2) · submenu (MN5) · context menu por clique-direito (sem demanda; se surgir, herda esta spec com trigger próprio) · navegação do shell (F6, consome a saída disclosure do MN1).
35.3 Os 7 testes#
- Pares novos medidos ✅. 2. Destrutivo = posição+separador+ícone+cor (nunca só cor); indisponível = aria-disabled+motivo (nunca só cinza). 3. Grayscale: posição/separador/check carregam. 4. Menu de ações × disclosure de links a 1s (roles×links). 5. Regra do um: um nível, uma live-region nenhuma, um canal de feedback. 6. Estado alternável visível (check). 7. 360px: tray herdado, itens 44px, overflow "⋯" 44px. (Suite 5B em
suite-navegacao.mjs— total 57/57 ✓ após erratas do gate visual: Tab/Esc/focusout no menu + setas opcionais APG no disclosure; tray renderizado, flip e sombra = gate visual.)
Também cita o §35: banco-dados.