Popover 气泡卡片
由点击触发、贴着触发器的一小块浮层,可以放任意内容与交互。
用法
点击展开,Escape 或点击外部关闭;positioner 负责定位,content 才是浮层本体
组件结构
加粗的是必需部件。
data-scope="popover":trigger · positioner · content · title · description · close-trigger · arrow
示例
朝向与间距
placement 是请求值,空间不足时定位引擎会自动翻面;offset 调整浮层与触发器的距离
受控
传入 open 后由宿主决定;这里额外关闭点击外部关闭,只有按钮与 Escape 能收起
尺寸
三档改变浮层的内边距与字号,不写 size 即默认档;逐个点开触发器查看差别
确认气泡
标题、说明与两个按钮组成一次就地确认;两个按钮按下后都只是收起浮层
长内容滚动
浮层自身不限高,为内部容器设置上限并开启滚动,标题与关闭按钮就不随内容滚动
模态浮层
modal 使焦点限制在浮层内:Tab 到末尾回绕,旁边的按钮此时无法获得焦点
事件
open-change 带一份 { open },报告的是本次要进入的状态;非受控时内部开合也照常触发一次
浮层与触发器同宽
测量触发器的实际宽度写进 content 的行内样式,同时解除最大宽度上限;触发器更换文案后宽度随之变化
书写方向
start / end 是逻辑对齐不是左右:RTL 下 bottom-start 贴的是锚点右缘,块轴上的对齐不受影响
落在指针位置
触发器缩为一个像素、按点击坐标固定放置,浮层就固定在刚点击的位置;再点一次更换落点
设计指引
何时使用
- 补充信息或一小组操作,不需要为此打开对话框。
- 内容中有可聚焦元素(按钮、输入框),这是它与文字提示的分界。
何时不用
特性
placement只是首选位置,空间不足时定位引擎自动翻面。modal可选:需要锁定下层时开启;展开期间可动态切换,模态档会锁定页面滚动并让背景失活。- 可以与触发器同宽,也可以落在指针位置。
end等对齐是逻辑方向,跟随书写方向,不是物理左右。
默认内容面使用 M2 磨砂配方,背景模糊只发生在浮层本体,箭头复用底色和边界,不重复模糊。正文保持不透明。触发器与关闭按钮走 Action Control 家族配方:触发器为 text 档中性描边,关闭按钮为 icon 档 ghost 面,悬停与按下沿画布承载阶梯换底,Space / Enter 与触屏按住期间投影 data-pressed,与指针按下同一副按压面。说明文字为 13px 说明档。系统减少透明度、高对比与强制色时,原位切换为实体表面;打印时收起交互浮层。
组合
最佳实践
- 打开后焦点进入浮层,Escape 关闭并归还焦点。
- 模态浮层关闭时,滚动锁与背景失活保留到真实退场动画结束;退场内容自身立即退出焦点与交互树。
- 内容控制在一屏内,需要滚动时应改用抽屉。
反模式
- 悬停触发却内含按钮:指针移动过去的途中就会关闭。
- 气泡内再弹出气泡。
API 参考
产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-popover> |
| Vue 组件 | XhPopoverArrow XhPopoverCloseTrigger XhPopoverContent XhPopoverDescription XhPopoverPositioner XhPopoverRoot XhPopoverTitle XhPopoverTrigger |
| 组合式函数 | usePopover |
| 状态机 | popoverMachine |
| 皮肤 | @xihan-ui/styles/popover.css |
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
open | boolean | ||
defaultOpen | boolean | ||
placement | Placement | ||
dir | Direction | 文字方向,默认 ltr。只改写浮层在行内轴上 start 与 end 的落点。 | |
offset | number | ||
modal | boolean | 模态浮层陷入焦点;默认 false(非模态,Tab 可离开)。 | |
closeOnEscape | boolean | ||
closeOnInteractOutside | boolean | ||
translations | Partial<PopoverTranslations> | ||
size | Size | 尺寸:sm / md / lg,决定面板的内边距档位。 | |
onOpenChange | (details: PopoverOpenChangeDetails) => void | open 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。 |
事件
自定义元素将载荷放在 detail;Vue 使用同名 emit。
| 事件 | 载荷 | 说明 |
|---|---|---|
open-change | PopoverOpenChangeDetails | open 状态变化;detail 为 { open: boolean } |
插槽
仅列出带载荷的插槽。
| Vue 组件 | 插槽 | 载荷 | 说明 |
|---|---|---|---|
XhPopoverRoot | default | PopoverRootSlotProps |
React 适配器 props
只列各组件自己声明的那些:继承自 ComponentPropsWithRef 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。
| React 组件 | 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
XhPopoverPositioner | container | () => Element | null | 浮层挂载的容器;未提供时按全局配置,再未提供时挂载到 body。 | |
XhPopoverRoot | children | SlotChildren<PopoverRootSlotProps> |
状态
公开状态写入 data-state。
| 部件 | 取值 |
|---|---|
trigger | 'open' | 'closed' |
positioner | 'open' | 'closed' |
content | 'open' | 'closed' |
以下名称仅用于内部状态机。
状态:open · closed
事件:OPEN · TOGGLE · CLOSE · CONTROLLED.OPEN · CONTROLLED.CLOSE · PRESS.START · PRESS.END
判据:isOpenControlled
connect API
getXxxProps() 返回对应部件的宿主属性。
| 成员 | 类型 | 说明 |
|---|---|---|
open | boolean | |
setOpen | (next: boolean) => void | |
getTriggerProps | () => T['button'] | |
getPositionerProps | () => T['element'] | |
getContentProps | () => T['element'] | |
getTitleProps | () => T['element'] | |
getDescriptionProps | () => T['element'] | |
getCloseTriggerProps | () => T['button'] | |
getArrowProps | () => T['element'] |
无障碍
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Enter / Space | focus in trigger | 切换开合,展开时把焦点移入 content |
Escape | open | 关闭并把焦点还给 trigger |
Tab | open 且 modal | 在 content 内向后循环焦点 |
Shift+Tab | open 且 modal | 在 content 内向前循环焦点 |
Enter / Space | held in trigger / close-trigger | 按住期间该按钮投影 data-pressed,与指针 :active 同一副按压面;抬起、失焦或浮层收起撤下 |
ARIA
以下属性由 connect 生成。
| 部件 | 属性 | 值 |
|---|---|---|
trigger | aria-controls | content 部件的 id |
trigger | aria-expanded | 'true' | 'false' |
trigger | aria-haspopup | 'dialog' |
content | aria-describedby | description 部件的 id |
content | aria-hidden | !open || undefined |
content | aria-labelledby | title 部件的 id |
content | aria-modal | 'true' | 'false' |
content | role | 'dialog' |
close-trigger | aria-label | props.translations.close |
arrow | aria-hidden | 'true' |
样式参考
皮肤
@xihan-ui/styles/popover.css 使用 [data-scope="popover"][data-part="trigger"] 部件选择器,位于 xihan.components 层。覆盖样式使用 xihan.overrides。
forced-colors: active 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。
数据属性
由 connect 生成;条件不成立时不输出无值属性。
| 部件 | 属性 | 值 |
|---|---|---|
trigger | data-pressed | ''(条件成立时才出现) |
trigger | data-state | 'open' | 'closed' |
trigger | data-xh-action-control | '' |
trigger | data-xh-action-display | 'always' |
trigger | data-xh-action-profile | 'text' |
trigger | data-xh-action-size | 'md' |
trigger | data-xh-action-variant | 'outline' |
positioner | data-hidden | ''(条件成立时才出现) |
positioner | data-placement | 定位引擎算出的实际落位 |
positioner | data-positioned | ''(条件成立时才出现) |
positioner | data-state | 'open' | 'closed' |
content | data-placement | 定位引擎算出的实际落位 |
content | data-size | props.size |
content | data-state | 'open' | 'closed' |
content | data-xh-material | 'frosted' |
close-trigger | data-pressed | ''(条件成立时才出现) |
close-trigger | data-xh-action-control | '' |
close-trigger | data-xh-action-display | 'always' |
close-trigger | data-xh-action-profile | 'icon' |
close-trigger | data-xh-action-size | 'sm' |
close-trigger | data-xh-action-variant | 'ghost' |
arrow | data-placement | 定位引擎算出的实际落位 |
CSS 变量
本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。
| 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 |
|---|---|---|---|---|---|
--xh-popover-arrow-size | arrow | --xh-_overlay-arrow-size | default | --xh-overlay-arrow-size | popover 的 arrow 部件 --xh-_overlay-arrow-size 覆盖槽。 |
--xh-popover-backdrop | content | -webkit-backdrop-filterbackdrop-filter | xh-material=frosted | --xh-_material-backdrop | popover 的 content 部件 -webkit-backdrop-filter、backdrop-filter 覆盖槽。 |
--xh-popover-bg | arrowcontent | background | defaultnot([data-xh-action-control])xh-material=frosted | --xh-_material-bg--xh-material-frosted-bg | popover 的 arrow、content 部件 background 覆盖槽。 |
--xh-popover-border | arrowcontent | border | defaultnot([data-xh-action-control])xh-material=frosted | --xh-_material-border--xh-material-frosted-border | popover 的 arrow、content 部件 border 覆盖槽。 |
--xh-popover-close-bg-active | close-trigger | background-color | disabledis(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_action-variant-bg-pressed | popover 的 close-trigger 部件 background-color 覆盖槽。 |
--xh-popover-close-bg-focus | close-trigger | background-color | focus-visible | --xh-_action-variant-bg-focus-visible | popover 的 close-trigger 部件 background-color 覆盖槽。 |
--xh-popover-close-bg-hover | close-trigger | background-color | disabledhoverloadingnot([data-disabled])not([data-loading]) | --xh-_action-variant-bg-hover | popover 的 close-trigger 部件 background-color 覆盖槽。 |
--xh-popover-close-fg | close-trigger | color | default | --xh-material-frosted-fg-muted | popover 的 close-trigger 部件 color 覆盖槽。 |
--xh-popover-close-fg-focus | close-trigger | color | focus-visible | --xh-popover-close-fg-hover | popover 的 close-trigger 部件 color 覆盖槽。 |
--xh-popover-close-fg-hover | close-trigger | color | disabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_action-variant-fg-focus-visible--xh-_action-variant-fg-hover--xh-_action-variant-fg-pressed | popover 的 close-trigger 部件 color 覆盖槽。 |
--xh-popover-close-radius | close-trigger | border-radius | default | --xh-shape-control | popover 的 close-trigger 部件 border-radius 覆盖槽。 |
--xh-popover-close-size | close-triggercontenttitle | block-sizeinline-sizepadding-inline-end | defaulthas([data-scope='popover'][data-part='close-trigger'])xh-action-profile=icon | --xh-_action-profile-visual-size--xh-control-h-sm | popover 的 close-trigger、content、title 部件 block-size、inline-size、padding-inline-end 覆盖槽。 |
--xh-popover-description-fg | description | color | default | --xh-fg-muted | popover 的 description 部件 color 覆盖槽。 |
--xh-popover-description-font-size | description | font-size | default | --xh-text-secondary-size | popover 的 description 部件 font-size 覆盖槽。 |
--xh-popover-fg | content | color | not([data-xh-action-control])xh-material=frosted | --xh-_material-fg | popover 的 content 部件 color 覆盖槽。 |
--xh-popover-gap | content | gap | default | --xh-space-2 | popover 的 content 部件 gap 覆盖槽。 |
--xh-popover-icon-size | close-triggercontenttrigger | --xh-icon-size | default | --xh-_action-profile-glyph-size--xh-glyph-size-md | popover 的 close-trigger、content、trigger 部件 --xh-icon-size 覆盖槽。 |
--xh-popover-layer | positioner | z-index | default | --xh-_layer | popover 的 positioner 部件 z-index 覆盖槽。 |
--xh-popover-max-h | content | max-block-size | default | --xh-overlay-max-h | popover 的 content 部件 max-block-size 覆盖槽。 |
--xh-popover-max-w | content | max-inline-size | default | --xh-_popover-max-w | popover 的 content 部件 max-inline-size 覆盖槽。 |
--xh-popover-px | content | padding-inline | default | --xh-_popover-pad | popover 的 content 部件 padding-inline 覆盖槽。 |
--xh-popover-py | content | padding-block | default | --xh-_popover-pad | popover 的 content 部件 padding-block 覆盖槽。 |
--xh-popover-radius | content | border-radius | default | --xh-shape-overlay | popover 的 content 部件 border-radius 覆盖槽。 |
--xh-popover-shadow | content | box-shadow | not([data-xh-action-control])xh-material=frosted | --xh-_material-shadow | popover 的 content 部件 box-shadow 覆盖槽。 |
--xh-popover-title-fg | title | color | default | --xh-material-frosted-fg | popover 的 title 部件 color 覆盖槽。 |
--xh-popover-title-font-size | title | font-size | default | --xh-text-label-size | popover 的 title 部件 font-size 覆盖槽。 |
--xh-popover-title-font-weight | title | font-weight | default | --xh-font-weight-semibold | popover 的 title 部件 font-weight 覆盖槽。 |
动效
动效角色:按压 · 状态 · 出现(锚定面板)(见动效规范)。
共享关键帧 xh-overlay-pop-in · xh-pop-out 由 family/motion.css 提供,皮肤 @import 它,单独引入仍成立。时长与缓动读动效令牌,改令牌即改全局节奏。
皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。
系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。
RTL
皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像。
