这条评论来自 @星见书音 ,发表于三天前。
<script setup lang="ts">
import { Avatar, HoverCard, Inline, Link, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Text>
这条评论来自
<HoverCard>
<Link href="#">@星见书音</Link>
<template #content>
<Stack gap="sm" class="w-64">
<Inline gap="sm" align="center">
<Avatar src="/avatars/selfie.webp" name="星见书音" size="md" />
<Stack gap="none">
<Text weight="medium">星见书音</Text>
<Text tone="muted" size="sm">@shion</Text>
</Stack>
</Inline>
<Text size="sm">读书、写字、偶尔画画。正在补完今年的新番。</Text>
<Inline gap="md">
<Text size="sm">
<Text as="span" weight="medium">128</Text>
关注
</Text>
<Text size="sm">
<Text as="span" weight="medium">2,048</Text>
粉丝
</Text>
</Inline>
</Stack>
</template>
</HoverCard>
,发表于三天前。
</Text>
</template>
用法
import { HoverCard } from '@hina-ui/vue'
悬停卡片给一个链接配上预览:指针在触发器上停留一段时间后浮出卡片,移开后收回;用键盘聚焦触发器时也按 openDelay 延时浮出。默认插槽是触发器,通常是一个链接;content 插槽是卡片内容。它只是看一眼的东西,不停止页面滚动,也不接管焦点。
<script setup lang="ts">
import { HoverCard, Link, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<HoverCard>
<Link href="#">Reka UI</Link>
<template #content>
<Stack gap="xs" class="w-64">
<Text weight="medium">Reka UI</Text>
<Text tone="muted" size="sm">
一套无样式、可访问的 Vue 组件基础,本库的大部分交互件建在它上面。
</Text>
</Stack>
</template>
</HoverCard>
</template>
示例
位置
side 指定卡片朝哪个方向浮出,align 指定它与触发器的对齐方式,与 Popover 相同,默认在正下方。
<script setup lang="ts">
import { HoverCard, Inline, Link, Text } from '@hina-ui/vue'
const sides = ['top', 'right', 'bottom', 'left'] as const
const names = { top: '上方', right: '右侧', bottom: '下方', left: '左侧' }
</script>
<template>
<Inline gap="lg">
<HoverCard v-for="side in sides" :key="side" :side="side">
<Link href="#">{{ names[side] }}</Link>
<template #content>
<Text size="sm">从{{ names[side] }}浮出的卡片。</Text>
</template>
</HoverCard>
</Inline>
</template>
延时
openDelay 是指针停留多久后浮出,closeDelay 是移开多久后收回,单位毫秒。指针从触发器移进卡片时不会收回。
<script setup lang="ts">
import { HoverCard, Inline, Link, Text } from '@hina-ui/vue'
</script>
<template>
<Inline gap="lg">
<HoverCard :open-delay="0" :close-delay="0">
<Link href="#">立即浮出</Link>
<template #content>
<Text size="sm">没有延时,指针一到就出现。</Text>
</template>
</HoverCard>
<HoverCard :open-delay="800" :close-delay="400">
<Link href="#">停留久一点</Link>
<template #content>
<Text size="sm">停留 800 毫秒才出现,移开 400 毫秒后收回。</Text>
</template>
</HoverCard>
</Inline>
</template>
受控
open 支持双向绑定。卡片仍然会因指针移开、点击外部或者按 Esc 而收回,并把 false 写回,外部的控件只负责打开。
已收回
<script setup lang="ts">
import { ref } from 'vue'
import { Button, HoverCard, Inline, Link, Text } from '@hina-ui/vue'
const open = ref(false)
</script>
<template>
<Inline gap="lg" align="center">
<Button variant="outline" tone="neutral" :disabled="open" @click="open = true">打开卡片</Button>
<HoverCard v-model:open="open">
<Link href="#">查看预览</Link>
<template #content>
<Text size="sm">卡片由外部打开;指针移开、点击外部或者按 Esc 都会收回。</Text>
</template>
</HoverCard>
<Text tone="muted" size="sm">{{ open ? '已打开' : '已收回' }}</Text>
</Inline>
</template>
外部锚点
默认插槽留空时,anchor 接收外部元素,用 v-model:open 控制打开。切换 anchor 与卡片内容即可让多个触发点共用一个卡片。提供默认插槽时,仍使用插槽中的触发器。
外部锚点不会自动打开卡片。直接设置 open = true 会立即打开,不经过 openDelay;移开后仍按 closeDelay 收回,移入卡片保持打开,键盘焦点离开锚点后收回。锚点清空、从文档移除,或其外层发生滚动时,卡片关闭。
示例使用 Button 绑定指针和键盘焦点事件。
退场期间仍跟随页面上有效的锚点;锚点清空或移除后,使用最后的位置完成退场。
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { Button, HoverCard, Inline, Stack, Text } from '@hina-ui/vue'
const open = ref(false)
const anchor = shallowRef<HTMLElement | null>(null)
const current = ref(1)
function show(event: PointerEvent | FocusEvent, index: number) {
const target = event.currentTarget as HTMLElement
if ('pointerType' in event ? event.pointerType === 'touch' : !target.matches(':focus-visible'))
return
anchor.value = target
current.value = index
open.value = true
}
</script>
<template>
<Inline>
<Button
v-for="index in 3"
:key="index"
variant="outline"
tone="neutral"
@pointerenter="show($event, index)"
@focus="show($event, index)"
>
锚点 {{ index }}
</Button>
<HoverCard v-model:open="open" :anchor="anchor">
<template #content>
<Stack gap="xs" class="w-56">
<Text weight="medium">锚点 {{ current }}</Text>
<Text tone="muted" size="sm">同一个卡片随当前锚点切换位置与内容。</Text>
</Stack>
</template>
</HoverCard>
</Inline>
</template>
虚拟锚点与持续跟随
anchor 也接受带 getBoundingClientRect() 的对象,返回视口坐标中的矩形。OverlayAnchor 类型可从包根导入;回调需要返回最新坐标,同一个对象不必反复替换。
可选的 contextElement 指定坐标所属的元素,用于识别滚动祖先和裁剪边界;它不会成为触发器,也不会扩大浮层的交互区域。
updatePositionStrategy 默认是 'optimized',在滚动、尺寸和布局变化时更新位置。设置为 'always' 后,挂载期间逐帧检查矩形,持续跟随仅有坐标变化的锚点。可以在打开期间切换策略。退场期间继续跟随,锚点清空或所属元素移除后保留最后的位置,卸载后停止测量。
虚拟锚点没有悬停区域,开关由 v-model:open 控制;指针移开和祖先滚动不会关闭卡片,点击外部或按 Esc 仍会关闭。元素锚点的悬停、延迟关闭和滚动关闭行为不变。
示例用 Button 打开面板,并用 ScrollArea 提供滚动容器。面板同时跟随坐标变化和容器滚动。
向下滚动可观察定位变化。
<script setup lang="ts">
import { ref, shallowRef, watch } from 'vue'
import { useRafFn } from '@vueuse/core'
import {
Button,
ScrollArea,
Inline,
Stack,
Text,
HoverCard,
type OverlayAnchor,
} from '@hina-ui/vue'
const open = ref(false)
const surface = ref<InstanceType<typeof Stack>>()
const anchor = shallowRef<OverlayAnchor | null>(null)
const x = ref(80)
const { pause, resume } = useRafFn(
({ delta }) => {
x.value = 80 + ((x.value - 80 + delta / 35) % 120)
},
{ immediate: false },
)
watch(open, value => (value ? resume() : pause()))
function show() {
const element = surface.value?.$el as HTMLElement | undefined
if (!element) return
anchor.value = {
contextElement: element,
getBoundingClientRect: () => {
const rect = element.getBoundingClientRect()
return new DOMRect(rect.left + x.value, rect.top + 100, 2, 20)
},
}
open.value = true
}
</script>
<template>
<Stack class="w-full">
<Inline>
<Button variant="outline" tone="neutral" @click="show">打开</Button>
<Text size="sm" tone="muted">向下滚动可观察定位变化。</Text>
</Inline>
<ScrollArea class="h-64 rounded-lg border border-line bg-inset">
<Stack ref="surface" class="relative h-128 shrink-0">
<Text
as="span"
aria-hidden="true"
class="absolute top-25 h-5 w-0.5 bg-accent"
:style="{ left: `${x}px` }"
/>
</Stack>
</ScrollArea>
<HoverCard v-model:open="open" :anchor="anchor" update-position-strategy="always" align="start">
<template #content>
<Text size="sm">移动坐标</Text>
</template>
</HoverCard>
</Stack>
</template>
定位层样式
positionerClass 为外层定位节点追加类名,class 仍作用于内层卡片。可以使用 utility 或全局 CSS 类定义移动过渡,与卡片的入退场动画分别控制。
示例在卡片已打开且切换锚点时启用 hn-transition-base,首次打开和关闭时移除移动过渡;减少动态效果偏好下不播放移动动画。触发器使用 Button。
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { Button, HoverCard, Inline, Stack, Text } from '@hina-ui/vue'
const open = ref(false)
const anchor = shallowRef<HTMLElement | null>(null)
const current = ref(1)
const moving = ref(false)
function show(event: PointerEvent | FocusEvent, index: number) {
const target = event.currentTarget as HTMLElement
if ('pointerType' in event ? event.pointerType === 'touch' : !target.matches(':focus-visible'))
return
if (!open.value) moving.value = false
else if (anchor.value !== target) moving.value = true
anchor.value = target
current.value = index
open.value = true
}
</script>
<template>
<Inline>
<Button
v-for="index in 3"
:key="index"
variant="outline"
tone="neutral"
@pointerenter="show($event, index)"
@focus="show($event, index)"
>
锚点 {{ index }}
</Button>
<HoverCard
v-model:open="open"
:anchor="anchor"
:positioner-class="
open && moving ? 'hn-transition-base motion-reduce:transition-none' : undefined
"
>
<template #content>
<Stack gap="xs" class="w-56">
<Text weight="medium">锚点 {{ current }}</Text>
<Text tone="muted" size="sm">同一个卡片平滑移动到当前锚点。</Text>
</Stack>
</template>
</HoverCard>
</Inline>
</template>
行为
- 指针停留
openDelay后浮出,移开closeDelay后收回;移进卡片内保持打开。 - 键盘聚焦插槽中的触发器时按
openDelay浮出,焦点离开后按closeDelay收回。 - 关闭延迟内移回卡片会取消关闭;退场动画开始后,卡片内容不再响应交互,移入原区域不会重新打开。
- 点击卡片外部或者按 Esc 也会收回。
- 卡片不停止页面滚动,页面其他部分照常可以交互。
- 触屏设备上不会因触摸浮出。
无障碍
- 卡片只是补充信息,其中的内容不应该是到达某处的唯一途径。
- 触发器应当是可以聚焦的元素,键盘用户才能看到卡片。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
anchor | OverlayAnchor | null | — | 定位元素或虚拟锚点,默认插槽为空时使用 |
updatePositionStrategy | 'optimized' | 'always' | 'optimized' | 定位更新策略 |
side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | 浮出的方向 |
align | 'start' | 'center' | 'end' | 'center' | 与触发器的对齐方式 |
sideOffset | number | 8 | 与触发器的距离,像素 |
openDelay | number | 300 | 停留多久后浮出,毫秒 |
closeDelay | number | 150 | 移开多久后收回,毫秒 |
padded | boolean | true | 卡片是否带内边距 |
open | boolean | — | 是否打开,支持双向绑定 |
class | string | — | 追加至卡片的类名 |
positionerClass | string | — | 追加至外层定位节点的类名 |
插槽
| 插槽 | 说明 |
|---|---|
| default | 可选触发器;留空时使用 anchor |
content | 卡片内容 |