Popover

Popover 非常适合用于浮动面板,包含任意内容,例如导航菜单、移动菜单和弹出菜单。

要开始使用,请通过 npm 安装无状态 UI

npm install @headlessui/react

Popover 使用 PopoverPopoverButtonPopoverPanel 组件构建。

点击 PopoverButton 会自动打开/关闭 PopoverPanel。当面板打开时,点击其内容以外的任何位置、按下 Escape 键或使用 Tab 键移出面板都会关闭 Popover。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
      <PopoverPanel anchor="bottom" className="flex flex-col">
        <a href="/analytics">Analytics</a>
        <a href="/engagement">Engagement</a>
        <a href="/security">Security</a>
        <a href="/integrations">Integrations</a>
      </PopoverPanel>
    </Popover>
  )
}

无状态 UI 会跟踪每个组件的许多状态,例如当前选中的列表框选项,Popover 是打开还是关闭,或者键盘当前聚焦的 Popover 中的哪个项目。

但是,由于这些组件是无状态的,并且在默认情况下完全没有样式,因此在您自己提供每个状态所需的样式之前,您无法在 UI 中看到这些信息。

为无状态 UI 组件的不同状态设置样式的最简单方法是使用每个组件公开的 data-* 属性。

例如,Popover 组件公开了一个 data-open 属性,它告诉您 Popover 当前是否打开。

<!-- Rendered `Popover` -->
<div data-open>
  <button data-open>Solutions</button>
  <div data-open>
    <a href="/insights">Insights</a>
    <a href="/automations">Automations</a>
    <a href="/reports">Reports</a>
  </div>
</div>

使用 CSS 属性选择器 根据这些数据属性的存在与否有条件地应用样式。如果您使用的是 Tailwind CSS,则 数据属性修饰符 会使这变得更加容易

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'

function Example() {
  return (
<Popover className="group">
<PopoverButton className="flex items-center gap-2"> Solutions
<ChevronDownIcon className="size-5 group-data-[open]:rotate-180" />
</PopoverButton> <PopoverPanel anchor="bottom" className="flex flex-col"> <a href="/insights">Insights</a> <a href="/automations">Automations</a> <a href="/reports">Reports</a> </PopoverPanel> </Popover> ) }

请参阅 组件 API,了解所有可用数据属性的列表。

每个组件还通过 渲染道具 公开其当前状态的信息,您可以使用这些信息有条件地应用不同的样式或呈现不同的内容。

例如,Popover 组件公开了一个 open 状态,它告诉您 Popover 当前是否打开。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx'

function Example() {
  return (
    <Popover>
{({ open }) => (
<> <PopoverButton className="flex items-center gap-2"> Solutions
<ChevronDownIcon className={clsx('size-5', open && 'rotate-180')} />
</PopoverButton> <PopoverPanel anchor="bottom" className="flex flex-col"> <a href="/insights">Insights</a> <a href="/automations">Automations</a> <a href="/reports">Reports</a> </PopoverPanel> </>
)}
</Popover> ) }

请参阅 组件 API,了解所有可用渲染道具的列表。

在渲染多个相关的 Popover 时,例如在网站的标题导航中,请使用 PopoverGroup 组件。这将确保面板在用户在组内的 Popover 之间使用 Tab 键切换时保持打开状态,但一旦用户使用 Tab 键移出组,就会关闭任何打开的面板。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
<PopoverGroup>
<Popover> <PopoverButton>Product</PopoverButton> <PopoverPanel>{/* ... */}</PopoverPanel> </Popover> <Popover> <PopoverButton>Solutions</PopoverButton> <PopoverPanel>{/* ... */}</PopoverPanel> </Popover> <Popover> <PopoverButton>Pricing</PopoverButton> <PopoverPanel>{/* ... */}</PopoverPanel> </Popover>
</PopoverGroup>
) }

PopoverPanel 默认情况下没有设置宽度,但您可以使用 CSS 添加宽度。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor="bottom" className="w-52">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

如果您希望面板宽度与 PopoverButton 宽度匹配,请使用 PopoverPanel 元素上公开的 --button-width CSS 变量。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor="bottom" className="flex w-[var(--button-width)] flex-col">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

anchor 属性添加到 PopoverPanel 以自动定位面板相对于 PopoverButton

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor="bottom start" className="flex flex-col">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

使用 toprightbottomleft 值将面板沿相应的边线居中,或将其与 startend 结合使用,以将面板对齐到特定角落,例如 top startbottom end

要控制按钮和面板之间的间隙,请使用 --anchor-gap CSS 变量。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor="bottom start" className="flex flex-col [--anchor-gap:4px] sm:[--anchor-gap:8px]">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

此外,您可以使用 --anchor-offset 控制面板应从其原始位置偏移的距离,并使用 --anchor-padding 控制面板与视窗之间应存在的最小空间。

anchor 属性还支持对象 API,允许您使用 JavaScript 控制 gapoffsetpadding 值。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor={{ to: 'bottom start', gap: '4px' }} className="flex flex-col">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

