Popup 弹层
通用浮层基础组件。通过 position 统摄 bottom / top / left / right / center / relative / fullscreen 七种形态,内置滚动锁定(useScrollLock)与 useTeleportTarget。
HDialog 与 HBottomSheet 已基于 HPopup(position=center/bottom) 重构,原有 API 完全不变。
基础 · bottom(底部面板)
position="bottom" 为底部面板(bottom sheet),支持 拖拽关闭:在面板内(scrollTop=0 时,含手柄区域)向下拖动,面板跟随位移、遮罩渐隐;位移 ≥ 80px 或快速下滑松手即先滑出视口再关闭,短距离松手 250ms 内回弹。内容滚动后(scrollTop > 0)向下拖动只滚动内容、不接管手势。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { HButton, HPopup } from 'happier-ui'
const show = ref(false)
</script>
<template>
<h-button @click="show = true">打开弹层</h-button>
<!-- 底部面板:支持拖拽关闭(scrollTop=0 时向下拖) -->
<h-popup v-model="show" position="bottom" title="底部弹层" :handle="true">
<p>position="bottom"</p>
<h-button @click="show = false">关闭</h-button>
</h-popup>
<!-- 居中对话框 -->
<h-popup v-model="show" position="center" title="居中弹层">
<p>position="center"</p>
<template #footer>
<h-button @click="show = false">确认</h-button>
</template>
</h-popup>
<!-- 左侧面板 -->
<h-popup v-model="show" position="left" title="左侧面板">
<p>position="left"</p>
<h-button @click="show = false">关闭</h-button>
</h-popup>
</template>relative(相对 trigger 定位)
无遮罩,JS 计算相对触发元素的固定坐标,支持边缘翻转与 resize/scroll 重算。适用于下拉选择、呼出菜单等。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { HButton, HPopup } from 'happier-ui'
const show = ref(false)
const btn = ref(null)
</script>
<template>
<h-button ref="btn" @click="show = !show">
{{ show ? '关闭' : '打开 relative 弹层' }}
</h-button>
<h-popup
v-model="show"
position="relative"
:trigger-ref="btn"
title="相对弹层"
closeable
radius="sm"
>
<p>相对 trigger 定位,带 X 关闭按钮</p>
<h-button @click="show = false">关闭</h-button>
</h-popup>
</template>fullscreen(全屏 + 下滑关闭)
position="fullscreen" 让面板完全覆盖视口:无圆角、无 safe-area 内边距,也不会渲染 title / #title header。请在 default slot 内自行组织导航栏或标题。
全屏内容可正常纵向滚动;仅在 scrollTop === 0 且向下拖时接管手势关闭:位移 ≥ 80px 或速度 ≥ 0.3px/ms 即关闭,未达阈值会在 250ms 内回弹。拖动过程中遮罩透明度同步降低,并临时 touch-action: none 锁住面板滚动以免手势与内容滚动打架。handle 在 fullscreen 无效。closeable、Esc 和遮罩点击仍然可用。
:swipe-close="false":关闭内置下滑手势。touch 监听不再生效(不preventDefault),面板touch-action从pan-y复位为auto,手势完全交还宿主(宿主用自己的手势调update:modelValue(false)关闭)。适用于面板内含多个独立滚动容器、内置手势会与其打架的场景。:keep-alive="true":关闭时仅隐藏(display:none)不卸载 slot 内容,再打开时内容不重建、入场动画照常重放。适合内容初始化昂贵(如 WebGL 背景)的场景。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { HButton, HPopup } from 'happier-ui'
const show = ref(false)
</script>
<template>
<h-button @click="show = true">打开全屏弹层</h-button>
<h-popup
v-model="show"
position="fullscreen"
aria-label="全屏设置"
closeable
:swipe-close="false"
:keep-alive="true"
>
<div class="fullscreen-page">
<h2>宿主自管头部</h2>
<p>内置下滑手势已禁用;关闭由宿主自控。</p>
</div>
</h-popup>
</template>API
Props
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
modelValue | boolean | false | 受控显隐(v-model) |
position | 'bottom' | 'top' | 'left' | 'right' | 'center' | 'relative' | 'fullscreen' | 'bottom' | 弹层形态与动画;fullscreen 占满视口并支持下滑关闭 |
triggerRef | HTMLElement | null | null | relative 定位的触发元素引用(ref 传对象) |
closeOnOverlay | boolean | true | 点击遮罩关闭(relative 无遮罩故无作用) |
closeOnEsc | boolean | true | Esc 键关闭 |
lockScroll | boolean | true | 打开时锁定 body 滚动 |
title | string | — | 面板标题文本 |
ariaLabel | string | — | 无障碍标签(title 为空时生效) |
teleport | string | HTMLElement | false | 'body' | Teleport 目标(传 false 就地渲染) |
closeable | boolean | false | 显示 X 关闭按钮(Lucide X 图标) |
closeIconPosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-right' | X 按钮位置 |
radius | 'none' | 'sm' | 'md' | 'lg' | — | 面板圆角粒度 |
handle | boolean | false | position="bottom" 时显示拖拽手柄 |
keepAlive | boolean | false | 关闭时保活 slot 内容(隐藏不卸载,重开重放入场动画);默认关闭即卸载、重开重挂载 |
swipeClose | boolean | true | bottom / fullscreen 下滑关闭手势开关;false 时内置手势不生效、面板 touch-action 复位为 auto(其他 position 无作用) |
maxWidth | string | number | — | bottom/top 形态面板最大宽度;number 按 px,string 原样(如 '640px' / 'none' / '100%')。写入 inline CSS 变量 --h-bottom-sheet-max-width,per-instance 覆盖 token;不传默认全宽(edge-to-edge) |
Emits
| 名称 | 参数 | 说明 |
|---|---|---|
update:modelValue | value: boolean | v-model 双向绑定 |
close | — | 面板关闭瞬间 |
open | — | 面板打开瞬间 |
after-leave | — | Transition 离场动画结束后 |
click-overlay | — | 点击遮罩瞬间(先于 close 处理) |
click-close-icon | — | 点击 X 关闭按钮瞬间 |
Slots
| 名称 | 作用域 | 说明 |
|---|---|---|
default | — | 面板正文内容 |
title | — | 自定义标题(覆盖 prop title) |
footer | — | 面板底部操作区 |
行为说明
- 宽度:bottom/top 面板默认全宽贴底(
--h-bottom-sheet-max-width默认100%,edge-to-edge)。宽屏需桌面居中卡片感时,传maxWidth(如:max-width="640")或全局覆盖--h-bottom-sheet-max-width限宽;限宽后面板仍水平居中(margin: 0 auto)。maxWidth仅影响引用该变量的 bottom/top 形态,left/right/center/relative/fullscreen 不受影响。 - 滚动锁定:默认
lockScroll: true,打开时通过useScrollLock(引用计数,模块级安全)禁止 body 滚动,关闭自动还原。 - 遮罩:除
position="relative"外均渲染遮罩层。遮罩点击关闭受closeOnOverlay控制。 - Bottom 拖拽手势:bottom 面板(含 handle)在
scrollTop === 0时向下拖动,面板跟随位移、遮罩透明度同步降低;位移 ≥ 80px 或速度 ≥ 0.3px/ms 松手先平滑滑出视口(250ms)再关闭,未达阈值 250ms 内回弹。内容滚动后(scrollTop > 0)不接管,交还内容滚动;从遮罩区域起拖不触发。swipeClose=false时手势禁用、面板touch-action复位auto,由宿主全权控制。 - Fullscreen 手势:内容位于顶部时向下拖动;位移 ≥ 80px 或速度 ≥ 0.3px/ms 关闭,未达阈值则回弹。全屏不渲染内置 header,需由 default slot 自管。
swipeClose=false时内置手势禁用,手势交由宿主处理(面板touch-action: auto),转场动画、滚动锁、overlay/Esc/closeable 关闭通道不受影响。 - keepAlive 保活:
keepAlive=true时 slot 内容首渲即挂载、关闭仅隐藏不卸载,重开内容不重建且入场动画重放;隐藏态不响应任何交互(overlay/Esc/X/手势),滚动锁仍随visible释放/恢复。 - Esc:Esc 键触发关闭(
closeOnEsc)。 - 关闭按钮:
closeable显示 X 图标按钮(Heroicons ×,通过 LucideX);默认隐藏。 - Title 无障碍:
titleprop 无 #title slot 时渲染 VSariaLabelledBy(HLabel不带aria-describedby)。 - Teleport:默认
'body'。传false就地渲染。 - 无 before-close:不做钩子拦截;受控宿主可通过
v-model提前阻止。