Popover
Popover 非常适合用于浮动面板,包含任意内容,例如导航菜单、移动菜单和弹出菜单。
要开始使用,请通过 npm 安装无状态 UI
npm install @headlessui/react
Popover 使用 Popover
、PopoverButton
和 PopoverPanel
组件构建。
点击 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>
)
}
使用 top
、right
、bottom
或 left
值将面板沿相应的边线居中,或将其与 start
或 end
结合使用,以将面板对齐到特定角落,例如 top start
或 bottom 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 控制 gap
、offset
和 padding
值。
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 Motion 和 React 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>
)
}
Popover
和 PopoverPanel
还公开了一个 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
及其子组件都会渲染一个对该组件有意义的默认元素。
Popover
、PopoverBackdrop
、PopoverPanel
和 PopoverGroup
组件都会渲染一个 <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 或者 Space当 | 切换面板 |
Esc | 关闭所有打开的弹窗 |
Tab | 在打开的面板内容之间循环 从打开的面板中按下 Tab 键将关闭该面板,从一个打开的面板的按钮到另一个兄弟弹窗的按钮(在 |
Shift + Tab | 反向循环焦点顺序 |
属性 | 默认值 | 描述 |
as | div | String | Component 弹窗应该渲染的元素或组件。popover应该渲染为。 |
数据属性 | 渲染道具 | 描述 |
data-open | open |
弹窗是否打开。popover是打开的。 |
— | close |
关闭弹窗并将焦点重新设置到 |
属性 | 默认值 | 描述 |
as | div | String | Component 弹窗应该渲染的元素或组件。弹窗背景应该渲染为。 |
transition | false | Boolean 元素是否应该渲染过渡属性,例如 |
数据属性 | 渲染道具 | 描述 |
data-open | open |
弹窗是否打开。popover是打开的。 |
属性 | 默认值 | 描述 |
as | button | String | Component 弹窗应该渲染的元素或组件。弹窗按钮应该渲染为。 |
disabled | false | Boolean 弹窗是否打开。弹窗按钮是否禁用. |
autoFocus | false | Boolean 弹窗是否打开。弹窗按钮是否在首次渲染时获得焦点。 |
数据属性 | 渲染道具 | 描述 |
data-open | open |
弹窗是否打开。popover是打开的。 |
data-focus | focus |
弹窗是否打开。弹窗按钮是否获得焦点。 |
data-hover | hover |
弹窗是否打开。弹窗按钮是否悬停。 |
data-active | active |
弹窗是否打开。弹窗按钮是否处于活动或按下状态。 |
data-autofocus | autofocus |
|
属性 | 默认值 | 描述 |
as | div | String | Component 弹窗应该渲染的元素或组件。弹窗面板应该渲染为。 |
transition | false | Boolean 元素是否应该渲染过渡属性,例如 |
anchor | — | Object 配置面板如何锚定到按钮。 |
anchor.to | bottom | String 相对于触发器的位置。弹窗面板相对于触发器。 使用 |
anchor.gap | 0 | Number | String 面板和触发器之间的间距。弹窗按钮和弹窗面板. 还可以使用 |
anchor.offset | 0 | Number | String 面板应该从其原始位置移动的距离。弹窗面板应该从其原始位置移动的距离。 还可以使用 |
anchor.padding | 0 | Number | String 面板和视窗之间的最小间距。弹窗面板和 还可以使用 |
static | false | Boolean 元素是否应该忽略内部管理的打开/关闭状态。 |
unmount | true | Boolean 元素是否应该根据打开/关闭状态卸载或隐藏。 |
portal | false | Boolean 元素是否应该渲染在门户网站中。 当 |
modal | false | Boolean 是否启用辅助功能,例如滚动锁定和焦点捕获。 |
focus | false | Boolean 当弹窗打开时,这将强制在 |
数据属性 | 渲染道具 | 描述 |
data-open | open |
弹窗是否打开。popover是打开的。 |
— | close |
关闭弹窗并将焦点重新设置到 |
通过将相关联的兄弟弹窗包装在 PopoverGroup
中来链接它们。从一个 PopoverPanel
中按下 Tab 键将使下一个弹窗的 PopoverButton
获得焦点,从 PopoverGroup
外部按下 Tab 键将完全关闭组内的所有弹窗。
属性 | 默认值 | 描述 |
as | div | String | Component 弹窗应该渲染的元素或组件。弹窗组应该渲染为。 |
属性 | 默认值 | 描述 |
as | button | String | Component 弹窗应该渲染的元素或组件。关闭按钮应该渲染为。 |
如果你有兴趣使用无头 UI 的预先设计的 Tailwind CSS 弹窗组件示例,请查看 Tailwind UI — 由我们精心设计和制作的精美组件集合。
这是支持我们对诸如这个之类的开源项目的贡献的一个好方法,并使我们能够改进它们并保持良好的维护。