有关这些选项的更多信息,请参阅 PopoverPanel API

如果您希望在打开 Popover 时在应用程序 UI 上设置背景样式,请使用 PopoverBackdrop 组件。

import { Popover, PopoverButton, PopoverBackdrop, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverBackdrop className="fixed inset-0 bg-black/15" />
<PopoverPanel anchor="bottom" className="flex flex-col bg-white"> <a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

在本例中,我们将 PopoverBackdrop 放在 DOM 中 Panel 之前,这样它就不会覆盖面板的内容。

但与所有其他组件一样,PopoverBackdrop 是完全无状态的,因此如何设置其样式取决于您。

要为 Popover 面板的打开和关闭设置动画,请将 transition 属性添加到 PopoverPanel 组件,然后使用 CSS 为过渡的不同阶段设置样式。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover>
      <PopoverButton>Solutions</PopoverButton>
      <PopoverPanel
        anchor="bottom"
transition
className="flex origin-top flex-col transition duration-200 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
>
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

如果您有背景,则可以通过将 transition 属性添加到 PopoverBackdrop 来独立于面板为其设置动画。

import { Popover, PopoverBackdrop, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
      <PopoverBackdrop
transition
className="fixed inset-0 bg-black/15 transition duration-100 ease-out data-[closed]:opacity-0"
/>
<PopoverPanel anchor="bottom"
transition
className="flex origin-top flex-col bg-white transition duration-200 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
>
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

在内部,transition 属性的实现方式与 Transition 组件完全相同。请参阅 Transition 文档 了解详情。

无状态 UI 还与 React 生态系统中的其他动画库(如 Framer MotionReact Spring)很好地组合在一起。您只需要向这些库公开一些状态即可。

例如,要使用 Framer Motion 为 Popover 设置动画,请将 static 属性添加到 PopoverPanel 组件,然后根据 open 渲染道具有条件地渲染它。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'

function Example() {
  return (
    <Popover>
{({ open }) => (
<> <PopoverButton>Solutions</PopoverButton> <AnimatePresence>
{open && (
<PopoverPanel
static
as={motion.div} initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} anchor="bottom" className="flex origin-top flex-col" >
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel>
)}
</AnimatePresence> </>
)}
</Popover> ) }

由于 Popover 可以包含交互式内容,例如表单控件,因此我们无法像处理 Menu 组件那样,在您点击其内部的某些内容时自动关闭它们。

要在点击面板的子级时手动关闭 Popover,请将该子级渲染为 CloseButton。您可以使用 as 属性来自定义要渲染的元素。

import { CloseButton, Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import MyLink from './MyLink'

function Example() {
  return (
    <Popover>
      <PopoverButton>Solutions</PopoverButton>
      <PopoverPanel anchor="bottom">
<CloseButton as={MyLink} href="/insights">
Insights
</CloseButton>
{/* ... */} </PopoverPanel> </Popover> ) }

PopoverPopoverPanel 还公开了一个 close 渲染道具,您可以使用它以命令式方式关闭面板,例如在运行异步操作之后。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover>
      <PopoverButton>Terms</PopoverButton>
      <PopoverPanel>
{({ close }) => (
<button
onClick={async () => {
await fetch('/accept-terms', { method: 'POST' })
close()
}}
>
Read and accept
</button>
)}
</PopoverPanel> </Popover> ) }

默认情况下,PopoverButton 在调用 close 之后会接收焦点,但您可以通过将 ref 传递到 close(ref) 中来更改此行为。

最后,无状态 UI 还提供了一个 useClose 钩子,可用于在您没有轻松访问 close 渲染道具时(例如在嵌套组件中)以命令式方式关闭最近的 Popover 祖先。

import { Popover, PopoverButton, PopoverPanel, useClose } from '@headlessui/react'

function MySearchForm() {
let close = useClose()
return ( <form onSubmit={(event) => { event.preventDefault() /* Perform search... */
close()
}}
>
<input type="search" /> <button type="submit">Submit</button> </form> ) } function Example() { return ( <Popover> <PopoverButton>Filters</PopoverButton> <PopoverPanel> <MySearchForm /> {/* ... */} </PopoverPanel> </Popover> ) }

useClose 钩子必须在嵌套在 Popover 中的组件中使用,否则它将不起作用。

默认情况下,Popover 及其子组件都会渲染一个对该组件有意义的默认元素。

PopoverPopoverBackdropPopoverPanelPopoverGroup 组件都会渲染一个 <div>,而 PopoverButton 组件则渲染一个 <button>

使用 as 属性将组件渲染为不同的元素或您自己的自定义组件,确保您的自定义组件 转发 refs,以便无状态 UI 可以正确地连接它们。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { forwardRef } from 'react'

let MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />
})
function Example() { return (
<Popover as="nav">
<PopoverButton as={MyCustomButton}>Solutions</PopoverButton>
<PopoverPanel as="form">{/* ... */}</PopoverPanel>
</Popover> ) }

