Menu suspenso
Menu de ações ancorado num botão. Vira de lado quando não cabe, fecha no clique fora, no
Escape e quando o foco sai, e anda pelas setas do teclado. Os itens podem vir escritos
no template, que é o caminho quando é o servidor quem sabe o que a pessoa pode fazer.
Exemplos
O mesmo componente, com posição e conteúdo diferentes.
Alinhado pela direita
data-placement="bottom-end": para o botão encostado na borda direita.
Pelo lado
data-placement="right-start" abre ao lado, como um submenu.
Com ícones
O ícone vai em .tuc-dropdown__icon e fica esmaecido; no item de perigo, acompanha o vermelho.
Botão de ícone
O "mais ações" de uma linha de tabela. Sem texto visível, o aria-label é obrigatório.
As posições são as mesmas do tooltip: lado top, bottom, left ou
right, e alinhamento start, center ou end. O padrão é
bottom-start. Quando o lado pedido não cabe e o oposto cabe melhor, o menu vira; e ele
nunca vaza da tela, desliza para dentro.
Como usar
Um botão com data-tuc-dropdown apontando para o painel, e o painel logo abaixo, com
hidden. Ele inicializa sozinho, inclusive o que chegar depois por HTMX.
<button type="button" class="tuc-btn is-outline" data-tuc-dropdown="#actions">Ações</button>
<div class="tuc-dropdown" id="actions" hidden>
<div class="tuc-dropdown__label">Contrato</div>
<button type="button" class="tuc-dropdown__item">
<span class="tuc-dropdown__text">Editar</span>
<span class="tuc-dropdown__shortcut">⌘E</span>
</button>
<a class="tuc-dropdown__item" href="/contracts/12/">
<span class="tuc-dropdown__text">Abrir</span>
</a>
<hr class="tuc-dropdown__separator">
<button type="button" class="tuc-dropdown__item is-danger">
<span class="tuc-dropdown__text">Excluir</span>
</button>
</div>O hidden é obrigatório
O script vem com defer e só roda depois de a página ser desenhada. Sem hidden, o menu aparece aberto no meio da página até lá. Na inicialização o painel é tirado do fluxo e passa a ser posicionado pelo componente, que tira o hidden sozinho.
Peças do painel
| Classe ou atributo | Para quê |
|---|---|
.tuc-dropdown | O painel. Precisa de id, para o gatilho apontar, e de hidden |
.tuc-dropdown__item | Um item. <a href> para navegar, <button type="button"> para agir |
.tuc-dropdown__text | O rótulo; corta com reticências quando não cabe |
.tuc-dropdown__icon | Ícone à esquerda, 16px, na cor de apoio |
.tuc-dropdown__shortcut | Atalho à direita, com números de largura fixa para as teclas alinharem |
.tuc-dropdown__label | Título de grupo, que não é clicável |
.tuc-dropdown__separator | Linha entre grupos, num <hr> |
is-danger | No item: ação destrutiva, em vermelho |
aria-disabled="true" | No item: desativado. As setas pulam, o clique não fecha o menu |
Item com href é um <a> de verdade. O clique do meio continua abrindo em outra aba, e
o menu não rouba o menu de contexto do navegador. Clicar em qualquer item fecha o menu.
No template do Django
As ações que dependem de permissão ficam de fora no servidor, e não escondidas no navegador. Numa listagem, o
id do painel leva a chave da linha para cada menu apontar para o seu.
{% for contract in contracts %}
<button type="button" class="tuc-btn is-outline is-icon is-sm" aria-label="Ações"
data-tuc-dropdown="#actions-{{ contract.pk }}" data-placement="bottom-end">...</button>
<div class="tuc-dropdown" id="actions-{{ contract.pk }}" hidden>
<a class="tuc-dropdown__item" href="{% url 'contract-edit' contract.pk %}">
<span class="tuc-dropdown__text">Editar</span>
</a>
{% if perms.contracts.delete_contract %}
<hr class="tuc-dropdown__separator">
<a class="tuc-dropdown__item is-danger" href="{% url 'contract-delete' contract.pk %}">
<span class="tuc-dropdown__text">Excluir</span>
</a>
{% endif %}
</div>
{% endfor %}Em JavaScript
Quando os itens são calculados na hora. O componente monta o painel, e cada item recebe o que fazer ao ser escolhido.
const menu = new Tucano.Dropdown('#actions', {
placement: 'bottom-end',
items: [
{ label: 'Contrato' },
{ text: 'Editar', icon: 'M12 20h9M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4z', shortcut: '⌘E', onClick: () => editContract() },
{ text: 'Duplicar', icon: Tucano.ICON_COPY, onClick: (dropdown) => duplicateContract() },
{ text: 'Abrir', href: '/contracts/12/' },
{ text: 'Arquivar', disabled: true },
{ separator: true },
{ text: 'Excluir', variant: 'danger', onClick: () => deleteContract() },
],
});
menu.open();
menu.close();
menu.toggle();
menu.destroy(); // fecha e solta os listeners| Chave do item | Para quê |
|---|---|
text | O rótulo |
icon | O d de um path SVG de 24×24, como os Tucano.ICON_* |
shortcut | Texto do atalho, à direita. Só mostra: quem liga a tecla é você |
onClick | Chamado ao escolher, com a instância do menu |
href | Faz o item ser um <a> |
variant | 'danger' vira is-danger |
disabled | Desativa: aria-disabled="true" e nenhum onClick |
separator | Sozinho, { separator: true } vira a linha |
label | Sozinho, { label: 'Seção' } vira título de grupo |
closeOnPick: false mantém o menu aberto depois da escolha, para itens que ligam e desligam
alguma coisa.
Teclado e acessibilidade
O gatilho ganha aria-haspopup="menu" e aria-expanded, o painel é um
menu e cada item um menuitem. Abrir leva o foco para dentro, e fechar o devolve ao gatilho.
| Tecla | Ação |
|---|---|
Enter Espaço | No gatilho: abre com o primeiro item já destacado |
| Clique | No gatilho: abre sem destacar nada; a primeira seta destaca o primeiro item |
↓ | No gatilho: abre direto no primeiro item |
↑ | No gatilho: abre direto no último item |
↑ ↓ | No menu: anda entre os itens, dando a volta e pulando os desativados |
Home End | Primeiro e último item |
Esc | Fecha e devolve o foco ao gatilho |
Tab | Fecha, e o foco segue a ordem da página a partir do gatilho |
Os itens ficam fora do Tab de propósito
Cada item recebe tabindex="-1". Dentro de um menu quem anda é a seta; com os itens tabuláveis, o Tab sairia do menu um item por vez, que é exatamente o que o padrão de menu evita. E o foco volta ao gatilho porque, sem isso, quem navega por teclado fecharia o menu e cairia no começo da página.
Além do Escape, o menu fecha com clique fora, quando o foco vai para outro lugar e quando o gatilho sai da
tela na rolagem. O realce do item acompanha o foco, e não só o ponteiro: é a seta que move o foco, e sem isso quem
usa o teclado não veria onde está.
API
Gerada do código a cada build — se algo não está aqui, não existe.
[data-tuc-dropdown]new Tucano.Dropdown(alvo, opcoes)data-placement data-tuc-dropdownopenAt open close toggle destroyOpções
| Opção | Padrão | Para quê |
|---|---|---|
placement | 'bottom-start' | |
items | null | [{ text, icon, shortcut, onClick, href, variant, disabled, separator, label }] |
closeOnPick | true |