# 前言及介绍

在当今的前端开发中,浮动元素扮演着越来越重要的角色。它们能够为用户提供额外的交互和信息,同时不会影响页面的整体布局。而 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()}>
        Reference element
      </button>
      {isOpen && (
        <FloatingFocusManager context={context} modal={false}>
          <div
            ref={refs.setFloating}
            style=<!--swig0-->
            {...getFloatingProps()}
          >
            Popover element
          </div>
        </FloatingFocusManager>
      )}
    </>
  );
}
  • getReferenceProps & getFloatingPropsuseInteractions 返回传播到相关元素上。它们包含诸如   onClick  、  aria-expanded   等相关的 props。
  • <FloatingFocusManager />   是 管理模态或非模态行为( modal and non-modal )弹出框焦点 的组件。它应该直接包裹浮动元素,并且 只在 popover 也被渲染时才被渲染。  FloatingFocusManager   —— FloatingFocusManager  docs.

Modal and non-modal behavior 模态或非模态行为

在上面的例子中我们使用了非模态的焦点管理,但是弹出框的焦点管理行为可以是模态的也可以是非模态的。它们的区别如下:

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=<!--swig1-->
            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 的抽象程度

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

cos 微信支付

微信支付

cos 支付宝

支付宝