bgm: https://music.163.com/#/song?id=490182455&userid=361029804
劇場版《NO GAME NO LIFE 遊戲人生 ZERO》主題曲
前言及介紹#
在當今的前端開發中,浮動元素扮演著越來越重要的角色。它們能夠為用戶提供額外的互動和信息,同時不會影響頁面的整體佈局。而 Floating UI 就是一個為了方便定位和創建浮動元素的 JavaScript 庫。通過它,你可以輕鬆地 控制浮動元素的位置和互動效果,從而提升用戶體驗。
如果你正在尋找一個簡單易用的浮動元素解決方案,或許 Floating UI 不是你的最佳選擇,該庫的主要目標是提供錨點定位的功能,而不是提供預建樣式或其他高級互動效果。但如果你是熟練掌握 React 並希望使用這樣高度自定義的庫,你就可以更好地使用它。
這個庫是有意 “低級” 的,它的唯一目標是提供 “錨點定位”。把它想像成一個缺失的 CSS 特性的 polyfill。不提供預建樣式,用戶互動僅適用於 React 用戶。
如果您正在尋找開箱即用的簡單功能,您可能會發現其他庫更適合您的用例。
寫這篇文章我更願意稱之為在 React 中使用 Floating UI 的經驗分享,特別是在 React 中使用該庫的方法,而不是教程,因為實際運用的時候發現 Floating UI 的文檔和示例已經相當詳盡,但苦於 Floating UI 的中文資料寥寥無幾,所以自己沉澱一些方便日後回憶,也希望能為需要的人提供一些幫助和參考。(當然,最快的還是直接去看英文文檔和例子,搜索 API 的使用)
安裝#
你可以通過包管理器或 CDN 來安裝 Floating UI。如果你使用 npm、yarn 或 pnpm,你可以運行以下命令:
npm install @floating-ui/dom
如果你使用 CDN,你可以在你的 HTML 文件中添加以下標籤:
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/dom"></script>
更多請看:Getting Started | Floating UI
React 中安裝#
在 React 中安裝只需要安裝 @floating-ui/react 這個包即可
yarn add @floating-ui/react
Popover#
在本文中,我將分享如何使用 Floating UI 來創建一種常見的浮動 UI 組件 ——Popover(彈出框)。Popover 是一種常見的浮動 UI 組件,它通常在用戶懸停或點擊某個元素時顯示,以提供額外的信息或選項。
案例演示#
通過以下學習,可以輕易構建一個點擊彈出的氣泡框 / 彈出層,如圖。
Demo 演示 👉 CodeSandbox
useFloating#
首先就是核心 hook —— useFloating
useFloating
hook 為浮動元素提供定位和上下文。我們需要傳遞一些信息:
open
:彈窗的打開狀態。onOpenChange
: 彈窗打開或關閉時將調用的回調函數。floating-ui 內部將使用它來更新它們的isOpen
狀態。placement
:浮動元素相對參考元素的位置,默認位置是'bottom'
,但您可能希望將工具提示放置在與按鈕相關的任何位置。為此,Floating UI 具有placement
选项。- 可用的基本位置是
'top'
、'right'
、'bottom'
、'left'
。 - 這些基本位置中的每一個都以
-start
和-end
的形式對齊。例如,'right-start'
或'bottom-end'
。這些允許您將工具提示與按鈕的邊緣對齊,而不是將其居中。
- 可用的基本位置是
middleware
:將中間件導入並傳遞到數組,以確保彈窗保留在屏幕上,無論它最終被放置在哪裡。autoPlacement
當您不知道哪個位置最適合浮動元素,或者不想明確指定它時,這個中間件很有用。- Middleware | Floating UI 其他中間件,可以看文檔,包括 offset (設置偏移) 、arrow (添加小箭頭) 、shift(沿著指定的軸移動浮動元素以使其保持可見)、flip(翻轉浮動元素的位置以使其保持可見)、inline (改進跨多行的內聯引用元素的定位) 等有用的中間件
whileElementsMounted
:只有在參考元素和浮動元素都掛載好的情況下,才會在必要時更新位置,以確保浮動元素保持錨定在參考元素上。- autoUpdate 如果用戶滾動或調整屏幕大小,浮動元素可能會與參考元素分離,因此需要再次更新其位置以確保其保持錨定狀態。
import { useFloating, autoUpdate, offset, flip, shift } from '@floating-ui/react';
function Popover() {
const [isOpen, setIsOpen] = useState(false);
const { x, y, strategy, refs, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [offset(10), flip(), shift()],
placement: 'top',
whileElementsMounted: autoUpdate,
});
}
Interaction hooks - useInteractions#
使用
useInteractions
傳入一個配置對象,可以使浮動元素能夠拓展打開、關閉行為或被屏幕閱讀器訪問等額外功能。在這個例子中,useClick()
添加了在單擊引用元素時切換彈出窗口打開或關閉的功能。useDismiss()
添加了當用戶按下esc
鍵或在彈出框外按下時關閉彈出框的功能。useRole()
將dialog
的正確ARIA
屬性添加到彈出框和引用元素。最後,useInteractions()
將他們所有的 props 合併到 prop getters 中,可以用於渲染。[^1]
一些配置的對象。使浮動元素能夠拓展打開、關閉行為或被屏幕閱讀器訪問等額外功能。
import {
// ...
useClick,
useDismiss,
useRole,
useInteractions,
} from '@floating-ui/react';
function Popover() {
const [isOpen, setIsOpen] = useState(false);
const { x, y, reference, floating, strategy, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [offset(10), flip(), shift()],
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const role = useRole(context);
// Merge all the interactions into prop getters
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
}
useClick()
添加了在單擊引用元素時切換彈出窗口打開或關閉的功能。useDismiss()
添加了當用戶按下esc
鍵或在彈出框外按下時關閉彈出框的功能。useRole()
將dialog
的正確ARIA
屬性添加到彈出框和引用元素。
最後,useInteractions()
將他們所有的 props 合併到 prop getters 中,可以用於渲染。將他們所有的 props 合併到可用於渲染的 prop getters 。
Rendering#
現在我們已經設置了所有的變量和 hook,我們可以渲染我們的元素了。
function Popover() {
// ...
return (
<>
<button ref={refs.setReference} {...getReferenceProps()}>
引用元素
</button>
{isOpen && (
<FloatingFocusManager context={context} modal={false}>
<div
ref={refs.setFloating}
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: 'max-content',
}}
{...getFloatingProps()}
>
Popover 元素
</div>
</FloatingFocusManager>
)}
</>
);
}
getReferenceProps
&getFloatingProps
由useInteractions
返回傳播到相關元素上。它們包含諸如onClick
、aria-expanded
等相關的 props。<FloatingFocusManager />
是 管理模態或非模態行為( modal and non-modal ) 的 彈出框焦點 的組件。它應該直接包裹浮動元素,並且 只在 popover 也被渲染時才被渲染。FloatingFocusManager
——FloatingFocusManager
docs.
Modal and non-modal behavior 模態或非模態行為#
Modal and non-modal behavior 模態或非模態行為
在上面的例子中我們使用了非模態的焦點管理,但是彈出框的焦點管理行為可以是模態的也可以是非模態的。它們的區別如下:
Modal 模態#
- 彈出窗口及其內容是 唯一可以接收焦點 的元素。當彈出窗口打開時,用戶無法與頁面的其餘部分(屏幕閱讀器也不能)互動,直到彈出窗口關閉。
- 需要一個 明確的關閉按鈕(儘管它可以在視覺上隱藏)。
此行為是默認行為:
<FloatingFocusManager context={context}>
<div />
</FloatingFocusManager>
Non-modal 非模態行為#
- 彈出窗口及其內容可以獲得焦點,但用戶仍然可以與頁面的其餘部分進行互動。
- 當在其外部進行 Tab 鍵時,彈出窗口會在失去焦點時自動關閉,並且自然 DOM 順序中的下個可聚焦元素獲得焦點。
- 不需要明確的關閉按鈕。
此行為可以使用 modal
prop 進行配置,如下所示:
<FloatingFocusManager context={context} modal={false}>
<div />
</FloatingFocusManager>
完整代碼#
經過億點點優化,就能簡簡單單造出這麼一個 Popover 組件啦~
import {
FloatingFocusManager,
Placement,
autoUpdate,
useFloating,
useInteractions,
shift,
offset,
flip,
useClick,
useRole,
useDismiss,
} from '@floating-ui/react';
import { cloneElement, useEffect, useId, useState } from 'react';
type PopoverProps = {
disabled?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
render: (props: { close: () => void }) => React.ReactNode;
placement?: Placement;
children: JSX.Element;
className?: string;
};
const Popover = ({ disabled, children, render, placement, open: passedOpen, onOpenChange }: PopoverProps) => {
const [open, setOpen] = useState(false);
const { x, y, reference, floating, strategy, context } = useFloating({
open,
onOpenChange: (op) => {
if (disabled) return;
setOpen(op);
onOpenChange?.(op);
},
middleware: [offset(10), flip(), shift()],
placement,
whileElementsMounted: autoUpdate,
});
const { getReferenceProps, getFloatingProps } = useInteractions([useClick(context), useRole(context), useDismiss(context)]);
const headingId = useId();
useEffect(() => {
if (passedOpen === undefined) return;
setOpen(passedOpen);
}, [passedOpen]);
return (
<>
{cloneElement(children, getReferenceProps({ ref: reference, ...children.props }))}
{open && (
<FloatingFocusManager context={context} modal={false}>
<div
ref={floating}
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
}}
className="z-10 bg-yellow-400 p-2 outline-none"
aria-labelledby={headingId}
{...getFloatingProps()}
>
{render({
close: () => {
setOpen(false);
onOpenChange?.(false);
},
})}
</div>
</FloatingFocusManager>
)}
</>
);
};
export default Popover;
結語#
下一次將介紹Dialog | Floating UI 的創建及封裝,包括 FloatingPortal 和 FloatingOverlay 的介紹,它與彈出框有類似的互動,但有兩個主要區別:
- 它是模態的,並在對話框後面呈現一個背景,使後面的內容變暗,使頁面的其餘部分無法訪問。
- 它在視口中居中,不錨定到任何特定的參考元素。
非常推薦大家去閱讀 Floating UI 官方文檔,它的思想我非常喜歡,無論是中間件還是 hook 的抽象程度。