Capas
Estado: EstableMenú de acciones
Las acciones por fila de las tablas del admin (Ver, Editar, Duplicar, Cancelar clase, Eliminar) o el menú de la cuenta. Crece desde el botón con resorte, flota en la capa superior (el overflow de la tabla no lo recorta), se voltea si no cabe y un resaltado se desliza a la opción bajo el cursor o el teclado. Enlaces, botones y envíos reales con button_to y confirmación.
- Resorte
- Capa superior
- Teclado
- Turbo
| Clase | Hora | Quién la da | Reservas | Acciones |
|---|---|---|---|---|
| Vinyasa Flow | 07:00 | Ana García | 9/12 | |
| Reformer Intermedio | 09:30 | Sofía Ruiz | 10/10 | |
| Mat Pilates | 19:00 | Ana García | 5/12 | |
Sin resultadosPrueba con otro filtro o con otra búsqueda. |
||||
| Clase | Hora | Quién la da | Reservas | Acciones |
|---|---|---|---|---|
| Vinyasa Flow | 07:00 | Ana García | 9/12 | |
| Reformer Intermedio | 09:30 | Sofía Ruiz | 10/10 | |
| Mat Pilates | 19:00 | Ana García | 5/12 | |
Sin resultadosPrueba con otro filtro o con otra búsqueda. |
||||
app/views/… (la demo de arriba)
<%# Sesiones de ejemplo (Ui::Demo::Session), sin consultas. En tu app las
acciones apuntan a sus rutas y cancelar es
m.with_item_button_to("Cancelar clase", session_path(s), method: :delete, confirm: "…")
Aquí cada envío manda un GET a esta ficha que actualiza el frame de abajo;
si cancelas la confirmación, no se envía. %>
<%= render Ui::ConfirmDialogComponent.new(id: "dropdown-menu-confirm") %>
<% tomorrow = Date.current + 1 %>
<% clases = [
[ 101, "Vinyasa Flow", "Ana García", "07:00", 12, 9 ],
[ 102, "Reformer Intermedio", "Sofía Ruiz", "09:30", 10, 10 ],
[ 103, "Mat Pilates", "Ana García", "19:00", 12, 5 ]
].map do |id, name, host, at, capacity, booked|
Ui::Demo::Session.new(id:, name:, host:, starts_at: Time.zone.parse("#{tomorrow} #{at}"), capacity:, booked:)
end %>
<% demo_form = { data: { turbo_frame: "dropdown-menu-result" } } %>
<div class="flex w-full max-w-xl flex-col items-center gap-5">
<%= render Ui::DataTableComponent.new(rows: clases, caption: "Clases de mañana", density: :compact, class: "w-full") do |t| %>
<% t.with_column("Clase") { |sc| tag.span(sc.name, class: "font-medium text-foreground") } %>
<% t.with_column("Hora", numeric: true) { |sc| sc.starts_at.strftime("%H:%M") } %>
<% t.with_column("Quién la da", hide_below: :md) { |sc| sc.host } %>
<% t.with_column("Reservas", numeric: true, hide_below: :sm) { |sc| "#{sc.booked}/#{sc.capacity}" } %>
<% t.with_column("Acciones", header_hidden: true, align: :right, width: "w-12") do |sc| %>
<% booked = sc.booked %>
<%= render Ui::DropdownMenuComponent.new(label: "Acciones de #{sc.name}", align: :end) do |m| %>
<% m.with_item_link("Ver reservas", href: "#dropdown-menu-ver-#{sc.id}", icon: :eye, hint: "#{booked}/#{sc.capacity}") %>
<% m.with_item_link("Editar", href: "#dropdown-menu-editar-#{sc.id}", icon: :pencil) %>
<% m.with_item_button_to("Duplicar a la próxima semana", ui_component_path("dropdown-menu"), method: :get, icon: :copy,
params: { hecho: "#{sc.name} duplicada al #{I18n.l(tomorrow + 7, format: "%-d %b")}" }, form: demo_form) %>
<% m.with_item_button("Copiar enlace", icon: :link,
data: { controller: "clipboard", clipboard_text_value: "https://lunaestudio.mx/clases/#{sc.id}", action: "clipboard#copy" }) %>
<% m.with_item_button("Check-in", icon: :check_in, disabled: true, hint: "Mañana") %>
<% m.with_item_separator %>
<% m.with_item_button_to("Cancelar clase", ui_component_path("dropdown-menu"), method: :get, icon: :ban, tone: :danger,
params: { hecho: "#{sc.name} cancelada" }, form: demo_form,
confirm: "¿Cancelar #{sc.name}? Se avisará a las #{booked} personas con reserva.") %>
<% m.with_item_button_to("Eliminar", ui_component_path("dropdown-menu"), method: :get, icon: :trash, tone: :danger,
params: { hecho: "#{sc.name} eliminada" }, form: demo_form,
confirm: "¿Eliminar #{sc.name}? Esta acción no se puede deshacer.") %>
<% end %>
<% end %>
<% end %>
<%= turbo_frame_tag "dropdown-menu-result", class: "block min-h-4 text-center text-xs text-muted-foreground" do %>
<% if params[:hecho].present? %>
<span class="text-foreground"><%= params[:hecho] %></span> · <span class="font-mono"><%= Time.current.strftime("%H:%M:%S") %></span>
<% else %>
Elige una acción de cualquier fila
<% end %>
<% end %>
<%# Menú de la cuenta: el slot trigger es el contenido del botón. En la app,
"Cerrar sesión" es href: destroy_user_session_path, method: :delete. %>
<%= render Ui::DropdownMenuComponent.new(label: "Cuenta de Ana García", width: "w-60") do |m| %>
<% m.with_trigger do %>
<span class="grid size-6 shrink-0 place-items-center rounded-full border border-border-subtle bg-raised text-3xs font-semibold text-muted-foreground" aria-hidden="true">AG</span>
<span class="truncate">Ana García</span>
<% end %>
<% m.with_header do %>
<p class="truncate text-sm font-medium text-foreground">Ana García</p>
<p class="truncate font-mono text-2xs text-muted-foreground">ana.garcia@lunaestudio.mx</p>
<% end %>
<% m.with_item_label("Cuenta") %>
<% m.with_item_link("Mi perfil", href: "#dropdown-menu-perfil", icon: :user) %>
<% m.with_item_link("Mi paquete", href: "#dropdown-menu-paquete", icon: :card, hint: "4 créditos") %>
<% m.with_item_label("Estudio") %>
<% m.with_item_link("Panel de admin", href: "#dropdown-menu-admin", icon: :dashboard) %>
<% m.with_item_separator %>
<% m.with_item_link("Cerrar sesión", href: "#dropdown-menu-salir", icon: :logout) %>
<% end %>
</div>Qué hace
- Popover nativo en la capa superior, colocado con eclipse_ui/anchor; sigue al botón al desplazar la tabla.
- Entra con el resorte snappy desde el borde del botón (--eui-origin) y sale con una transición corta, también cuando lo cierra el navegador.
- El resaltado se desliza con resorte entre opciones, con cursor o teclado; en las destructivas toma el tono de peligro.
- Cierra al elegir antes de que actúe la opción, así el diálogo de confirmación devuelve el foco al botón.
- Dispara dropdown-menu:open, dropdown-menu:close y dropdown-menu:select ({ item, label }).
Props
| Nombre | Tipo | Default | Descripción |
|---|---|---|---|
| label | String | (obligatorio) | Nombre accesible del botón (aria-label) y del menú. Con trigger propio, que incluya el texto visible. |
| align | :start | :center | :end | :start | Alineación con el botón. :end para la última columna de una tabla. |
| side | :bottom | :top | :bottom | Lado preferido; se voltea solo si no cabe. |
| width | String (clase) | "min-w-48" | Ancho del panel, p. ej. "w-60". |
| trigger_class | String | — | Clases del botón si no quieres las de serie. |
| chevron | Boolean | true | Chevron que gira en el trigger propio. |
| with_trigger | slot | tres puntos | Contenido del botón (avatar + nombre, etiqueta…), solo contenido en línea. |
| with_header | slot | — | Encabezado sobre las opciones (nombre y correo); el menú lo usa como descripción. |
| with_item_link | (texto, href:, method:, …) | — | Enlace; method: :delete lo vuelve data-turbo-method. |
| with_item_button | (texto, data:, …) | — | Botón para acciones del cliente (data: { action: … }). |
| with_item_button_to | (texto, url, method:, params:, form:, …) | — | Envío real con <form>. confirm: o data: { turbo_confirm:, confirm_*: } van al botón (el submitter que lee Turbo). |
| with_item_separator | slot | — | Línea divisoria (también with_item_divider). |
| with_item_label | (texto) | — | Título de grupo; agrupa lo que sigue (role=group). |
| icon: / hint: / shortcut: | Symbol | String | — | Por ítem: ícono (:eye, :pencil, :copy, :link, :ban, :trash, :user, :card, :dashboard, :logout…), texto a la derecha o atajo en <kbd>. |
| tone: / disabled: | :danger / Boolean | — | Por ítem: acción destructiva (:destructive también sirve) o deshabilitada (aria-disabled; el teclado la salta). |
Accesibilidad
- Patrón ARIA menu button - aria-haspopup="menu", aria-expanded y aria-controls en el botón; role=menu, menuitem, group y separator.
- Teclado: ↓/↑/Enter/Espacio abren; ↑/↓ (cíclico), Inicio/Fin y búsqueda por letra; Esc cierra y devuelve el foco; Tab cierra y sigue.
- Foco itinerante (tabindex 0 en la opción activa); las opciones deshabilitadas llevan aria-disabled y se saltan.
- Sin JS el botón abre el menú con popovertarget y los enlaces y formularios funcionan.