banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Floating UI 使用經驗分享 - Dialog

上文:Floating UI 使用經驗分享 - Popover

在本文中,我將分享如何使用 Floating UI 來創建另一種常見的浮動 UI 元件 ——Dialog(對話框)。Dialog 是一個浮動元素,顯示需要立即關注的信息,它會出現在頁面內容上並阻止與頁面的互動,直到它被關閉。

它與彈出框有類似的互動,但有兩個主要區別:

  • 它是模態的,並在對話框後面呈現一個背景,使後面的內容變暗,使頁面的其餘部分無法訪問。
  • 它在視口中居中,不錨定到任何特定的參考元素。

一個可訪問的對話框元件具有以下要點:

  • Dismissal:當用戶按下 esc 鍵或在打開的對話框外按下時,它會關閉。
  • Role:元素被賦予相關的角色和 ARIA 屬性,以便螢幕閱讀器可以訪問。
  • Focus management: 焦點完全被困在對話框中,必須由用戶解除。

目標元件#

目標:實現一個這樣的 Dialog Demo 👇

Pasted image 20230616145507

接下來我們需要創建一個名為 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 元件的主要功能是創建一個可互動的浮動對話框,它可以通過點擊關閉按鈕或點擊對話框外部區域來關閉。對話框的打開和關閉狀態可以通過 openonOpenChange 參數進行控制(受控),也可以通過內部狀態進行自動管理(非受控)。

Dialog 元件使用了 @floating-ui/react 庫的多個 Hook:

  • useFloating:用於管理對話框的打開和關閉狀態。
  • useClickuseRoleuseDismiss:用於處理對話框的互動,如點擊和角色管理。
  • useInteractions:用於獲取和設置互動屬性。

此外,Dialog 元件還使用了 FloatingPortalFloatingOverlayFloatingFocusManager 元件來創建浮動對話框的 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-icons![](file:///C:\Users\34504\AppData\Roaming\Tencent\QQTempSys\3)6GH))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 [uncontrolledOpensetUncontrolledOpen= useState(initialOpen);  
  const open = controlledOpen ?? uncontrolledOpen;  
  const setOpen = setControlledOpen ?? setUncontrolledOpen;  
  
  const { referencefloatingcontext } = useFloating({  
    open,  
    onOpenChange: setOpen,  
  });  
  
  const click = useClick(context);  
  const role = useRole(context);  
  const dismiss = useDismiss(context, { enabled: isDismiss, outsidePressEvent: 'mousedown' });  
  
  const { getReferencePropsgetFloatingProps } = 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

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#

Interaction 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();
}

渲染#

Rendering

現在我們已經設置了所有的變數和 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 從互動掛鉤傳播到相關元素上。它們包含諸如 onClickaria-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>
    )
  );
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。