Skip to content
API 风格:

Popover (cuadro emergente) ​

Capa emergente de contenido enriquecido: alberga más información que un Tooltip (título, contenido, botones de acción). El disparador se envuelve en el slot por defecto y el contenido emergente se pasa a través del slot content. Con disparo por clic, la capa emergente es role="dialog" y atrapa el foco; con disparo por suspensión, solo muestra el contenido sin interrumpir el flujo del teclado.

Uso básico ​

Disparo por clic
vue
<script setup lang="ts">
</script>

<template>
  <LhPopover title="Aviso de tinta" placement="top">
    <LhButton>Pulsa para abrir</LhButton>
    <template #content>
      <p>Este es un contenido enriquecido; puedes incluir cualquier markup.</p>
    </template>
  </LhPopover>
</template>
vue
<script>
export default {}
</script>

<template>
  <LhPopover title="Aviso de tinta" placement="top">
    <LhButton>Pulsa para abrir</LhButton>
    <template #content>
      <p>Este es un contenido enriquecido; puedes incluir cualquier markup.</p>
    </template>
  </LhPopover>
</template>

Forma de disparo ​

hover / focus / manual
vue
<template>
  <!-- Disparo por suspensión -->
  <LhPopover trigger="hover" title="Disparo por suspensión" :show-after="0">
    <LhButton>Pasa el cursor sobre mí</LhButton>
    <template #content><p>Mover el puntero hacia la capa emergente no la cierra.</p></template>
  </LhPopover>
  <!-- Disparo por foco -->
  <LhPopover trigger="focus" title="Disparo por foco" :show-after="0">
    <LhInput placeholder="Campo de enfoque" />
    <template #content><p>Se muestra cuando el campo recibe el foco.</p></template>
  </LhPopover>
  <!-- Control manual -->
  <LhPopover v-model:visible="manualVisible" trigger="manual" title="Control manual">
    <LhButton @click="manualVisible = !manualVisible">Conmutador externo</LhButton>
    <template #content><p>La visibilidad la controla por completo el v-model externo.</p></template>
  </LhPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const manualVisible = ref(false)
</script>
vue
<template>
  <!-- Disparo por suspensión -->
  <LhPopover trigger="hover" title="Disparo por suspensión" :show-after="0">
    <LhButton>Pasa el cursor sobre mí</LhButton>
    <template #content><p>Mover el puntero hacia la capa emergente no la cierra.</p></template>
  </LhPopover>
  <!-- Disparo por foco -->
  <LhPopover trigger="focus" title="Disparo por foco" :show-after="0">
    <LhInput placeholder="Campo de enfoque" />
    <template #content><p>Se muestra cuando el campo recibe el foco.</p></template>
  </LhPopover>
  <!-- Control manual -->
  <LhPopover v-model:visible="manualVisible" trigger="manual" title="Control manual">
    <LhButton @click="manualVisible = !manualVisible">Conmutador externo</LhButton>
    <template #content><p>La visibilidad la controla por completo el v-model externo.</p></template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return { manualVisible: false }
  },
}
</script>

Doce posiciones ​

placement
vue
<template>
  <LhPopover
    v-for="p in placements"
    :key="p"
    :placement="p"
    :title="p"
    trigger="hover"
    :show-after="0"
  >
    <LhButton size="small">{{ p }}</LhButton>
    <template #content><p>Posición: {{ p }}</p></template>
  </LhPopover>
</template>

<script setup lang="ts">
const placements = [
  'top', 'top-start', 'top-end',
  'bottom', 'bottom-start', 'bottom-end',
  'left', 'left-start', 'left-end',
  'right', 'right-start', 'right-end',
] as const
</script>
vue
<template>
  <LhPopover
    v-for="p in placements"
    :key="p"
    :placement="p"
    :title="p"
    trigger="hover"
    :show-after="0"
  >
    <LhButton size="small">{{ p }}</LhButton>
    <template #content><p>Posición: {{ p }}</p></template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return {
      placements: [
        'top', 'top-start', 'top-end',
        'bottom', 'bottom-start', 'bottom-end',
        'left', 'left-start', 'left-end',
        'right', 'right-start', 'right-end',
      ],
    }
  },
}
</script>

Cuando no hay espacio suficiente en la ventana, la capa emergente voltea automáticamente de dirección (flip) y se sujeta en el eje transversal (shift); la posición de la flecha cambia en consecuencia.

Contenido interactivo anidado ​

Ejemplo de formulario
vue
<template>
  <LhPopover placement="right" title="Nota rápida">
    <LhButton>Añadir nota</LhButton>
    <template #content>
      <div class="demo-popover-form">
        <LhInput v-model="note" placeholder="Escribe la nota" />
        <LhButton type="primary" @click="saveNote">Guardar</LhButton>
      </div>
    </template>
  </LhPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const note = ref('')
