Popover

弹出窗口非常适合浮动面板,其中包含任意内容,例如导航菜单、移动菜单和弹出菜单。

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

npm install @headlessui/react

弹出窗口使用 PopoverPopover.ButtonPopover.Panel 组件构建。

点击 Popover.Button 将自动打开/关闭 Popover.Panel。当面板打开时,点击其内容外部的任何位置、按 Esc 键或跳出面板将关闭弹出窗口。

import { Popover } from '@headlessui/react' function MyPopover() { return ( <Popover className="relative"> <Popover.Button>Solutions</Popover.Button> <Popover.Panel className="absolute z-10"> <div className="grid grid-cols-2"> <a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </div> <img src="/solutions.jpg" alt="" /> </Popover.Panel> </Popover> ) }

这些组件完全没有样式,因此如何对 Popover 进行样式化由你决定。在我们的示例中,我们在 Popover.Panel 上使用绝对定位来将其定位在 Popover.Button 附近,而不会干扰正常的文档流。

无状态 UI 会跟踪每个组件的许多状态信息,例如当前选中的哪个列表框选项、弹出窗口是打开还是关闭,或者当前通过键盘激活的弹出窗口中的哪个项目。

但由于这些组件是无状态的,并且在开箱即用时完全没有样式,因此你无法看到这些信息在你的 UI 中,除非你自己提供每个状态所需的样式。

每个组件都会通过你用来有条件地应用不同样式或渲染不同内容的渲染道具来公开其当前状态的信息。

例如,Popover 组件公开了一个 open 状态,它告诉你弹出窗口当前是否打开。

import { Popover } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/20/solid' function MyPopover() { return ( <Popover>
{({ open }) => (
/* Use the `open` state to conditionally change the direction of the chevron icon. */ <> <Popover.Button> Solutions <ChevronDownIcon className={open ? 'rotate-180 transform' : ''} /> </Popover.Button>
<Popover.Panel>
<a href="/insights">Insights</a> <a href="/automations">Automations</a> <a href="/reports">Reports</a> </Popover.Panel> </> )} </Popover> ) }

有关每个组件的完整渲染道具 API,请参阅组件 API 文档

每个组件还会通过一个 data-headlessui-state 属性公开其当前状态的信息,你可以使用它来有条件地应用不同的样式。

渲染道具 API中的任何状态为 true 时,它们将在该属性中列出为以空格分隔的字符串,以便你可以使用CSS 属性选择器[attr~=value] 的形式对其进行定位。

例如,这是 Popover 组件在弹出窗口打开时渲染的内容

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

如果你正在使用Tailwind CSS,你可以使用@headlessui/tailwindcss 插件来使用修饰符(如 ui-open:*)定位此属性

import { Popover } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/20/solid' function MyPopover() { return ( <Popover> <Popover.Button> Solutions
<ChevronDownIcon className="ui-open:rotate-180 ui-open:transform" />
</Popover.Button> <Popover.Panel> <a href="/insights">Insights</a> <a href="/automations">Automations</a> <a href="/reports">Reports</a> </Popover.Panel> </Popover> ) }

要使 Popover 实际渲染一个浮动面板,使其靠近你的按钮,你需要使用一些依赖于 CSS、JS 或两者的样式技术。在前面的示例中,我们使用了 CSS 绝对和相对定位,以便面板渲染在打开它的按钮附近。

对于更复杂的方法,你可能可以使用像Popper JS这样的库。这里我们使用 Popper 的 usePopper hook 来将 Popover.Panel 渲染为一个浮动面板,使其靠近按钮。

import { useState } from 'react' import { Popover } from '@headlessui/react' import { usePopper } from 'react-popper' function MyPopover() {
let [referenceElement, setReferenceElement] = useState()
let [popperElement, setPopperElement] = useState()
let { styles, attributes } = usePopper(referenceElement, popperElement)
return ( <Popover>
<Popover.Button ref={setReferenceElement}>Solutions</Popover.Button>
<Popover.Panel
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{/* ... */} </Popover.Panel> </Popover> ) }

默认情况下,你的 Popover.Panel 会根据 Popover 组件本身内部跟踪的内部打开状态自动显示/隐藏。

import { Popover } from '@headlessui/react' function MyPopover() { return ( <Popover> <Popover.Button>Solutions</Popover.Button> {/* By default, the `Popover.Panel` will automatically show/hide when the `Popover.Button` is pressed. */} <Popover.Panel>{/* ... */}</Popover.Panel> </Popover> ) }

如果你希望自己处理(也许是因为你出于某种原因需要添加一个额外的包装器元素),你可以将 static 属性传递给 Popover.Panel 以告诉它始终渲染,然后使用 open 渲染道具来自己控制何时显示/隐藏面板。

