Popover 气泡卡片
点触发元素,在它旁边弹出一块可交互的浮层:筛选面板、简短表单、说明卡片。 只是看一眼的补充说明用 Tooltip;打断当前流程、必须处理的用 Modal。
触发元素放进 trigger 插槽,点它切换;内容放默认插槽。点外面、按 Esc 或再点一次触发元素关闭。
<pt-popover aria-label="尺寸设置">
<pt-button slot="trigger" variant="secondary">尺寸设置</pt-button>
<div>
<p>宽度与高度</p>
<pt-input aria-label="宽度" value="100%"></pt-input>
</div>
</pt-popover>placement 是首选方位,放不下时翻到对面;align 是交叉轴对齐。浮层要贴着另一块区域时把它放进 anchor 插槽。
<pt-popover placement="top" align="start" aria-label="上方">
<pt-button slot="trigger" variant="secondary">top · start</pt-button>
<div>在触发元素上方,行首对齐。</div>
</pt-popover>
<pt-popover align="start" aria-label="锚点">
<pt-input slot="anchor" aria-label="关键词" placeholder="浮层贴着输入框"></pt-input>
<pt-button slot="trigger" variant="secondary">选择</pt-button>
<div>定位以 anchor 插槽里的输入框为准。</div>
</pt-popover>触发与锚点
触发元素放进 trigger 插槽,通常是 <button> 或 pt-button,点它切换展开。键盘由它自己负责: 原生按钮上的 Enter / Space 本来就会派发 click。
浮层默认贴着触发元素定位。要贴着另一块区域(例如整个输入框,而不只是旁边的按钮)时,把那块区域放进 anchor 插槽,点击仍由触发元素负责。
位置
placement 是首选方位(默认 bottom),视口放不下时翻到对面,入场动画随之从锚点一侧滑入;反射到属性上的仍是首选值。 align 是交叉轴上的对齐(start / center / end,默认 center):placement 为 top / bottom 时 start 是行首一侧(RTL 下是右),为 left / right 时 start 是上。offset 是浮层与锚点的间距,默认 4px。
浮层经 popover API 进顶层:放在 overflow: hidden 的容器、表格单元格或带 transform 的祖先里都不会被裁切。 窄屏(≤640px)下切换为底部抽屉,带背景遮罩,点遮罩关闭。
宽度默认 288px,用 CSS 变量 --pt-popover-width 改。
关闭与拦截
三种用户操作会关闭浮层,reason 分别是:
escape:按 Esc(输入法组合中的 Esc 不算);outside:在浮层与触发元素之外按下指针;trigger:再点一次触发元素。
关闭之前先发可取消的 ptRequestClose,event.preventDefault() 就保持打开、也不发 ptClose —— 例如浮层里的表单有未保存的改动。 事件约定与 Modal 一致:
ptOpen:浮层每次真正渲染出来之后发,不论是交互打开还是外部写open打开的。ptClose:只在用户交互关闭时发,detail.reason同上。外部把open写成false不发,也不经过ptRequestClose。
与其它弹层共存。 打开的弹层(Modal、Popover,以及后续的 Menu、HoverCard)共用一个层栈,Esc 只关最上面那层: Modal 里打开的气泡卡片,第一下 Esc 只关气泡卡片,第二下才关 Modal。浮层里展开着的 pt-select、有内容的搜索框 会先把 Esc 用掉(收起下拉、清空搜索词),气泡卡片不跟着关。
焦点
- 打开后焦点移进浮层:落在内容里第一个可聚焦元素上,没有就落在浮层本身。
auto-focus="false"时焦点留在触发元素上。 - 关闭时焦点若还在浮层里,回到触发元素;因为点了外面而关闭时不抢焦点 —— 用户已经点向别处了。
- 默认非模态:Tab 可以离开浮层,页面其余部分照常可用。
模态(modal)
加 modal 后按 Radix 的模态 Popover 处理,浮层带 aria-modal="true",另外做四件事:
- 焦点关在浮层里:Tab 在浮层里循环;焦点还在外面时(
auto-focus="false")第一次 Tab 就被拉进来。 - 点外面不穿透:浮层下面铺一层透明遮罩(同样进顶层),点外面只关浮层(
reason为outside), 页面上的按钮、链接收不到这一下点击。 - 页面其余部分对读屏隐藏:从气泡卡片往上走到
<body>,沿途的兄弟元素都设为inert—— 读屏看不到、 也 Tab 不进去。选inert而不是aria-hidden:后者只管读屏,被它包着的元素仍能被点到、被程序聚焦, axe 还会报「aria-hidden 里有可聚焦元素」。读屏播报区(带aria-live的元素,如轻提示)不隐藏; 使用方自己设的inert关闭后原样保留;浮层打开期间再弹出的pt-modal/pt-sheet若挂在被隐藏的区域里, 会在打开期间把自己放出来。 - 锁住页面滚动,关闭后还原。
关闭后焦点还给触发元素 —— 包括点外面关闭的情况:页面是 inert 的,那一下点击没有把焦点交给任何东西。 打开期间改 modal 立即生效。
无障碍
浮层是 role="dialog",用 aria-label 给它一个名称(包装层里属性名是 ariaLabelText)。 触发元素上由组件维护 aria-expanded、aria-haspopup="dialog" 与 aria-controls。
aria-controls 指向的是放进默认插槽的内容元素,而不是影子树里的浮层面板:IDREF 不跨影子边界, 指向面板等于没写。内容元素没有 id 时组件自动补一个。aria-expanded 写在触发元素的宿主上 —— 原生 <button> 直接生效;pt-button 的真实按钮在它自己的影子树里,追求读屏完整时优先用原生按钮(同 Collapsible)。
API
属性
| 属性 | Attribute | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
align | align | "center" | "end" | "start" | 'center' | 交叉轴上的对齐:top / bottom 时 start 是行首一侧(RTL 下是右),left / right 时 start 是上 |
ariaLabelText | aria-label | string | undefined | — | 浮层(role="dialog")的无障碍名称:原生 HTML 里写 aria-label, React / Vue / Angular 包装层里属性名是 ariaLabelText。 |
autoFocus | auto-focus | boolean | true | 打开后把焦点移进浮层:落在内容里第一个可聚焦元素上,没有就落在浮层本身。 设为 false 时焦点留在触发元素上。默认为真:原生 HTML 里关掉它写 `auto-focus="false"`。 |
modal | modal | boolean | false | 模态:焦点关在浮层里(焦点在浮层外时第一次 Tab 就被拉进来);点外面只关浮层、不穿透到页面; 页面其余部分 inert(读屏看不到);锁住页面滚动。打开期间改它立即生效 |
offset | offset | number | 4 | 浮层与锚点之间的距离(px) |
open | open | boolean | false | 是否展开。用户交互时组件自己改写;外部直接赋值立即生效,不发 ptRequestClose / ptClose |
placement | placement | "bottom" | "left" | "right" | "top" | 'bottom' | 相对锚点的方位。视口放不下时翻到对面,反射的属性仍是这里的首选值 |
事件
| 事件 | detail 类型 | 说明 |
|---|---|---|
ptClose | { reason: PtPopoverCloseReason; } | 用户交互关闭后触发,detail 说明是怎么关的。外部把 open 写成 false **不发** (与 pt-modal / pt-tooltip 一致:受控写法下再回告一次只会多绕一圈) **不冒泡**:弹层常互相嵌套(弹窗里的菜单、气泡卡片里的下拉),冒泡的话外层上的监听会收到内层的开关。 |
ptOpen | void | 浮层渲染出来之后发(交互或属性打开都发,与 pt-modal 一致) **不冒泡**:弹层常互相嵌套(弹窗里的菜单、气泡卡片里的下拉),冒泡的话外层上的监听会收到内层的开关。 |
ptRequestClose | { reason: PtPopoverCloseReason; } | 用户要关闭浮层时(Esc / 点外面 / 再点触发元素)、真正关闭之前触发,可取消: `event.preventDefault()` 后浮层保持打开,也不会发 ptClose。直接把 open 设为 false 不经过它。 **不冒泡**:弹层常互相嵌套(弹窗里的菜单、气泡卡片里的下拉),冒泡的话外层上的监听会收到内层的开关。 |
插槽
| 名称 | 说明 |
|---|---|
(默认) | 浮层内容 |
anchor | 定位锚点(可选),渲染在触发元素之前。不给时贴着触发元素 |
trigger | 触发元素,点击切换展开 |
可定制的内部元素(::part())
| part | 说明 |
|---|---|
base | 浮层面板(与 panel 同一元素) |
panel | 浮层面板 |
CSS 变量
| 变量 | 说明 |
|---|---|
--pt-popover-width | 浮层宽度,默认 288px。窄屏抽屉形态下铺满宽度,不受它影响 |