Skip to content
API 风格:

Menú desplegable ​

Menú de comandos que se despliega hacia abajo. El disparador puede ser cualquier elemento; los elementos del menú se identifican mediante command y, al hacer clic, se emiten a través del evento command. Está implementado según el patrón menu de WAI-ARIA: las flechas recorren el menú en bucle, Home/End van al primer y último elemento, Enter/Space lo activan, y Esc cierra y devuelve el foco.

Uso básico ​

Disparador click + evento command

Último comando: —

vue
<template>
  <LhDropdown @command="onCommand">
    <LhButton>{{ selectedLabel || 'Más acciones' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="new">Nuevo</LhDropdownItem>
      <LhDropdownItem command="edit">Editar</LhDropdownItem>
      <LhDropdownItem command="delete">Eliminar</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const lastCommand = ref('')
// El texto del disparador lo mantiene quien lo usa: el evento command solo emite el valor del comando;
// cómo se muestra la etiqueta queda decidiéndolo el exterior
const labelMap: Record<string, string> = { new: 'Nuevo', edit: 'Editar', delete: 'Eliminar' }
const selectedLabel = ref('')
function onCommand(command: string | number | undefined) {
  lastCommand.value = String(command)
  selectedLabel.value = labelMap[String(command)] ?? String(command)
}
</script>
vue
<script>
export default {
  data() {
    return {
      lastCommand: '',
      // El texto del disparador lo mantiene quien lo usa: el evento command solo emite el valor del comando;
      // cómo se muestra la etiqueta queda decidiéndolo el exterior
      labelMap: { new: 'Nuevo', edit: 'Editar', delete: 'Eliminar' },
      selectedLabel: '',
    }
  },
  methods: {
    onCommand(command) {
      this.lastCommand = String(command)
      this.selectedLabel = this.labelMap[String(command)] ?? String(command)
    },
  },
}
</script>

<template>
  <LhDropdown @command="onCommand">
    <LhButton>{{ selectedLabel || 'Más acciones' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="new">Nuevo</LhDropdownItem>
      <LhDropdownItem command="edit">Editar</LhDropdownItem>
      <LhDropdownItem command="delete">Eliminar</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Forma de disparo ​

Por defecto es click, pero puede cambiarse a despliegue al pasar el puntero o con el botón derecho.

hover / contextmenu
vue
<script setup lang="ts">
import { ref } from 'vue'
const hoverLabel = ref('')
function onHoverCommand(command: string | number | undefined) {
  hoverLabel.value = String(command)
}
const contextLabel = ref('')
function onContextCommand(command: string | number | undefined) {
  contextLabel.value = String(command)
}
</script>

<template>
  <!-- Disparo al pasar el puntero -->
  <LhDropdown trigger="hover" @command="onHoverCommand">
    <LhButton>{{ hoverLabel || 'Disparo al pasar el puntero' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="hover-1">Opción 1</LhDropdownItem>
      <LhDropdownItem command="hover-2">Opción 2</LhDropdownItem>
    </template>
  </LhDropdown>
  <!-- Disparo con el botón derecho -->
  <LhDropdown trigger="contextmenu" @command="onContextCommand">
    <LhButton>{{ contextLabel || 'Disparo con el botón derecho' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="ctx-1">Actualizar</LhDropdownItem>
      <LhDropdownItem command="ctx-2">Copiar</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { hoverLabel: '', contextLabel: '' }
  },
  methods: {
    onHoverCommand(command) {
      this.hoverLabel = String(command)
    },
    onContextCommand(command) {
      this.contextLabel = String(command)
    },
  },
}
</script>

<template>
  <!-- Disparo al pasar el puntero -->
  <LhDropdown trigger="hover" @command="onHoverCommand">
    <LhButton>{{ hoverLabel || 'Disparo al pasar el puntero' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="hover-1">Opción 1</LhDropdownItem>
      <LhDropdownItem command="hover-2">Opción 2</LhDropdownItem>
    </template>
  </LhDropdown>
  <!-- Disparo con el botón derecho -->
  <LhDropdown trigger="contextmenu" @command="onContextCommand">
    <LhButton>{{ contextLabel || 'Disparo con el botón derecho' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="ctx-1">Actualizar</LhDropdownItem>
      <LhDropdownItem command="ctx-2">Copiar</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

El disparo contextmenu bloquea el menú contextual predeterminado del navegador.

Elementos desactivados y línea divisoria ​

disabled / divided
vue
<script setup lang="ts">
import { ref } from 'vue'
const actionLabel = ref('')
function onActionCommand(command: string | number | undefined) {
  actionLabel.value = String(command)
}
</script>

<template>
  <!-- Menú de acciones con elemento desactivado y línea divisoria -->
  <LhDropdown @command="onActionCommand">
    <LhButton>{{ actionLabel || 'Acción' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="export">Exportar</LhDropdownItem>
      <LhDropdownItem command="import">Importar</LhDropdownItem>
      <LhDropdownItem divided command="audit">Registro de auditoría</LhDropdownItem>
      <LhDropdownItem disabled command="purge">Vaciar datos</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { actionLabel: '' }
  },
  methods: {
    onActionCommand(command) {
      this.actionLabel = String(command)
    },
  },
}
</script>

<template>
  <!-- Menú de acciones con elemento desactivado y línea divisoria -->
  <LhDropdown @command="onActionCommand">
    <LhButton>{{ actionLabel || 'Acción' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="export">Exportar</LhDropdownItem>
      <LhDropdownItem command="import">Importar</LhDropdownItem>
      <LhDropdownItem divided command="audit">Registro de auditoría</LhDropdownItem>
      <LhDropdownItem disabled command="purge">Vaciar datos</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

El elemento desactivado sigue siendo enfocable (aria-disabled="true"), de modo que los lectores de pantalla pueden leerlo; pero las flechas lo saltan, y ni el clic ni Enter/Space disparan command.

Posición y altura máxima ​

placement y maxHeight
vue
<script setup lang="ts">
import { ref } from 'vue'
const itemLabel = ref('')
function onItemCommand(command: string | number | undefined) {
  itemLabel.value = `Elemento ${command}`
}
</script>

<template>
  <!-- Alineado a la derecha abajo y con altura máxima limitada -->
  <LhDropdown placement="bottom-end" :max-height="160" @command="onItemCommand">
    <LhButton>{{ itemLabel || 'Alineado a la derecha abajo' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem v-for="i in 12" :key="i" :command="i">Elemento {{ i }}</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { itemLabel: '' }
  },
  methods: {
    onItemCommand(command) {
      this.itemLabel = `Elemento ${command}`
    },
  },
}
</script>

<template>
  <!-- Alineado a la derecha abajo y con altura máxima limitada -->
  <LhDropdown placement="bottom-end" :max-height="160" @command="onItemCommand">
    <LhButton>{{ itemLabel || 'Alineado a la derecha abajo' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem v-for="i in 12" :key="i" :command="i">Elemento {{ i }}</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Visibilidad controlada y comportamiento tras el clic ​

v-model:visible controla manualmente la visibilidad; hide-on-click controla si el menú se repliega automáticamente tras seleccionar un elemento; popper-class añade una clase personalizada a la capa emergente; teleport-target especifica el contenedor de montaje.

Visibilidad controlada
vue
<script setup lang="ts">
import { ref } from 'vue'
const menuVisible = ref(false)
</script>

<template>
  <!-- Visibilidad controlada: no se repliega al seleccionar -->
  <LhDropdown
    v-model:visible="menuVisible"
    :hide-on-click="false"
    popper-class="demo-dropdown-popper"
  >
    <LhButton>{{ menuVisible ? 'Replegar' : 'Desplegar' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="copy">Copiar (no se repliega al seleccionar)</LhDropdownItem>
      <LhDropdownItem command="paste">Pegar</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { menuVisible: false }
  },
}
</script>

<template>
  <!-- Visibilidad controlada: no se repliega al seleccionar -->
  <LhDropdown
    v-model:visible="menuVisible"
    :hide-on-click="false"
    popper-class="demo-dropdown-popper"
  >
    <LhButton>{{ menuVisible ? 'Replegar' : 'Desplegar' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="copy">Copiar (no se repliega al seleccionar)</LhDropdownItem>
      <LhDropdownItem command="paste">Pegar</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
Montar en un contenedor especificado
vue
<script setup lang="ts">
</script>

<template>
  <!-- Montar en un contenedor especificado -->
  <LhDropdown teleport-target="#dropdown-zone">
    <LhButton>Más ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="a">Opción</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {}
</script>

<template>
  <!-- Montar en un contenedor especificado -->
  <LhDropdown teleport-target="#dropdown-zone">
    <LhButton>Más ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="a">Opción</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Props ​

PropDescripciónTipoPredeterminado
triggerForma de disparo"hover" | "click" | "contextmenu"'click'
placementPosición de despliegue (12 opciones, igual que Tooltip)"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end"'bottom-start'
visibleEstado de visibilidad (v-model:visible)boolean—
disabledSi se desactiva el disparadorboolean—
hideOnClickSi se cierra automáticamente tras hacer clic en un elemento del menúbooleantrue
teleportTargetObjetivo del Teleport (selector o elemento)string | HTMLElement'body'
popperClassNombre de clase personalizado de la capa emergentestring—
maxHeightAltura máxima del menú (un número son px)string | number—
themeVarsVariables de tema a nivel de componente{ [x: string]: string; }—
bgPatternPatrón de fondo a nivel de componente; sobrescribe la configuración del ConfigProviderstring—
sxProps de disposición rápida (aplicadas al elemento raíz del disparador)LhSxProps—

Events ​

EventoDescripciónParámetros
commandSe dispara al hacer clic en un elemento del menú(command: string | number | undefined)
clickSe dispara al hacer clic en un elemento del menú (LhDropdownItem)(command: string | number | undefined)
visible-changeCambio de visibilidad(value: boolean)
update:visibleCambio de visibilidad (v-model)(value: boolean)

Slots ​

SlotDescripción
defaultContenido del disparador
dropdownContenido del menú (lista de LhDropdownItem)

Methods ​

MétodoDescripción
showDespliegue programático
hideRepliegue programático
toggleAlternancia programática

Props ​

PropDescripciónTipoPredeterminado
commandIdentificador del comando; al hacer clic se emite con el evento commandstring | number—
disabledSi se desactivabooleanfalse
dividedSi se muestra una línea divisoria encimabooleanfalse

Accesibilidad ​

  • El disparador lleva aria-haspopup="menu" y aria-expanded; el menú es role="menu" y sus elementos, role="menuitem"
  • El teclado sigue el patrón menu de APG: ↑/↓ en bucle (saltando los elementos desactivados), Home/End saltan al primer y último elemento, Enter/Space disparan command y cierran, Esc cierra y devuelve el foco
  • Los elementos desactivados usan aria-disabled en lugar de eliminarse del árbol de foco; tanto las flechas como Enter los saltan
  • Cuando el menú se desborda en el eje principal, la dirección se invierte automáticamente; si lo hace en el eje secundario, el menú se vuelve a limitar al viewport

Definiciones de tipos ​

Las estructuras de tipos TypeScript exportadas por este componente (extraídas automáticamente de packages/components/dropdown/src/types.ts y de las fuentes @lh-ui/share/core/*; abre cada entrada para ver la definición completa; la descripción de los atributos está en la tabla de Props de arriba):

LhDropdownProps
ts
export interface LhDropdownProps {
  /** Forma de disparo */
  trigger?: LhDropdownTrigger
  /** Posición de despliegue */
  placement?: LhDropdownPlacement
  /** Estado de visibilidad controlado manualmente (v-model:visible) */
  visible?: boolean
  /** Si se desactiva */
  disabled?: boolean
  /** Si se cierra automáticamente tras hacer clic en un elemento del menú */
  hideOnClick?: boolean
  /** Objetivo del Teleport (selector o elemento; por defecto, body) */
  teleportTarget?: string | HTMLElement
  /** Nombre de clase personalizado de la capa emergente */
  popperClass?: string
  /** Altura máxima del menú */
  maxHeight?: string | number
  /** Variables de tema a nivel de componente; sobrescribe la configuración del ConfigProvider */
  themeVars?: Record<string, string>
  /** Patrón de fondo a nivel de componente; sobrescribe la configuración del ConfigProvider */
  bgPattern?: string
  /** Props de disposición rápida (aplicadas al elemento raíz del disparador) */
  sx?: LhSxProps
}
LhDropdownItemProps
ts
export interface LhDropdownItemProps {
  /** Identificador del comando; al hacer clic se emite con el evento command */
  command?: string | number
  /** Si se desactiva (conserva la capacidad de recibir el foco para entrar en el árbol de accesibilidad) */
  disabled?: boolean
  /** Si se muestra una línea divisoria */
  divided?: boolean
}
LhDropdownTrigger (de @lh-ui/share/core/dropdown (logic.ts))
ts
/** Forma de disparo */
export type LhDropdownTrigger = 'hover' | 'click' | 'contextmenu'

La belleza de la tradición, el cimiento del código