import { Popover } from '@headlessui/react' function MyPopover() { return ( <Popover> {({ open }) => ( <> <Popover.Button>Solutions</Popover.Button>
{open && (
<div>
{/*
Using the `static` prop, the `Popover.Panel` is always
rendered and the `open` state is ignored.
*/}
<Popover.Panel static>{/* ... */}</Popover.Panel>
</div>
)}
</> )} </Popover> ) }

由于弹出窗口可能包含交互式内容(如表单控件),因此我们无法像 Menu 组件那样,在你点击其内部的某些内容时自动关闭它们。

要在点击面板的子项时手动关闭弹出窗口,请将该子项渲染为 Popover.Button。你可以使用 as 属性来自定义要渲染的元素。

import { Popover } from '@headlessui/react' import MyLink from './MyLink' function MyPopover() { return ( <Popover> <Popover.Button>Solutions</Popover.Button> <Popover.Panel>
<Popover.Button as={MyLink} href="/insights">
Insights
</Popover.Button>
{/* ... */} </Popover.Panel> </Popover> ) }

或者,PopoverPopover.Panel 公开了一个 close() 渲染道具,你可以使用它来强制关闭面板,例如在运行异步操作之后

import { Popover } from '@headlessui/react' function MyPopover() { return ( <Popover> <Popover.Button>Terms</Popover.Button> <Popover.Panel>
{({ close }) => (
<button onClick={async () => {
await fetch('/accept-terms', { method: 'POST' })
close()
}}
>
Read and accept </button> )} </Popover.Panel> </Popover> ) }

默认情况下,Popover.Button 会在调用 close() 后接收焦点,但你可以通过将 ref 传递给 close(ref) 来更改此行为。

如果你希望在每次打开弹出窗口时在应用程序 UI 上添加一个背景,请使用 Popover.Overlay 组件

import { Popover } from '@headlessui/react' function MyPopover() { return ( <Popover> {({ open }) => ( <> <Popover.Button>Solutions</Popover.Button>
<Popover.Overlay className="fixed inset-0 bg-black opacity-30" />
<Popover.Panel>{/* ... */}</Popover.Panel> </> )} </Popover> ) }

在这个示例中,我们在 DOM 中将 Popover.Overlay 放置在 Panel 之前,这样它就不会覆盖面板的内容。

但与所有其他组件一样,Popover.Overlay 是完全无状态的,因此如何对其进行样式化由你决定。

要为弹出窗口面板的打开/关闭添加动画,请使用提供的 Transition 组件。你只需将 Popover.Panel 包裹在 <Transition> 中,过渡就会自动应用。

import { Popover, Transition } from '@headlessui/react' function MyPopover() { return ( <Popover> <Popover.Button>Solutions</Popover.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Popover.Panel>{/* ... */}</Popover.Panel>
</Transition>
</Popover> ) }

默认情况下,我们内置的 Transition 组件会自动与 Popover 组件通信,以处理打开/关闭状态。但是,如果你需要更多地控制此行为,你可以显式地控制它

import { Popover, Transition } from '@headlessui/react' function MyPopover() { return ( <Popover>
{({ open }) => (
<>
<Popover.Button>Solutions</Popover.Button> {/* Use the `Transition` component. */} <Transition
show={open}
enter="transition duration-100 ease-out" enterFrom="transform scale-95 opacity-0" enterTo="transform scale-100 opacity-100" leave="transition duration-75 ease-out" leaveFrom="transform scale-100 opacity-100" leaveTo="transform scale-95 opacity-0" >
{/* Mark this component as `static` */}
<Popover.Panel static>{/* ... */}</Popover.Panel>
</Transition> </> )}
</Popover>
)
}

由于它们是无状态的,因此无状态 UI 组件也可以很好地与 React 生态系统中的其他动画库(如Framer MotionReact Spring)组合。

在渲染多个相关的弹出窗口时(例如,在站点的页眉导航中),请使用 Popover.Group 组件。这将确保在用户在组内的弹出窗口之间进行制表时面板保持打开状态,但在用户制表到组外部时会关闭任何打开的面板

import { Popover } from '@headlessui/react' function MyPopover() { return (
<Popover.Group>
<Popover> <Popover.Button>Product</Popover.Button> <Popover.Panel>{/* ... */}</Popover.Panel> </Popover> <Popover> <Popover.Button>Solutions</Popover.Button> <Popover.Panel>{/* ... */}</Popover.Panel> </Popover>
</Popover.Group>
) }

Popover 及其子组件都渲染一个适合该组件的默认元素:PopoverOverlayPanelGroup 组件都渲染一个 <div>Button 组件渲染一个 <button>

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

