上文:Floating UI 使用經驗分享 - Popover
在本文中,我將分享如何使用 Floating UI 來創建另一種常見的浮動 UI 元件 ——Dialog(對話框)。Dialog 是一個浮動元素,顯示需要立即關注的信息,它會出現在頁面內容上並阻止與頁面的互動,直到它被關閉。
它與彈出框有類似的互動,但有兩個主要區別:
- 它是模態的,並在對話框後面呈現一個背景,使後面的內容變暗,使頁面的其餘部分無法訪問。
- 它在視口中居中,不錨定到任何特定的參考元素。
一個可訪問的對話框元件具有以下要點:
Dismissal
:當用戶按下esc
鍵或在打開的對話框外按下時,它會關閉。Role
:元素被賦予相關的角色和 ARIA 屬性,以便螢幕閱讀器可以訪問。Focus management
: 焦點完全被困在對話框中,必須由用戶解除。
目標元件#
目標:實現一個這樣的 Dialog Demo 👇
接下來我們需要創建一個名為 Dialog
的 React 元件,它使用了 @floating-ui/react
庫來創建一個可互動的浮動對話框。以下是對該元件的設想:
元件參數#
Dialog
元件需要接受以下參數:
rootId
:浮動元素的根元素,可選。open
:控制對話框是否打開的布林值。initialOpen
:對話框初始是否打開的布林值,默認為false
。onOpenChange
:當對話框打開狀態改變時的回調函數,接受一個布林值參數。render
:一個函數,接受一個物件參數,該物件包含一個close
方法,用於關閉對話框。該函數返回要在對話框中渲染的 React 節點。className
:應用於對話框的 CSS 類名。overlayClass
:應用於浮動覆蓋層的 CSS 類名。containerClass
:應用於對話框容器的 CSS 類名。isDismiss
:一個布林值,決定是否啟用點擊外部區域關閉對話框的功能,默認為true
。children
:React 子元素,可以是一個按鈕,點擊後打開該彈窗。showCloseButton
:一個布林值,決定是否顯示關閉按鈕,默認為true
。
元件功能#
Dialog
元件的主要功能是創建一個可互動的浮動對話框,它可以通過點擊關閉按鈕或點擊對話框外部區域來關閉。對話框的打開和關閉狀態可以通過 open
和 onOpenChange
參數進行控制(受控),也可以通過內部狀態進行自動管理(非受控)。
Dialog
元件使用了 @floating-ui/react
庫的多個 Hook:
useFloating
:用於管理對話框的打開和關閉狀態。useClick
、useRole
和useDismiss
:用於處理對話框的互動,如點擊和角色管理。useInteractions
:用於獲取和設置互動屬性。
此外,Dialog
元件還使用了 FloatingPortal
、FloatingOverlay
和 FloatingFocusManager
元件來創建浮動對話框的 UI。
完整代碼#
結合實際可以寫出這樣一個功能較為完整的 Dialog 案例,可以自定義遮罩層、內部元素的樣式,也可以控制點擊遮罩層是否關閉彈窗等,還可以結合 Framer-motion 製作彈窗動畫等(以後有機會也寫一篇)
import {
FloatingFocusManager,
FloatingOverlay,
FloatingPortal,
useClick,
useDismiss,
useFloating,
useInteractions,
useRole,
} from '@floating-ui/react';
import clsx from 'clsx';
import React, { cloneElement, useState } from 'react';
import { CgClose } from 'react-icons6GH))S[9A2G57O0%MM45V.gif)';
type DialogProps = {
rootId?: string;
open?: boolean;
initialOpen?: boolean;
onOpenChange?: (open: boolean) => void;
children?: JSX.Element;
render: (props: { close: () => void }) => React.ReactNode;
className?: string;
overlayClass?: string;
containerClass?: string;
isDismiss?: boolean;
showCloseButton?: boolean;
};
export default function Dialog({
initialOpen = false,
open: controlledOpen,
onOpenChange: setControlledOpen,
children,
className,
render,
rootId: customRootId,
overlayClass,
containerClass,
showCloseButton = true,
isDismiss = true,
}: DialogProps) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
const { reference, floating, context } = useFloating({
open,
onOpenChange: setOpen,
});
const click = useClick(context);
const role = useRole(context);
const dismiss = useDismiss(context, { enabled: isDismiss, outsidePressEvent: 'mousedown' });
const { getReferenceProps, getFloatingProps } = useInteractions([click, role, dismiss]);
const onClose = () => setOpen(false);
return (
<>
{children && cloneElement(children, getReferenceProps({ ref: reference, ...children.props }))}
<FloatingPortal id={customRootId}>
{open && (
<FloatingOverlay
className={clsx('absolute inset-0 z-10 flex h-full w-full items-center', overlayClass ?? 'bg-black/60')}
lockScroll
>
<div className={clsx('m-auto grid place-items-center', containerClass)}>
<FloatingFocusManager context={context}>
<div
className={clsx('relative overflow-hidden rounded-md bg-white', className ?? 'mx-24')}
ref={floating}
{...getFloatingProps()}
>
{showCloseButton && <CgClose className="absolute right-2 top-2 h-6 w-6 cursor-pointer" onClick={onClose} />}
{render({ close: onClose })}
</div>
</FloatingFocusManager>
</div>
</FloatingOverlay>
)}
</FloatingPortal>
</>
);
}
基本 Dialog Hooks#
官方示例 👉 CodeSandbox demo
此示例演示如何創建用於單個實例的對話框以熟悉基礎知識。
讓我們看一下這個例子:
打開狀態#
import {useState} from 'react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
}
isOpen
確定對話框當前是否在螢幕上打開。它用於條件渲染。
useFloating hook#
useFloating()
hook 為我們的對話提供上下文。我們需要傳遞一些信息:
open
:來自我們上面的useState()
挂钩的打開狀態。onOpenChange
: 對話框打開或關閉時調用的回調函數。我們將使用它來更新我們的isOpen
狀態。
import {useFloating} from '@floating-ui/react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
const {refs, context} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
});
}
互動 hooks#
useClick()
添加了在單擊引用元素時打開或關閉對話框的功能。但是,對話框可能並不附加到引用元素,因此這是可選的。(一般對話框都是獨立出來 Portal 的,也就是上下文是 body)useDismiss()
添加了當用戶按下esc
鍵或在對話框外按下時關閉對話框的功能。可以將其的outsidePressEvent
選項設置為'mousedown'
以便觸摸事件變得懶惰並且不會穿過背景,因為默認行為是急切的。(不太好理解,大概是)useRole()
將dialog
的正確 ARIA 屬性添加到對話框和引用元素。
最後, useInteractions()
將他們所有的 props 合併到 prop getters 中,然後就可以用於渲染。
import {
// ...
useClick,
useDismiss,
useRole,
useInteractions,
useId,
} from '@floating-ui/react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
const {refs, context} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
});
const click = useClick(context);
const dismiss = useDismiss(context, {
outsidePressEvent: 'mousedown',
});
const role = useRole(context);
// Merge all the interactions into prop getters
const {getReferenceProps, getFloatingProps} = useInteractions([
click,
dismiss,
role,
]);
// Set up label and description ids
const labelId = useId();
const descriptionId = useId();
}
渲染#
現在我們已經設置了所有的變數和 hook,可以渲染我們的元素了。
function Dialog() {
// ...
return (
<>
<button ref={refs.setReference} {...getReferenceProps()}>
Reference element
</button>
{isOpen && (
<FloatingOverlay
lockScroll
style={{background: 'rgba(0, 0, 0, 0.8)'}}
>
<FloatingFocusManager context={context}>
<div
ref={refs.setFloating}
aria-labelledby={labelId}
aria-describedby={descriptionId}
{...getFloatingProps()}
>
<h2 id={labelId}>Heading element</h2>
<p id={descriptionId}>Description element</p>
<button onClick={() => setIsOpen(false)}>
Close
</button>
</div>
</FloatingFocusManager>
</FloatingOverlay>
)}
</>
);
}
{...getReferenceProps()}
/{...getFloatingProps()}
上一篇說過,將 props 從互動掛鉤傳播到相關元素上。它們包含諸如onClick
、aria-expanded
等道具。
FloatingPortal & FloatingOverlay & FloatingFocusManager#
<FloatingOverlay />
是一個在浮動元素後面渲染背景覆蓋元素的元件,具有鎖定主體滾動的能力。 FloatingOverlay docs- 提供了一個固定的基本樣式,使背景內容變暗並阻止浮動元素後面的指針事件。
- 它是一個常規的
<div/>
,因此可以通過任何 CSS 解決方案進行樣式設置。
<FloatingFocusManager />
- FloatingPortal docs 一個管理和控制頁面中浮動元素焦點的元件
- 自動檢測焦點變化,調整頁面上的浮動元素的位置和狀態,確保頁面上所有元素的可訪問性和可用性。
- 默認情況通常將焦點捕獲在內部。
- 它應該直接包裹浮動元素,並且只在對話框也被渲染時才被渲染。 FloatingFocusManager docs
<FloatingPortal />
將浮動元素傳送到給定的容器元素中 —— 默認情況下,在應用程序根之外並進入 body。- 可以自定義 root,也就是可選擇具有 id 的節點,或者創建它並將其附加到指定的根(body)
import {FloatingPortal} from '@floating-ui/react';
function Tooltip() {
return (
isOpen && (
<FloatingPortal>
<div>Floating element</div>
</FloatingPortal>
)
);
}