命令描述

Enter 或者 SpacePopoverButton 获得焦点时。

切换面板

Esc

关闭所有打开的弹窗

Tab

在打开的面板内容之间循环

从打开的面板中按下 Tab 键将关闭该面板,从一个打开的面板的按钮到另一个兄弟弹窗的按钮(在 PopoverGroup 内)将关闭第一个面板。

Shift + Tab

反向循环焦点顺序

主要的弹窗组件。

属性默认值描述
asdiv
String | Component

弹窗应该渲染的元素或组件。popover应该渲染为。

数据属性渲染道具描述
data-openopen

Boolean

弹窗是否打开。popover是打开的。

close

(ref) => void

关闭弹窗并将焦点重新设置到 PopoverButton。可以选择性地传递一个 refHTMLElement 来代替焦点。

这可以用来创建弹窗组件的背景。点击背景将关闭弹窗。

属性默认值描述
asdiv
String | Component

弹窗应该渲染的元素或组件。弹窗背景应该渲染为。

transitionfalse
Boolean

元素是否应该渲染过渡属性,例如 data-closed data-enterdata-leave

数据属性渲染道具描述
data-openopen

Boolean

弹窗是否打开。popover是打开的。

这是用来切换弹窗的触发组件。

属性默认值描述
asbutton
String | Component

弹窗应该渲染的元素或组件。弹窗按钮应该渲染为。

disabledfalse
Boolean

弹窗是否打开。弹窗按钮是否禁用.

autoFocusfalse
Boolean

弹窗是否打开。弹窗按钮是否在首次渲染时获得焦点。

数据属性渲染道具描述
data-openopen

Boolean

弹窗是否打开。popover是打开的。

data-focusfocus

Boolean

弹窗是否打开。弹窗按钮是否获得焦点。

data-hoverhover

Boolean

弹窗是否打开。弹窗按钮是否悬停。

data-activeactive

Boolean

弹窗是否打开。弹窗按钮是否处于活动或按下状态。

data-autofocusautofocus

Boolean

autoFocus 属性是否设置为 true

这个组件包含弹窗的内容。

属性默认值描述
asdiv
String | Component

弹窗应该渲染的元素或组件。弹窗面板应该渲染为。

transitionfalse
Boolean

元素是否应该渲染过渡属性,例如 data-closed data-enterdata-leave

anchor
Object

配置面板如何锚定到按钮。

anchor.tobottom
String

相对于触发器的位置。弹窗面板相对于触发器。

使用 toprightbottomleft 将其置于相应边缘的中心,或将其与 startend 组合以将其对齐到特定的角落,例如 top startbottom end 弹窗面板到特定角落,例如 top startbottom end弹窗面板

anchor.gap0
Number | String

面板和触发器之间的间距。弹窗按钮弹窗面板.

还可以使用 --anchor-gap CSS 变量控制。

anchor.offset0
Number | String

面板应该从其原始位置移动的距离。弹窗面板应该从其原始位置移动的距离。

还可以使用 --anchor-offset CSS 变量控制。

anchor.padding0
Number | String

面板和视窗之间的最小间距。弹窗面板

还可以使用 --anchor-padding CSS 变量控制。

staticfalse
Boolean

元素是否应该忽略内部管理的打开/关闭状态。

unmounttrue
Boolean

元素是否应该根据打开/关闭状态卸载或隐藏。

portalfalse
Boolean

元素是否应该渲染在门户网站中。

anchor 属性设置时,会自动设置为 true

modalfalse
Boolean

是否启用辅助功能,例如滚动锁定和焦点捕获。

focusfalse
Boolean

当弹窗打开时,这将强制在 PopoverPanel 内设置焦点。如果焦点离开此组件,它也将关闭弹窗。

数据属性渲染道具描述
data-openopen

Boolean

弹窗是否打开。popover是打开的。

close

(ref) => void

关闭弹窗并将焦点重新设置到 PopoverButton。可以选择性地传递一个 refHTMLElement 来代替焦点。

通过将相关联的兄弟弹窗包装在 PopoverGroup 中来链接它们。从一个 PopoverPanel 中按下 Tab 键将使下一个弹窗的 PopoverButton 获得焦点,从 PopoverGroup 外部按下 Tab 键将完全关闭组内的所有弹窗。

属性默认值描述
asdiv
String | Component

弹窗应该渲染的元素或组件。弹窗组应该渲染为。

当点击此按钮时,它将关闭最近的 PopoverPanel 父级。或者,使用 useClose 钩子以命令方式关闭弹窗面板。

属性默认值描述
asbutton
String | Component

弹窗应该渲染的元素或组件。关闭按钮应该渲染为。

如果你有兴趣使用无头 UI 的预先设计的 Tailwind CSS 弹窗组件示例,请查看 Tailwind UI — 由我们精心设计和制作的精美组件集合。

这是支持我们对诸如这个之类的开源项目的贡献的一个好方法,并使我们能够改进它们并保持良好的维护。