import { forwardRef } from 'react' import { Popover } from '@headlessui/react'
let MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />
}) function MyPopover() {
return (
<Popover as="nav">
<Popover.Button as={MyCustomButton}> Solutions </Popover.Button>
<Popover.Panel as="form"> {/* ... */} </Popover.Panel> </Popover> ) }

在打开的面板上按 Tab 键将在面板内容中的第一个可聚焦元素上设置焦点。如果正在使用 Popover.Group,则 Tab 键会在打开面板内容的末尾和下一个弹出窗口的按钮之间循环。

点击 Popover.Button 将切换面板的打开和关闭状态。点击打开面板外部的任何位置将关闭该面板。

命令描述

EnterSpacePopover.Button 获得焦点时。

切换面板

Esc

关闭任何打开的弹出窗口

Tab

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

从打开的面板中跳出将关闭该面板,并且从一个打开的面板跳到兄弟弹出窗口的按钮(在 Popover.Group 中)将关闭第一个面板

Shift + Tab

在焦点顺序中向后循环

支持嵌套弹出窗口,并且所有面板都将在根面板关闭时正确关闭。

所有相关的 ARIA 属性都会自动管理。

以下是弹出窗口与其他类似组件的比较

  • <Menu />。Popover 比菜单用途更广泛。菜单仅支持非常有限的内容,并且具有特定的可访问性语义。箭头键也用于导航菜单的项目。菜单最适合类似于大多数操作系统标题栏中找到的菜单的 UI 元素。如果您的浮动面板包含图像或比简单链接更多的标记,请使用 Popover。

  • <Disclosure />。Disclosures 适用于通常重新排列文档的内容,例如手风琴。Popover 还具有 Disclosure 之上的额外行为:它们呈现覆盖层,并且在用户单击覆盖层(通过单击 Popover 内容之外)或按下 Escape 键时关闭。如果您的 UI 元素需要此行为,请使用 Popover 而不是 Disclosure。

  • <Dialog />。Dialogs 旨在吸引用户的全部注意力。它们通常在屏幕中央呈现浮动面板,并使用背景来使应用程序内容的其余部分变暗。它们还会捕获焦点,并阻止用户将标签键移出 Dialog 内容,直到 Dialog 被关闭。Popover 更具上下文性,通常位于触发它们的元素附近。

主要的 Popover 组件。

属性默认值描述
asdiv
String | Component

Popover 应该呈现为的元素或组件。

渲染属性描述
open

Boolean

Popover 是否打开。

close

(ref?: ref | HTMLElement) => void

关闭 popover 并重新聚焦 Popover.Button。可以选择传入 refHTMLElement 来代替聚焦该元素。

这可用于为您的 Popover 组件创建覆盖层。单击覆盖层将关闭 Popover。

属性默认值描述
asdiv
String | Component

Popover.Overlay 应该呈现为的元素或组件。

渲染属性描述
open

Boolean

Popover 是否打开。

这是用于切换 Popover 的触发组件。您也可以将此 Popover.Button 组件用在 Popover.Panel 中,如果您这样做,它将作为 close 按钮。我们还将确保为按钮提供正确的 aria-* 属性。

属性默认值描述
asbutton
String | Component

Popover.Button 应该呈现为的元素或组件。

渲染属性描述
open

Boolean

Popover 是否打开。

此组件包含 Popover 的内容。

属性默认值描述
asdiv
String | Component

Popover.Panel 应该呈现为的元素或组件。

focusfalse
Boolean

这将在 Popover 打开时强制在 Popover.Panel 内部聚焦。如果焦点离开此组件,它也会关闭 Popover

staticfalse
Boolean

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

注意:staticunmount 不能同时使用。如果您尝试这样做,您将收到 TypeScript 错误。

unmounttrue
Boolean

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

注意:staticunmount 不能同时使用。如果您尝试这样做,您将收到 TypeScript 错误。

渲染属性描述
open

Boolean

Popover 是否打开。

close

(ref?: ref | HTMLElement) => void

关闭 popover 并重新聚焦 Popover.Button。可以选择传入 refHTMLElement 来代替聚焦该元素。

通过将相关联的兄弟 popover 包裹在 Popover.Group 中来链接它们。从一个 Popover.Panel 中跳出将聚焦下一个 popover 的 Popover.Button,而跳出 Popover.Group 将完全关闭组中的所有 popover。

属性默认值描述
asdiv
String | Component

Popover.Group 应该呈现为的元素或组件。

如果您有兴趣使用 Headless UI 和 Tailwind CSS 的预先设计组件示例,请查看 Tailwind UI — 我们精心打造的精美设计组件集。

这是支持我们对像这样的开源项目的工作的好方法,并使我们能够改进它们并保持良好的维护。