banner
cos

cos

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

Floating UI 使用經驗分享 - Popover

bgm: https://music.163.com/#/song?id=490182455&userid=361029804
劇場版《NO GAME NO LIFE 遊戲人生 ZERO》主題曲

THERE IS A REASON
THERE IS A REASON
鈴木這個

前言及介紹#

在當今的前端開發中,浮動元素扮演著越來越重要的角色。它們能夠為用戶提供額外的互動和信息,同時不會影響頁面的整體佈局。而 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

image.png

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#

Interaction hooks

使用 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#

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 & getFloatingPropsuseInteractions 返回傳播到相關元素上。它們包含諸如  onClick 、 aria-expanded  等相關的 props。
  • <FloatingFocusManager />  是 管理模態或非模態行為( modal and non-modal )彈出框焦點 的組件。它應該直接包裹浮動元素,並且 只在 popover 也被渲染時才被渲染。 FloatingFocusManager  —— FloatingFocusManager docs.

Modal and non-modal behavior 模態或非模態行為#

Modal and non-modal behavior 模態或非模態行為

在上面的例子中我們使用了非模態的焦點管理,但是彈出框的焦點管理行為可以是模態的也可以是非模態的。它們的區別如下:

Modal 模態#

Modal

  • 彈出窗口及其內容是 唯一可以接收焦點 的元素。當彈出窗口打開時,用戶無法與頁面的其餘部分(屏幕閱讀器也不能)互動,直到彈出窗口關閉。
  • 需要一個 明確的關閉按鈕(儘管它可以在視覺上隱藏)。

此行為是默認行為:

<FloatingFocusManager context={context}>
  <div />
</FloatingFocusManager>

Non-modal 非模態行為#

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 的抽象程度。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。