Menú desplegable
Menú de acciones anclado a un botón. Cambia de lado cuando no cabe, se cierra con un clic fuera, con
Escape y cuando el foco sale, y se recorre con las flechas del teclado. Los ítems pueden venir escritos
en la plantilla, que es el camino cuando es el servidor quien sabe lo que la persona puede hacer.
Ejemplos
El mismo componente, con posición y contenido distintos.
Alineado a la derecha
data-placement="bottom-end": para el botón pegado al borde derecho.
Hacia un lado
data-placement="right-start" abre al lado, como un submenú.
Con iconos
El icono va en .tuc-dropdown__icon y queda atenuado; en el ítem de peligro, acompaña al rojo.
Botón de icono
El "más acciones" de una fila de tabla. Sin texto visible, el aria-label es obligatorio.
Las posiciones son las mismas del tooltip: lado top, bottom, left o
right, y alineación start, center o end. La predeterminada es
bottom-start. Cuando el lado pedido no cabe y el opuesto cabe mejor, el menú se da la vuelta; y
nunca se sale de la pantalla, se desliza hacia dentro.
Cómo usar
Un botón con data-tuc-dropdown apuntando al panel, y el panel justo debajo, con
hidden. Se inicializa solo, incluso lo que llegue después por HTMX.
<button type="button" class="tuc-btn is-outline" data-tuc-dropdown="#actions">Acciones</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">Eliminar</span>
</button>
</div>El hidden es obligatorio
El script viene con defer y solo se ejecuta después de que la página se dibuja. Sin hidden, el menú aparece abierto en medio de la página hasta entonces. Al inicializarse, el panel sale del flujo y pasa a ser posicionado por el componente, que quita el hidden por sí solo.
Piezas del panel
| Clase o atributo | Para qué |
|---|---|
.tuc-dropdown | El panel. Necesita un id, para que el disparador apunte a él, y hidden |
.tuc-dropdown__item | Un ítem. <a href> para navegar, <button type="button"> para actuar |
.tuc-dropdown__text | La etiqueta; se corta con puntos suspensivos cuando no cabe |
.tuc-dropdown__icon | Icono a la izquierda, 16px, en el color secundario |
.tuc-dropdown__shortcut | Atajo a la derecha, con números de ancho fijo para que las teclas se alineen |
.tuc-dropdown__label | Título de grupo, que no es clicable |
.tuc-dropdown__separator | Línea entre grupos, en un <hr> |
is-danger | En el ítem: acción destructiva, en rojo |
aria-disabled="true" | En el ítem: desactivado. Las flechas lo saltan y el clic no cierra el menú |
Un ítem con href es un <a> de verdad. El clic central sigue abriendo en otra pestaña, y
el menú no le roba el menú contextual al navegador. Hacer clic en cualquier ítem cierra el menú.
En la plantilla de Django
Las acciones que dependen de permisos se quedan fuera en el servidor, y no escondidas en el navegador. En un listado, el
id del panel lleva la clave de la fila para que cada menú apunte al suyo.
{% for contract in contracts %}
<button type="button" class="tuc-btn is-outline is-icon is-sm" aria-label="Acciones"
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">Eliminar</span>
</a>
{% endif %}
</div>
{% endfor %}En JavaScript
Cuando los ítems se calculan en el momento. El componente monta el panel, y cada ítem recibe qué hacer al ser elegido.
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: 'Archivar', disabled: true },
{ separator: true },
{ text: 'Eliminar', variant: 'danger', onClick: () => deleteContract() },
],
});
menu.open();
menu.close();
menu.toggle();
menu.destroy(); // cierra y suelta los listeners| Clave del ítem | Para qué |
|---|---|
text | La etiqueta |
icon | El d de un path SVG de 24×24, como los Tucano.ICON_* |
shortcut | Texto del atajo, a la derecha. Solo lo muestra: quien asocia la tecla eres tú |
onClick | Se llama al elegir, con la instancia del menú |
href | Hace que el ítem sea un <a> |
variant | 'danger' se convierte en is-danger |
disabled | Desactiva: aria-disabled="true" y ningún onClick |
separator | Solo, { separator: true } se convierte en la línea |
label | Solo, { label: 'Sección' } se convierte en título de grupo |
closeOnPick: false mantiene el menú abierto después de elegir, para ítems que activan y desactivan
algo.
Teclado y accesibilidad
El disparador recibe aria-haspopup="menu" y aria-expanded, el panel es un
menu y cada ítem un menuitem. Abrir lleva el foco hacia dentro, y cerrar lo devuelve al disparador.
| Tecla | Acción |
|---|---|
Enter Espacio | En el disparador: abre con el primer ítem ya destacado |
| Clic | En el disparador: abre sin destacar nada; la primera flecha destaca el primero |
↓ | En el disparador: abre directamente en el primer ítem |
↑ | En el disparador: abre directamente en el último ítem |
↑ ↓ | En el menú: recorre los ítems, dando la vuelta y saltando los desactivados |
Home End | Primer y último ítem |
Esc | Cierra y devuelve el foco al disparador |
Tab | Cierra, y el foco sigue el orden de la página a partir del disparador |
Los ítems quedan fuera del Tab a propósito
Cada ítem recibe tabindex="-1". Dentro de un menú, quien recorre es la flecha; con ítems tabulables, Tab saldría del menú de ítem en ítem, que es justamente lo que el patrón de menú evita. Y el foco vuelve al disparador porque, sin eso, quien navega con teclado cerraría el menú y caería al principio de la página.
Además de Escape, el menú se cierra con un clic fuera, cuando el foco va a otro lugar y cuando el disparador sale de
la pantalla al desplazarse. El resaltado del ítem sigue al foco, y no solo al puntero: es la flecha la que mueve el foco, y sin eso quien
usa el teclado no vería dónde está.
API
Generada a partir del código en cada build — si algo no está aquí, no existe.
[data-tuc-dropdown]new Tucano.Dropdown(alvo, opcoes)data-placement data-tuc-dropdownopenAt open close toggle destroyOpciones
Las notas de esta tabla salen de los comentarios del código fuente, que están escritos en portugués.
| Opción | Por defecto | Para qué |
|---|---|---|
placement | 'bottom-start' | |
items | null | [{ text, icon, shortcut, onClick, href, variant, disabled, separator, label }] |
closeOnPick | true |