彈出式視窗

彈出式視窗非常適合用於包含任意內容的浮動面板,例如導覽選單、行動裝置選單和飛出式選單。

首先,透過 npm 安裝 Headless UI

npm install @headlessui/react

彈出式視窗使用 PopoverPopover.ButtonPopover.Panel 元件建置。

按一下 Popover.Button 將自動開啟/關閉 Popover.Panel。開啟面板時,按一下其內容以外的任何位置、按一下 ESC 鍵或從面板中以 Tab 鍵切換離開,即可關閉彈出式視窗。

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.Panel 上使用絕對定位,將其定位在 Popover.Button 附近,並不會影響一般的文件流程。

Headless UI 會追蹤各個元件的許多狀態,例如目前選取哪個清單框選項、彈出式視窗是否開啟或關閉,或彈出式視窗中的哪個項目目前已透過鍵盤啟用。

但是,由於元件出廠設定並未套用樣式,因此在您自己提供每種狀態所需的樣式之前,您無法在 UI 中看見這些資訊。

每個元件都會透過 渲染屬性公開其目前狀態的資訊,您可以使用這些屬性有條件地套用不同樣式或渲染不同內容。

例如,Popover 元件顯示 open 狀態,在這個狀態中會告訴您 popover 目前是否開啟。

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> ) }

如需每項元件的完整渲染 prop API,請參考 元件 API 文件

每個元件也會透過 data-headlessui-state 屬性公開其目前狀態的相關資訊,您可以使用該屬性有條件地套用不同樣式。

渲染 prop API 中的任何狀態為 true 時,這些狀態會在此屬性中列為以空白分隔的字串,這樣您就可以使用 [attr~=value] 形式的 CSS 屬性選擇器 將其設為目標。

例如,以下是 popover 開啟時 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 勾子將我們的 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 prop 傳遞給 Popover.Panel 以指示它始終渲染,然後使用 open 渲染 prop 來控制面板的顯示/隱藏時間。

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> ) }

預設情況下,呼叫 close() 後,Popover.Button 會接收焦點,但你可以透過將參照傳入 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> ) }

在此範例中,我們將 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 組件。這能確保當使用者在群組內的不同浮動視窗之間以 Tab 鍵移動時,浮動視窗會保持開啟,但當使用者在群組外以 Tab 鍵移動時,所有開啟的浮動視窗都會關閉

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 屬性,將組件顯示為不同的元素或您自己的自訂組件,確定您的自訂組件 forward refs,以確保 Headless 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,就能夠開啟或關閉面板。按一下開啟面板之外的任意位置,將會關閉面板。

指令說明

EnterSpace在焦點在 Popover.Button 上時。

切換面板

Esc

關閉所有開啟中的對話框

Tab

循環於開啟的面板內容之間

從開啟的面板按下 Tab 鍵離開,將會關閉該面板,從一個開啟的面板按下 Tab 鍵,移動至同層對話框的按鈕(在 Popover.Group 內),會關閉第一個面板。

Shift + Tab

向後循環焦點順序

支援嵌套對話框,且每當關閉根目錄面板時,所有面板都會正確關閉。

所有相關的 ARIA 屬性都會自動管理。

以下為對話框與其他類似組件的比較

  • <Menu />。Popovers 的用途比功能單更廣泛。功能單僅支援非常受限的內容並具有特定無障礙語意。箭頭鍵也能在功能單的項目中移動。功能單最適合類似於大多數作業系統標題列中功能單的 UI 元件。如果您的浮動面板具有圖像或比單純連結更多的標記,請使用 Popover。

  • <Disclosure />。揭露對於通常會重新整理文件的項目很有用,例如手風琴。Popovers 也具有揭露以外的行為:它們會呈現疊加,並且當使用者按一下疊加(按一下 Popover 內容以外的地方)或按一下跳脫鍵時關閉。如果您的 UI 元件需要這種行為,請使用 Popover 而非揭露。

  • <Dialog />。對話框旨在吸引使用者的全部注意力。它們通常會在螢幕中央呈現一個浮動面板,並且透過幕景將應用程式其他內容變暗。它們也會擷取焦點,並防止從對話框內容中跳離,直到對話框被關閉為止。Popovers 的語境性更強,並且通常會定位在觸發它們的元件附近。

主要的 Popover 元件。

屬性預設說明
asdiv
字串 | 元件

Popover 應呈現為的元素或元件。

Render 屬性說明
open

布林

popover 是否開啟。

close

(ref?: 參照 | HTMLElement) => void

關閉 popover 並將焦點重新設定為 Popover.Button。選擇性地傳遞 參照HTMLElement,以將焦點設定到那個元素。

可以透過此項建立您 Popover 元件的疊加。按一下疊加會關閉 Popover。

屬性預設說明
asdiv
字串 | 元件

Popover.Overlay 應呈現為的元素或元件。

Render 屬性說明
open

布林

popover 是否開啟。

這是切換 Popover 的觸發元件。您也可以在 Popover.Panel 內使用這個 Popover.Button 元件,如果您這樣做,它會表現得像 close 按鈕。我們還會確定在按鈕中提供正確的 aria-* 屬性。

屬性預設說明
asbutton
字串 | 元件

Popover.Button 應呈現為的元素或元件。

Render 屬性說明
open

布林

popover 是否開啟。

此元件包含你的 Popover 的內容。

屬性預設說明
asdiv
字串 | 元件

Popover.Panel 應作為之元素或元件。

焦點false
布林

Popover 開啟時,這將強制聚焦在 Popover.Panel 內部。如果焦點離開此元件,它也會關閉 Popover

靜態false
布林

這個元素是否應忽略內部管理的開啟/關閉狀態。

注意:靜態卸載 不可同時使用。如果你嘗試這樣做,會出現 TypeScript 錯誤。

卸載true
布林

這個元素是否應該根據開啟/關閉狀態進行卸載或隱藏。

注意:靜態卸載 不可同時使用。如果你嘗試這樣做,會出現 TypeScript 錯誤。

Render 屬性說明
open

布林

popover 是否開啟。

close

(ref?: 參照 | HTMLElement) => void

關閉 popover 並將焦點重新設定為 Popover.Button。選擇性地傳遞 參照HTMLElement,以將焦點設定到那個元素。

透過將相關兄弟元素的 popover 包在 Popover.Group 內進行連結。從一個 Popover.Panel 中按 Tab 離開會聚焦下一個 popover 的 Popover.Button,而從 Popover.Group 外按 Tab 離開則會關閉群組中的所有 popover。

屬性預設說明
asdiv
字串 | 元件

Popover.Group 應作為之元素或元件。

如果你有興趣使用 Headless UI 和 Tailwind CSS 的預先設計元件範例,可以查看 Tailwind UI — 我們所建構的美麗設計與精湛工藝元件合輯。

這是支援我們開發此類開源專案的絕佳方式,也能讓我們改良並妥善維護這些專案。