Tucano v0.37.2

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 atributoPara qué
.tuc-dropdownEl panel. Necesita un id, para que el disparador apunte a él, y hidden
.tuc-dropdown__itemUn ítem. <a href> para navegar, <button type="button"> para actuar
.tuc-dropdown__textLa etiqueta; se corta con puntos suspensivos cuando no cabe
.tuc-dropdown__iconIcono a la izquierda, 16px, en el color secundario
.tuc-dropdown__shortcutAtajo a la derecha, con números de ancho fijo para que las teclas se alineen
.tuc-dropdown__labelTítulo de grupo, que no es clicable
.tuc-dropdown__separatorLínea entre grupos, en un <hr>
is-dangerEn 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 ítemPara qué
textLa etiqueta
iconEl d de un path SVG de 24×24, como los Tucano.ICON_*
shortcutTexto del atajo, a la derecha. Solo lo muestra: quien asocia la tecla eres tú
onClickSe llama al elegir, con la instancia del menú
hrefHace que el ítem sea un <a>
variant'danger' se convierte en is-danger
disabledDesactiva: aria-disabled="true" y ningún onClick
separatorSolo, { separator: true } se convierte en la línea
labelSolo, { 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.

TeclaAcción
Enter EspacioEn el disparador: abre con el primer ítem ya destacado
ClicEn 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 EndPrimer y último ítem
EscCierra y devuelve el foco al disparador
TabCierra, 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.

Marcado
[data-tuc-dropdown]
En JS
new Tucano.Dropdown(alvo, opcoes)
Atributos
data-placement data-tuc-dropdown
Métodos
openAt open close toggle destroy

Opciones

Las notas de esta tabla salen de los comentarios del código fuente, que están escritos en portugués.

OpciónPor defectoPara qué
placement'bottom-start'
itemsnull[{ text, icon, shortcut, onClick, href, variant, disabled, separator, label }]
closeOnPicktrue