backdrop
组件

Popover

通过按钮触发,在 portal 中显示富内容。

功能

  • 可受控或非受控
  • 自定义边、对齐、偏移、碰撞处理
  • 可选显示指向箭头
  • 完全管理并支持自定义的焦点控制
  • 支持模态和非模态
  • 高度可自定义的关闭和层级行为

安装

在命令行中安装组件。

sh
$ npm add reka-ui

结构

导入所有部件并进行组合。

vue
<script setup>
import { PopoverAnchor, PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
</script>

<template>
  <PopoverRoot>
    <PopoverTrigger />
    <PopoverAnchor />
    <PopoverPortal>
      <PopoverContent>
        <PopoverClose />
        <PopoverArrow />
      </PopoverContent>
    </PopoverPortal>
  </PopoverRoot>
</template>

API 参考

Root

包含 Popover 的所有部件。

PropDefaultType
defaultOpen
false
boolean

The open state of the popover when it is initially rendered. Use when you do not need to control its open state.

modal
false
boolean

The modality of the popover. When set to true, interaction with outside elements will be disabled and only popover content will be visible to screen readers.

open
boolean

The controlled open state of the popover.

EmitPayload
update:open
[value: boolean]

Event handler called when the open state of the popover changes.

Slots (default)Payload
open
boolean

Current open state

close
(): void

Close the popover

Trigger

用于切换 Popover 显示状态的按钮。默认情况下,PopoverContent 会相对于此触发器进行定位。

PropDefaultType
as
'button'
AsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Data AttributeValue
[data-state]"open" | "closed"

Anchor

用于作为 PopoverContent 定位基准的可选元素。如果未使用此部件,内容将相对于 PopoverTrigger 进行定位。

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

reference
ReferenceElement

The reference (or anchor) element that is being referred to for positioning.

If not provided will use the current component as anchor.

Portal

使用此部件时,会将内容部分传送到 body 中。

PropDefaultType
defer
boolean

Defer the resolving of a Teleport target until other parts of the application have mounted (requires Vue 3.5.0+)

reference

disabled
boolean

Disable teleport and render the component inline

reference

forceMount
boolean

Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

to
string | HTMLElement

Vue native teleport component prop :to

reference

Content

Popover 打开时弹出的组件。

tip
Built with Presence component - supports any animation techniques while maintaining access to presence emitted events.
PropDefaultType
align
'start' | 'center' | 'end'

The preferred alignment against the trigger. May change when collisions occur.

alignFlip
boolean

Flip alignment when colliding with boundary. May only occur when prioritizePosition is true.

alignOffset
number

An offset in pixels from the start or end alignment options.

arrowPadding
number

The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it from overflowing the corners.

as
'div'
AsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

avoidCollisions
boolean

When true, overrides the side and align preferences to prevent collisions with boundary edges.

collisionBoundary
Element | (Element | null)[] | null

The element used as the collision boundary. By default this is the viewport, though you can provide additional element(s) to be included in this check.

collisionPadding
number | Partial<Record<'top' | 'right' | 'bottom' | 'left', number>>

The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { top: 20, left: 20 }.

disableOutsidePointerEvents
boolean

When true, hover/focus/click interactions will be disabled on elements outside the DismissableLayer. Users will need to click twice on outside elements to interact with them: once to close the DismissableLayer, and again to trigger the element.

disableUpdateOnLayoutShift
boolean

Whether to disable the update position for the content when the layout shifted.

forceMount
boolean

Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

hideWhenDetached
boolean

Whether to hide the content when the trigger becomes fully occluded.

positionStrategy
'fixed' | 'absolute'

The type of CSS position property to use.

prioritizePosition
boolean

Force content to be position within the viewport.

Might overlap the reference element, which may not be desired.

reference
ReferenceElement

The custom element or virtual element that will be set as the reference to position the floating element.

If provided, it will replace the default anchor element.

side
'top' | 'right' | 'bottom' | 'left'

The preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled.

sideFlip
boolean

Flip to the opposite side when colliding with boundary.

sideOffset
number

The distance in pixels from the trigger.

sticky
'partial' | 'always'

The sticky behavior on the align axis. partial will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless.

updatePositionStrategy
'always' | 'optimized'

Strategy to update the position of the floating element on every animation frame.

EmitPayload
closeAutoFocus
[event: Event]

Event handler called when auto-focusing on close. Can be prevented.

escapeKeyDown
[event: KeyboardEvent]

Event handler called when the escape key is down. Can be prevented.

focusOutside
[event: FocusOutsideEvent]

Event handler called when the focus moves outside of the DismissableLayer. Can be prevented.

interactOutside
[event: PointerDownOutsideEvent | FocusOutsideEvent]

Event handler called when an interaction happens outside the DismissableLayer. Specifically, when a pointerdown event happens outside or focus moves outside of it. Can be prevented.

openAutoFocus
[event: Event]

Event handler called when auto-focusing on open. Can be prevented.

pointerDownOutside
[event: PointerDownOutsideEvent]

Event handler called when a pointerdown event happens outside of the DismissableLayer. Can be prevented.

Data AttributeValue
[data-state]"open" | "closed"
[data-side]"left" | "right" | "bottom" | "top"
[data-align]"start" | "end" | "center"
CSS VariableDescription
--reka-popover-content-transform-origin
根据内容和箭头的位置/偏移计算出的 transform-origin
--reka-popover-content-available-width
触发器和边界边缘之间的剩余宽度
--reka-popover-content-available-height
触发器和边界边缘之间的剩余高度
--reka-popover-trigger-width
触发器的宽度
--reka-popover-trigger-height
触发器的高度

Arrow

可选的箭头元素,随 Popover 一起显示。可用于在视觉上连接锚点和 PopoverContent。必须在 PopoverContent 内部渲染。

PropDefaultType
as
'svg'
AsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

height
5
number

The height of the arrow in pixels.

rounded
boolean

When true, render the rounded version of arrow. Do not work with as/asChild

width
10
number

The width of the arrow in pixels.

Close

用于关闭已打开 Popover 的按钮。

PropDefaultType
as
'button'
AsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

示例

限制内容尺寸

您可能需要限制内容的宽度以匹配触发器的宽度。您可能还希望限制其高度,使其不超过视口。

我们提供了多个 CSS 自定义属性,例如 --reka-popover-trigger-width--reka-popover-content-available-height 来支持此需求。使用它们来限制内容的尺寸。

vue
<script setup>
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
</script>

<template>
  <PopoverRoot>
    <PopoverTrigger>…</PopoverTrigger>
    <PopoverPortal>
      <PopoverContent
        class="PopoverContent"
        :side-offset="5"
      >

      </PopoverContent>
    </PopoverPortal>
  </PopoverRoot>
</template>
css
/* styles.css */
.PopoverContent {
  width: var(--reka-popover-trigger-width);
  max-height: var(--reka-popover-content-available-height);
}

感知原点的动画

我们提供了一个 CSS 自定义属性 --reka-popover-content-transform-origin。使用它可以根据 sidesideOffsetalignalignOffset 以及任何碰撞情况,从计算出的原点对内容进行动画处理。

vue
<script setup>
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
</script>

<template>
  <PopoverRoot>
    <PopoverTrigger>…</PopoverTrigger>
    <PopoverPortal>
      <PopoverContent class="PopoverContent">

      </PopoverContent>
    </PopoverPortal>
  </PopoverRoot>
</template>
css
/* styles.css */
.PopoverContent {
  transform-origin: var(--reka-popover-content-transform-origin);
  animation: scaleIn 0.5s ease-out;
}

@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

感知碰撞的动画

我们提供了 data-sidedata-align 属性。它们的值会在运行时根据碰撞情况发生变化。使用它们来创建感知碰撞和方向的动画。

vue
<script setup>
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
</script>

<template>
  <PopoverRoot>
    <PopoverTrigger>…</PopoverTrigger>
    <PopoverPortal>
      <PopoverContent class="PopoverContent">

      </PopoverContent>
    </PopoverPortal>
  </PopoverRoot>
</template>
css
/* styles.css */
.PopoverContent {
  animation-duration: 0.6s;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.PopoverContent[data-side="top"] {
  animation-name: slideUp;
}
.PopoverContent[data-side="bottom"] {
  animation-name: slideDown;
}

@keyframes slideDown {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

使用自定义锚点

如果您不希望使用触发器作为锚点,可以将内容锚定到另一个元素。

vue
<script setup>
import { PopoverAnchor, PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
</script>

<template>
  <PopoverRoot>
    <PopoverAnchor as-child>
      <div class="Row">
        行作为锚点 <PopoverTrigger>触发器</PopoverTrigger>
      </div>
    </PopoverAnchor>

    <PopoverPortal>
      <PopoverContent>…</PopoverContent>
    </PopoverPortal>
  </PopoverRoot>
</template>
css
/* styles.css */
.Row {
  background-color: gainsboro;
  padding: 20px;
}

使用插槽 Prop 关闭

或者,您可以使用 PopoverRoot 的插槽 Prop 提供的 close 方法,以编程方式关闭 Popover。

vue
<script setup>
import { PopoverAnchor, PopoverArrow, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
</script>

<template>
  <PopoverRoot v-slot="{ close }">
    <PopoverTrigger>打开</PopoverTrigger>
    <PopoverAnchor />
    <PopoverPortal>
      <PopoverContent>
        <button type="submit" @click="close">
          提交
        </button>
        <PopoverArrow />
      </PopoverContent>
    </PopoverPortal>
  </PopoverRoot>
</template>

可访问性

遵循 Dialog WAI-ARIA 设计模式

键盘交互

KeyDescription
Space
打开/关闭 Popover。
Enter
打开/关闭 Popover。
Tab
将焦点移动到下一个可聚焦元素
Shift + Tab
将焦点移动到上一个可聚焦元素
Esc
关闭 Popover 并将焦点移动到 PopoverTrigger

自定义 API

通过将原始部件抽象到您自己的组件中来创建您自己的 API。

抽象箭头并设置默认配置

此示例抽象了 PopoverArrow 部件并设置了默认的 sideOffset 配置。

用法

vue
<script setup lang="ts">
import { Popover, PopoverContent, PopoverTrigger } from './your-popover'
</script>

<template>
  <Popover>
    <PopoverTrigger>Popover trigger</PopoverTrigger>
    <PopoverContent>Popover content</PopoverContent>
  </Popover>
</template>

实现

ts
// your-popover.ts
export { default as PopoverContent } from 'PopoverContent.vue'

export { PopoverRoot as Popover, PopoverTrigger } from 'reka-ui'
vue
<!-- PopoverContent.vue -->
<script setup lang="ts">
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'
import { PopoverContent, PopoverPortal, useForwardPropsEmits } from 'reka-ui'

const props = defineProps<PopoverContentProps>()
const emits = defineEmits<PopoverContentEmits>()

const forwarded = useForwardPropsEmits(props, emits)
</script>

<template>
  <PopoverPortal>
    <PopoverContent v-bind="{ ...forwarded, ...$attrs }">
      <slot />
    </PopoverContent>
  </PopoverPortal>
</template>