关于浮动元素,你还在自己计算位置吗?来看看 Floating UI 吧!

2,640 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 16 天,点击查看活动详情


什么是浮动元素?就是在页面布局之外浮动,而不会破坏页面布局的元素。
浮动元素在实际项目中有广泛的使用。比如工具提示、弹出框、下拉菜单等。
拿掘金举例。
比如这样:
image.png
这样:
image.png
还有这样:
image.png
它的原理并不复杂,通过相对定位和绝对定位计算 left 和 top 两个值就可以。
但是实际上我们很少去手动计算位置,因为太麻烦了。
在过去,最受大家欢迎的是 Popper 这个库,而且它是一个非常老牌的库,在 2006 年就开源了,几乎是这个领域内做的最大的。
但是现在有了一个更新、更全能的 Floating UI!
Floating UI 同样是 Popper 团队开发的,是在 Popper v2 版本之上的演变。
Floating UI 当然不仅仅是 Popper 的替代品,而是多了很多新的功能。

优势

跨平台兼容

浮动元素不只是 HTML 独有的需求,在移动端、Canvas、WebGL 等其他通过 JavaScript 编写的环境中,都有这个需求。所以 Floating UI 可以在原生 JS、React、React Native 等环境中使用。

体积

Floating UI 的体积仅有 600 b,而且它支持摇树。相比之下 Popper 3kb 的体积,并且不可摇树,显得非常臃肿了。

快速入门

安装

Floating UI 有好几个包,分别对应不同的平台。
下面是 Web 平台的包。

npm i @floating-ui/dom

如果想省事,还可以使用 ESM 格式通过 CDN 来加载 Floating UI。

<script type="module">
  import * as FloatingUIDOM from 'https://cdn.skypack.dev/@floating-ui/dom';
</script> 

computePosition

computePosition 是 Floating UI 的核心 API,它可以通过计算浮动元素的坐标将其定位在参考元素旁边。
我们利用这个 API 制作一个工具提示来尝试一下。
这是 html 部分。

<button id="button" aria-describedby="tooltip">我是一个按钮</button>
<div id="tooltip" role="tooltip">我是一个悬浮的工具提示</div>

包含了一个按钮一个 div,我们想让 div 浮动到按钮的正下方。
再来添加一些样式。

#tooltip {
  color: #fff;
  background: #363636;
  font-size: 1.2rem;
  padding: 10px 15px;
  border-radius: 8px;
  position: absolute;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05);
}

button {
  border-radius: 8px;
  border: none;
  outline: none;
  font-size: 1.2rem;
  cursor: pointer;
  padding: 10px 15px;
  color: #fff;
  background: rgb(48, 19, 129);
}

最后实现我们的功能。

import { computePosition } from "https://cdn.skypack.dev/@floating-ui/dom";

const button = document.querySelector("#button");
const tooltip = document.querySelector("#tooltip");

computePosition(button, tooltip).then(({ x, y }) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

我们首先获取到两个元素,button 是参考元素,tooltip 是浮动元素。
然后通过异步计算,得到 x 和 y 的坐标,设置到浮动元素上,完成定位。
我们还可以设置元素不同的位置,这通过 computePosition 的第三个参数来完成。

computePosition(button, tooltip , { placement: 'top-start' })

placement 可以指定浮动元素应该靠近参考元素的哪一侧。
image.png
一共有 12 个选项。

  • left-start
  • left
  • left-end
  • top-start
  • top
  • top-end
  • right-start
  • right
  • right-end
  • bottom-start
  • bottom
  • bottom-end

默认值是 bottom。

中间件

Floating UI 还提供了中间件的概念,就是在调用 computePosition 之后,then 之前运行的一段代码,可以改变浮动元素的定位和行为。
中间件是实现除了基本定位功能之外的其他功能统一的方式。
Floating UI 提供了下面几个中间件:

  • offset:修改参考元素和浮动元素之间的间距
  • shift:移动浮动元素,让它保持始终可见。它还会处理元素溢出到窗口之外的情况。
  • flip:帮我们修改坐标,如果我们设置浮动元素在顶部,但是距离顶部太近,会放置到底部。
  • size:调整浮动元素的大小。
  • autoPlacement:通过选择可用空间最多的位置来自动确定浮动元素的位置。
  • inline:改进跨多行的内联元素的定位,比如 a 标签。

使用中间件:

computePosition(button, tooltip, {
  placement: "top",
  middleware: [offset(4), flip(), shift({padding: 5})],
}).then(({ x, y }) => {
  // ...
});

中间件的使用也是有顺序的,比如 offset 应该始终处于数组的第一个位置。

交互

到现在为止,我们也只是解决了定位的问题。交互的问题还没有处理。
现在工具提示是一直显示的,通常情况下,它应该默认是隐藏状态,当我们通过某种交互事件后,它才会显示。比如点击、鼠标悬停等。
为了实现交互功能,我们需要封装代码。

function setUpTooltip() {
  computePosition(button, tooltip, {
    placement: "top",
    middleware: [offset(4), flip(), shift({ padding: 5 })],
  }).then(({ x, y }) => {
    Object.assign(tooltip.style, {
      left: `${x}px`,
      top: `${y}px`,
    });
  });
}

function showTooltip() {
  tooltip.style.display = "block";
  setUpTooltip();
}

function hideTooltip() {
  tooltip.style.display = "none";
}

现在只需要调用 showTooltip 就可以控制元素显示,调用 hideTooltip 就可以控制元素隐藏。
我们把这两个函数绑定到元素的鼠标移入移出事件上。

[
  ["mouseenter", showTooltip],
  ["mouseleave", hideTooltip],
].forEach(([event, listener]) => {
  button.addEventListener(event, listener);
});

现在功能就全部完成了。

在 React 中使用 Floating UI

在 React 中使用 Floating UI 非常友好,它提供了对应的包,我们只需要换成这个包就可以了。

npm install @floating-ui/react-dom

要实现上面演示的原生案例,对应的 React 代码如下:

import { useFloating, shift, offset, flip, useInteractions, useHover } from "@floating-ui/react-dom";

export default function App() {
  const [open, setOpen] = useState(true);
  const { x, y, reference, floating, strategy, context } = useFloating({
    placement: "right",
    middleware: [offset(4), flip(), shift({ padding: 5 })],
    open,
    onOpenChange: setOpen,
  });
  const { getReferenceProps, getFloatingProps } = useInteractions([
    useHover(context),
  ]);

  return (
    <>
      <button ref={reference} {...getReferenceProps()}>Button</button>
      { open && <div
        id="tooltip"
        ref={floating}
        style={{ top: y, left: x }}
        {...getFloatingProps()}
      >
        Tooltip
      </div>
      }
    </>
  );
}

可以看到,Floating UI 在 React 中具有了更多的功能,比如提供了 useInteractions Hook 帮助我们处理交互的问题。

总结

通过在几个项目中使用 Floating UI,我总结出的优点有以下几个:

  • API 友好,几乎不需要多做其他事情。
  • 文档和示例完善,可以去官网上复制代码。
  • 体积小,支持摇树,性能更好。
  • 跨环境,有 JavaScript 的地方就可以使用。
  • 跨框架,原生、React、Vue 等都可以使用。

关于 Floating UI 的介绍就到这里,快去试试吧。