Skip to content
API 风格:

Popover 弹出框 ​

富内容弹出层:比 Tooltip 承载更多信息(标题、正文、操作按钮)。触发器包裹在默认插槽里,弹出内容通过 content 插槽传入。点击触发时弹出层是 role="dialog" 并圈住焦点,悬停触发时只展示内容、不打断键盘流。

基础用法 ​

点击触发
vue
<script setup lang="ts">
</script>

<template>
  <LhPopover title="水墨提示" placement="top">
    <LhButton>点击弹出</LhButton>
    <template #content>
      <p>这是一段富内容,可以放置任意 markup。</p>
    </template>
  </LhPopover>
</template>
vue
<script>
export default {}
</script>

<template>
  <LhPopover title="水墨提示" placement="top">
    <LhButton>点击弹出</LhButton>
    <template #content>
      <p>这是一段富内容,可以放置任意 markup。</p>
    </template>
  </LhPopover>
</template>

触发方式 ​

hover / focus / manual
vue
<template>
  <!-- 悬停触发 -->
  <LhPopover trigger="hover" title="悬停触发" :show-after="0">
    <LhButton>悬停我</LhButton>
    <template #content><p>指针移进弹出层不会触发关闭。</p></template>
  </LhPopover>
  <!-- 聚焦触发 -->
  <LhPopover trigger="focus" title="聚焦触发" :show-after="0">
    <LhInput placeholder="聚焦输入框" />
    <template #content><p>输入框获得焦点时展示。</p></template>
  </LhPopover>
  <!-- 手动控制 -->
  <LhPopover v-model:visible="manualVisible" trigger="manual" title="手动控制">
    <LhButton @click="manualVisible = !manualVisible">外部开关</LhButton>
    <template #content><p>显隐完全由外部 v-model 控制。</p></template>
  </LhPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const manualVisible = ref(false)
</script>
vue
<template>
  <!-- 悬停触发 -->
  <LhPopover trigger="hover" title="悬停触发" :show-after="0">
    <LhButton>悬停我</LhButton>
    <template #content><p>指针移进弹出层不会触发关闭。</p></template>
  </LhPopover>
  <!-- 聚焦触发 -->
  <LhPopover trigger="focus" title="聚焦触发" :show-after="0">
    <LhInput placeholder="聚焦输入框" />
    <template #content><p>输入框获得焦点时展示。</p></template>
  </LhPopover>
  <!-- 手动控制 -->
  <LhPopover v-model:visible="manualVisible" trigger="manual" title="手动控制">
    <LhButton @click="manualVisible = !manualVisible">外部开关</LhButton>
    <template #content><p>显隐完全由外部 v-model 控制。</p></template>
  </LhPopover>
</template>

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