const saved = ref('')
function saveNote() {
  saved.value = note.value
  note.value = ''
}
</script>
vue
<template>
  <LhPopover placement="right" title="Nota rápida">
    <LhButton>Añadir nota</LhButton>
    <template #content>
      <div class="demo-popover-form">
        <LhInput v-model="note" placeholder="Escribe la nota" />
        <LhButton type="primary" @click="saveNote">Guardar</LhButton>
      </div>
    </template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return { note: '', saved: '' }
  },
  methods: {
    saveNote() {
      this.saved = this.note
      this.note = ''
    },
  },
}
</script>

Más propiedades ​

disabled / arrow / width / hideAfter / popperClass / teleportTarget
Contenedor Teleport (#demo-popover-host)
vue
<script setup lang="ts">
</script>

<template>
  <!-- Deshabilitado -->
  <LhPopover disabled title="Deshabilitado">
    <LhButton disabled>Disparador deshabilitado</LhButton>
    <template #content><p>Con disabled, ni el disparador ni la capa emergente responden.</p></template>
  </LhPopover>
  <!-- Sin flecha -->
  <LhPopover title="Sin flecha" :arrow="false" :show-after="0">
    <LhButton>Sin flecha</LhButton>
    <template #content><p>Con arrow a false no se renderiza la flecha indicadora.</p></template>
  </LhPopover>
  <!-- Ancho fijo -->
  <LhPopover title="Ancho fijo" width="240" :show-after="0">
    <LhButton>Ancho fijo</LhButton>
    <template #content><p>Aquí se ha especificado el ancho de la capa emergente con width: 240px.</p></template>
  </LhPopover>
  <!-- Ocultar con retardo -->
  <LhPopover title="Ocultar con retardo" :hide-after="800" :show-after="0">
    <LhButton>Ocultar con retardo</LhButton>
    <template #content><p>Tras salir del disparador, tarda 800 ms en cerrarse.</p></template>
  </LhPopover>
  <!-- Clase personalizada -->
  <LhPopover title="Clase personalizada" popper-class="demo-popper-accent" :show-after="0">
    <LhButton>Clase personalizada</LhButton>
    <template #content><p>popperClass se añade al elemento raíz de la capa emergente y sirve para personalizar el estilo.</p></template>
  </LhPopover>
  <!-- Objetivo de teletransporte -->
  <LhPopover title="Objetivo de teletransporte" teleport-target="#demo-popover-host" :show-after="0">
    <LhButton>Contenedor Teleport</LhButton>
    <template #content><p>La capa emergente se teletransporta al contenedor de abajo en lugar de a body.</p></template>
  </LhPopover>
</template>
vue
<script>
export default {}
</script>

<template>
  <!-- Deshabilitado -->
  <LhPopover disabled title="Deshabilitado">
    <LhButton disabled>Disparador deshabilitado</LhButton>
    <template #content><p>Con disabled, ni el disparador ni la capa emergente responden.</p></template>
  </LhPopover>
  <!-- Sin flecha -->
  <LhPopover title="Sin flecha" :arrow="false" :show-after="0">
    <LhButton>Sin flecha</LhButton>
    <template #content><p>Con arrow a false no se renderiza la flecha indicadora.</p></template>
  </LhPopover>
  <!-- Ancho fijo -->
  <LhPopover title="Ancho fijo" width="240" :show-after="0">
    <LhButton>Ancho fijo</LhButton>
    <template #content><p>Aquí se ha especificado el ancho de la capa emergente con width: 240px.</p></template>
  </LhPopover>
  <!-- Ocultar con retardo -->
  <LhPopover title="Ocultar con retardo" :hide-after="800" :show-after="0">
    <LhButton>Ocultar con retardo</LhButton>
    <template #content><p>Tras salir del disparador, tarda 800 ms en cerrarse.</p></template>
  </LhPopover>
  <!-- Clase personalizada -->
  <LhPopover title="Clase personalizada" popper-class="demo-popper-accent" :show-after="0">
    <LhButton>Clase personalizada</LhButton>
    <template #content><p>popperClass se añade al elemento raíz de la capa emergente y sirve para personalizar el estilo.</p></template>
  </LhPopover>
  <!-- Objetivo de teletransporte -->
  <LhPopover title="Objetivo de teletransporte" teleport-target="#demo-popover-host" :show-after="0">
    <LhButton>Contenedor Teleport</LhButton>
    <template #content><p>La capa emergente se teletransporta al contenedor de abajo en lugar de a body.</p></template>
  </LhPopover>
</template>

Visibilidad controlada ​

v-model:visible controla manualmente la visibilidad de la capa emergente.

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

<template>
  <LhPopover v-model:visible="popVisible" title="Capa controlada" trigger="manual">
    <LhButton>{{ popVisible ? 'Cerrar' : 'Abrir' }}</LhButton>
  </LhPopover>
</template>
vue
<script>
export default {
  data() {
    return { popVisible: false }
  },
}
</script>

<template>
  <LhPopover v-model:visible="popVisible" title="Capa controlada" trigger="manual">
    <LhButton>{{ popVisible ? 'Cerrar' : 'Abrir' }}</LhButton>
  </LhPopover>
</template>

Popover API ​

Props ​

PropDescripciónTipoPredeterminado
placementPosición de visualización"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end"'top'
triggerForma de disparo"click" | "hover" | "focus" | "manual"'click'
titleTítulo (también se puede usar el slot title)string—
visibleEstado de visualización controlado manualmente (v-model:visible)boolean—
disabledSi se deshabilitaboolean—
arrowSi se muestra la flechabooleantrue
widthAncho de la capa emergentestring | number—
popperClassClase personalizada de la capa emergentestring—
teleportTargetObjetivo del Teleport (selector o elemento)string | HTMLElement'body'
showAfterRetraso antes de mostrar (ms)number—
hideAfterRetraso antes de ocultar (ms)number—
themeVarsVariables de tema a nivel de componente, prevalecen sobre la configuración de ConfigProvider{ [x: string]: string; }—
bgPatternPatrón de fondo a nivel de componente, prevalece sobre la configuración de ConfigProviderstring—
sxProps de maquetación rápida sx (se aplican al elemento raíz del disparador)LhSxProps—

Eventos ​

EventoDescripciónParámetro
update:visibleCambio de visibilidad(value: boolean)
showSe dispara tras mostrar—
hideSe dispara tras ocultar—

Slots ​

SlotDescripción
defaultContenido del disparador
titleTítulo personalizado
contentContenido emergente

Métodos ​

MétodoDescripción
showMostrar por programación
hideOcultar por programación
toggleAlternar por programación

Accesibilidad ​

  • Con disparo por clic, la capa emergente es role="dialog" + aria-modal="true"; el disparador lleva aria-haspopup / aria-expanded
  • Con disparo por clic, el foco se mueve al primer elemento enfocable de la capa emergente; Tab circula dentro de ella y, al cerrar, el foco vuelve al disparador
  • Con disparo por suspensión o por foco no se mueve el foco, para no interrumpir la operación con teclado
  • Escape cierra y la pulsación fuera de la capa emergente también; si hay título, se relaciona mediante aria-labelledby
  • El motor de posicionamiento es una función pura propia (flip/shift anti-desbordamiento), sin dependencias de terceros en tiempo de ejecución

El disparador que sale de la ventana cierra la capa: la capa emergente tiene posición fixed; cuando el disparador sale de la ventana, la capa deja de seguirlo y queda flotando sobre el contenido de la página con un z-index alto. Por eso, sea cual sea la forma de disparo (incluido el modo controlado manual), al salir el disparador por completo de la ventana el panel se cierra y emite update:visible para sincronizar el estado externo.

Definiciones de tipos ​

Las estructuras de tipos TypeScript que exporta este componente son las siguientes (extraídas automáticamente de packages/components/popover/src/types.ts y del código fuente de @lh-ui/share/core/*; despliega cada entrada para ver la definición completa; la descripción de los atributos está en la tabla de Props de arriba):

LhPopoverProps
ts
/**
 * Props del componente Popover
 */
export interface LhPopoverProps {
  /** Posición de visualización */
  placement?: LhPlacement
  /** Forma de disparo */
  trigger?: LhPopoverTrigger
  /** Título */
  title?: string
  /** Estado de visualización controlado manualmente (v-model:visible) */
  visible?: boolean
  /** Si se deshabilita */
  disabled?: boolean
  /** Si se muestra la flecha */
  arrow?: boolean
  /** Ancho de la capa emergente */
  width?: string | number
  /** Clase personalizada de la capa emergente */
  popperClass?: string
  /** Objetivo del Teleport (selector o elemento, body por defecto) */
  teleportTarget?: string | HTMLElement
  /** Retraso antes de mostrar (ms) */
  showAfter?: number
  /** Retraso antes de ocultar (ms) */
  hideAfter?: number
  /** Variables de tema a nivel de componente, prevalecen sobre la configuración de ConfigProvider */
  themeVars?: Record<string, string>
  /** Patrón de fondo a nivel de componente, prevalece sobre la configuración de ConfigProvider */
  bgPattern?: string
  /** Props de maquetación rápida sx (se aplican al elemento raíz del disparador) */
  sx?: LhSxProps
}
LhPopoverPlacement (de @lh-ui/share/core/shared/popper)
ts
/**
 * Motor de posicionamiento Popper (función pura, sin dependencias de framework)
 *
 * Cálculos geométricos compartidos por las capas emergentes
 * (Tooltip / Popover / Popconfirm / Dropdown, etc.):
 * colocación de 12 posiciones, volteo al desbordar en el eje principal (flip)
 * y sujeción en el eje transversal (shift).
 * Todos los parámetros de entrada son literales rect, sin tocar el DOM;
 * se puede probar directamente con pruebas unitarias.
 */

/** Posición de la capa emergente (dirección + alineación) */
export type LhPlacement =
  | 'top'
  | 'top-start'
  | 'top-end'
  | 'bottom'
  | 'bottom-start'
  | 'bottom-end'
  | 'left'
  | 'left-start'
  | 'left-end'
  | 'right'
  | 'right-start'
  | 'right-end'
LhPopoverTrigger (de @lh-ui/share/core/popover (logic.ts))
ts
/** Forma de disparo */
export type LhPopoverTrigger = 'click' | 'hover' | 'focus' | 'manual'

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