十二种方位 ​

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>方位:{{ 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>方位:{{ 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>

视口空间不足时弹出层会自动翻转方向(flip)并夹回交叉轴(shift),箭头位置随之切换。

嵌套交互内容 ​

表单示例
vue
<template>
  <LhPopover placement="right" title="快速备注">
    <LhButton>添加备注</LhButton>
    <template #content>
      <div class="demo-popover-form">
        <LhInput v-model="note" placeholder="填写备注内容" />
        <LhButton type="primary" @click="saveNote">保存</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="快速备注">
    <LhButton>添加备注</LhButton>
    <template #content>
      <div class="demo-popover-form">
        <LhInput v-model="note" placeholder="填写备注内容" />
        <LhButton type="primary" @click="saveNote">保存</LhButton>
      </div>
    </template>
  </LhPopover>
</template>

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

更多属性 ​

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

<template>
  <!-- 已禁用 -->
  <LhPopover disabled title="已禁用">
    <LhButton disabled>禁用触发器</LhButton>
    <template #content><p>disabled 时触发器与弹出层均不响应。</p></template>
  </LhPopover>
  <!-- 无箭头 -->
  <LhPopover title="无箭头" :arrow="false" :show-after="0">
    <LhButton>无箭头</LhButton>
    <template #content><p>arrow 为 false 时不渲染指向箭头。</p></template>
  </LhPopover>
  <!-- 固定宽度 -->
  <LhPopover title="固定宽度" width="240" :show-after="0">
    <LhButton>固定宽度</LhButton>
    <template #content><p>通过 width 指定弹出层宽度,这里为 240px。</p></template>
  </LhPopover>
  <!-- 延迟隐藏 -->
  <LhPopover title="延迟隐藏" :hide-after="800" :show-after="0">
    <LhButton>延迟隐藏</LhButton>
    <template #content><p>移出触发器后延迟 800ms 才收起。</p></template>
  </LhPopover>
  <!-- 自定义类名 -->
  <LhPopover title="自定义类名" popper-class="demo-popper-accent" :show-after="0">
    <LhButton>自定义类名</LhButton>
    <template #content><p>popperClass 会加在弹出层根元素上,可用于定制样式。</p></template>
  </LhPopover>
  <!-- 指定传送目标 -->
  <LhPopover title="指定传送目标" teleport-target="#demo-popover-host" :show-after="0">
    <LhButton>Teleport 容器</LhButton>
    <template #content><p>弹出层被传送到下方容器而非 body。</p></template>
  </LhPopover>
</template>
vue
<script>
export default {}
</script>

<template>
  <!-- 已禁用 -->
  <LhPopover disabled title="已禁用">
    <LhButton disabled>禁用触发器</LhButton>
    <template #content><p>disabled 时触发器与弹出层均不响应。</p></template>
  </LhPopover>
  <!-- 无箭头 -->
  <LhPopover title="无箭头" :arrow="false" :show-after="0">
    <LhButton>无箭头</LhButton>
    <template #content><p>arrow 为 false 时不渲染指向箭头。</p></template>
  </LhPopover>
  <!-- 固定宽度 -->
  <LhPopover title="固定宽度" width="240" :show-after="0">
    <LhButton>固定宽度</LhButton>
    <template #content><p>通过 width 指定弹出层宽度,这里为 240px。</p></template>
  </LhPopover>
  <!-- 延迟隐藏 -->
  <LhPopover title="延迟隐藏" :hide-after="800" :show-after="0">
    <LhButton>延迟隐藏</LhButton>
    <template #content><p>移出触发器后延迟 800ms 才收起。</p></template>
  </LhPopover>
  <!-- 自定义类名 -->
  <LhPopover title="自定义类名" popper-class="demo-popper-accent" :show-after="0">
    <LhButton>自定义类名</LhButton>
    <template #content><p>popperClass 会加在弹出层根元素上,可用于定制样式。</p></template>
  </LhPopover>
  <!-- 指定传送目标 -->
  <LhPopover title="指定传送目标" teleport-target="#demo-popover-host" :show-after="0">
    <LhButton>Teleport 容器</LhButton>
    <template #content><p>弹出层被传送到下方容器而非 body。</p></template>
  </LhPopover>
</template>

受控显隐 ​

v-model:visible 手动控制弹出层显隐。

受控显隐
vue
<script setup lang="ts">
import { ref } from 'vue'
const popVisible = ref(false)
</script>

<template>
  <LhPopover v-model:visible="popVisible" title="受控弹出层" trigger="manual">
    <LhButton>{{ popVisible ? '收起' : '展开' }}</LhButton>
  </LhPopover>
</template>
vue
<script>
export default {
  data() {
    return { popVisible: false }
  },
}
</script>

<template>
  <LhPopover v-model:visible="popVisible" title="受控弹出层" trigger="manual">
    <LhButton>{{ popVisible ? '收起' : '展开' }}</LhButton>
  </LhPopover>
</template>

Popover API ​

Props ​

属性说明类型默认值
placement展示位置"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end"'top'
trigger触发方式"click" | "hover" | "focus" | "manual"'click'
title标题(也可用 title 插槽)string—
visible手动控制的显示状态(v-model:visible)boolean—
disabled是否禁用boolean—
arrow是否显示箭头booleantrue
width弹出层宽度string | number—
popperClass弹出层自定义类名string—
teleportTargetTeleport 目标(选择器或元素)string | HTMLElement'body'
showAfter延迟显示时间(ms)number—
hideAfter延迟隐藏时间(ms)number—
themeVars组件级主题变量,覆盖 ConfigProvider 配置{ [x: string]: string; }—
bgPattern组件级背景花纹,覆盖 ConfigProvider 配置string—
sxsx 快速布局属性(作用于触发器根元素)LhSxProps—

Events ​

事件说明参数
update:visible显隐变化(value: boolean)
show显示后触发—
hide隐藏后触发—

Slots ​

插槽说明
default触发器内容
title自定义标题
content弹出内容

Methods ​

方法说明
show编程式显示
hide编程式隐藏
toggle编程式切换

无障碍 ​

  • 点击触发时弹出层为 role="dialog" + aria-modal="true",触发器带 aria-haspopup / aria-expanded
  • 点击触发会把焦点移入弹出层首个可聚焦元素,Tab 在弹出层内循环,关闭后归还触发器焦点
  • 悬停/聚焦触发时不移动焦点,避免打断键盘操作
  • Escape 关闭、点击弹出层外部关闭;有标题时通过 aria-labelledby 关联
  • 定位引擎为自研纯函数(flip/shift 防溢出),零第三方运行时依赖

触发器滚出视口即收起:弹出层为 fixed 定位,触发器离开视口后它不会再随触发器移动, 会以高 z-index 悬浮在页面内容上。因此无论何种触发方式(含 manual 受控模式), 触发器整体滚出视口时面板都会收起,并抛出 update:visible 同步外部状态。

类型定义 ​

本组件对外导出的 TypeScript 类型结构如下(自动提取自 packages/components/popover/src/types.ts 与 @lh-ui/share/core/* 源码,点击各条目展开查看完整定义;属性说明见上方 Props 表):

LhPopoverProps
ts
/**
 * Popover 组件 Props
 */
export interface LhPopoverProps {
  /** 展示位置 */
  placement?: LhPlacement
  /** 触发方式 */
  trigger?: LhPopoverTrigger
  /** 标题 */
  title?: string
  /** 手动控制的显示状态(v-model:visible) */
  visible?: boolean
  /** 是否禁用 */
  disabled?: boolean
  /** 是否显示箭头 */
  arrow?: boolean
  /** 弹出层宽度 */
  width?: string | number
  /** 弹出层自定义类名 */
  popperClass?: string
  /** Teleport 目标(选择器或元素,默认 body) */
  teleportTarget?: string | HTMLElement
  /** 延迟显示时间(ms) */
  showAfter?: number
  /** 延迟隐藏时间(ms) */
  hideAfter?: number
  /** 组件级主题变量,覆盖 ConfigProvider 配置 */
  themeVars?: Record<string, string>
  /** 组件级背景花纹,覆盖 ConfigProvider 配置 */
  bgPattern?: string
  /** sx 快速布局属性(作用于触发器根元素) */
  sx?: LhSxProps
}
LhPopoverPlacement (来自 @lh-ui/share/core/shared/popper)
ts
/**
 * Popper 定位引擎(纯函数,无框架依赖)
 *
 * 弹出层(Tooltip / Popover / Popconfirm / Dropdown 等)共用的几何计算:
 * 12 种 placement 摆放、主轴溢出翻转(flip)、交叉轴夹回(shift)。
 * 入参全部是 rect 字面量,不触碰 DOM,可直接单测。
 */

/** 弹出层方位(方向 + 对齐) */
export type LhPlacement =
  | 'top'
  | 'top-start'
  | 'top-end'
  | 'bottom'
  | 'bottom-start'
  | 'bottom-end'
  | 'left'
  | 'left-start'
  | 'left-end'
  | 'right'
  | 'right-start'
  | 'right-end'
LhPopoverTrigger (来自 @lh-ui/share/core/popover (logic.ts))
ts
/** 触发方式 */
export type LhPopoverTrigger = 'click' | 'hover' | 'focus' | 'manual'