关于在用户界面中定位弹出框(工具提示、弹出框、下拉菜单)的所有知识
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我是Popper的核心开发者和维护者,也是Tippy的创建者。Popper 和 Tippy 是两个流行的库,用于在 Web 应用程序中构建工具提示、弹出框、下拉菜单、菜单等。在从事这些工作几年后,我想分享我在此期间积累的知识。这些元素在 Web 应用程序中无处不在,因此了解它们的工作原理对于创造良好的用户体验至关重要!
免责声明:这并非我所知的全部,因为其中涉及诸多细微差别和微妙之处,难以在一篇文章中尽述😅。本文将重点探讨关键问题及其解决方案。
爆米花?
从本质上讲,弹出元素是指那些从文档的正常流程中“弹出”(绝对定位或固定定位)并浮动到参考元素或目标元素(例如按钮)附近,叠加在用户界面上的元素。从概念上讲,用于定位它们的 CSS 代码非常简单:只需设置position: absolute元素的属性以及一些坐标top,left即可将其定位到参考元素旁边的某个位置(顶部、底部、左侧或右侧)。
然而,获取这些坐标的计算过程十分复杂。虽然我们可以简单地计算坐标,例如将其居中放置在按钮上方,但很快就会遇到一些特殊情况,而像 Popper 这样强大的解决方案可以帮我们解决这些问题。对于可重用的组件库来说,这一点至关重要,因为我们不希望用户每次向 UI 添加弹出框时都手动计算其坐标,以使其尽可能完美地适应屏幕。理想情况下,我们希望只需调用一个position()函数即可自动完成这项繁重的工作。
首先,您需要将弹出框放置在用户界面中某个位置的参考元素旁边。为此,您可以从 12 种放置位置中选择一个:四种基本位置(,,,,top)或带有变体的位置(或):rightbottomleftstartend
然而,如果不使用任何其他逻辑,这样做是有问题的(例如,如果您只使用 CSS)。让我们来探讨一下为什么我们需要 JavaScript。
问题 1:防止弹出框被裁剪或超出边界主轴时发生溢出
当引用元素靠近边界边缘(例如窗口左边缘)且工具提示的宽度大于引用元素时,就会出现问题:部分工具提示文本会被截断,导致无法阅读。如果工具提示位于左侧,文本会被裁剪而无法查看;如果位于右侧,则会导致文本溢出,需要滚动才能查看(从左到右布局)。
我们该如何解决这个问题?我们计算它超出边界的程度,然后调整它的视图位置来避免这种情况。在 Popper 中,这被称为preventOverflow修改器。
Popper 还允许您指定弹出框与边界之间的空白或内边距。在本例中,默认情况下它与边界边缘齐平。
问题 2:弹出框何时会被裁剪或超出边界的 alt 轴?
现在我们已经防止了左右溢出——但我们又遇到了另一个问题。
当参考元素靠近边界边缘(例如窗口顶部边缘)时,工具提示会被截断。这与前一个问题中的防止溢出问题类似,但发生在不同的轴上。
然而,这次我们无法阻止沿 y 轴的溢出,因为如果这样做,弹出框会覆盖引用元素并遮挡其内容。有时这或许可以接受,但在大多数情况下是不可接受的(尽管 Popper 允许你这样做!)。
相反,我们希望将其翻转到一个更合适的位置,而不仅仅是将其移到可见区域。在 Popper 中,这被称为flip修饰符。
解决这两个问题是波普尔理论的核心,但远非其全部。
问题 3:滚动容器
你的引用元素和弹出框可以位于DOM 中的任何位置。例如,你的引用元素和弹出框元素可以位于不同的滚动容器中。弹出框需要能够处理你赋予它的任何 DOM 上下文。
请看以下HTML代码:
<div id="scroll">
Lots of text here
<button id="reference">My Button</button>
Lots of text here
</div>
<div id="tooltip">My Centered Tooltip</div>
当滚动容器滚动时,引用元素在屏幕上的位置会发生变化。但是,你的工具提示“感知不到”这种变化,因此它不会跟踪滚动过程中引用元素的位置。
为了解决这个问题,Popper 会给容器添加一个滚动监听器,并在每次触发滚动事件时重新计算 popper 的位置。这样就能让它像预期那样始终紧贴着引用元素。(这也适用于主元素window/html/body)。
你可能会认为这会造成性能问题,但计算表明,即使在低端手机等性能较差的硬件上,这个过程也应该能在 10 毫秒内完成(帧预算 + 开销)。Popper 的设计非常高效,以确保这一点。
您可能还会注意到弹出框超出了红色边界。在这种情况下,滚动容器并非像视口那样的“根”,也不是弹出框的裁剪父元素,因为它位于 DOM 结构中滚动容器之外,所以我们仍然可以将其居中显示。
问题 4:offsetParent
绝对定位的元素是相对于其参考系offsetParent(即被编辑的那个position)进行定位的。这与 DOM 上下文方面的问题 3 相关——offsetParent这会造成一个问题:getBoundingClientRect()获取参考元素的矩形区域需要用到 `<div>` 属性,但它始终是相对于视口的。如果弹出框不是相对于视口定位的,那么在测量参考元素的位置时,我们需要考虑它的 ` <div>` 属性offsetParent。
在上图中,我们需要从弹出框的坐标偏移量中减去参考元素的坐标,offsetParent否则x就会出现下图红色阴影框所示的错误位置。原因是参考元素比视口更靠近参考元素的坐标 (0, 0),因此弹出框的位置会过远。yxyoffsetParent
问题 5:由于不同的裁剪上下文而导致的隐藏
参考元素和弹出元素处于不同的裁剪上下文中可能会导致问题。如果参考元素被完全裁剪并隐藏,弹出元素可能会显得与参考元素“分离”,甚至完全没有附着在任何东西上。
Popper 在以下情况下会附加属性:
data-popper-escaped当弹出框脱离引用元素的裁剪容器时(它看起来像是分离的)。data-popper-reference-hidden当引用元素从视图中隐藏时(它看起来好像没有附着任何东西) 。
这样,当弹出器不再看起来像是在某个物体附近漂浮时,就可以将其淡出或隐藏起来。
问题 6:箭头
到目前为止,我们讨论的都是主弹出框。但在显示的每个图像中,都有一个小箭头(插入符号或三角形),它始终指向位于弹出框外部的参考元素的中心。
虽然箭本身不是问题,但它却引发了几个问题。
- 箭头会始终尝试相对于参考点保持居中,但是,在某些情况下,这是不可能的,例如以下情况:
我们需要始终将箭头限制在弹出框内。这会带来一些美观问题,因此 Popper 也允许您在不需要时隐藏箭头。
- 如果参考点是“点状”的,则箭头需要改变形状——通过计算其与理想位置的偏移量来进行插值,从而指向参考元素:
Popper 以如下形式提供这些数据centerOffset:
问题 7:虚拟元素
我们不能假设定位所依据的是一个“真实”元素。你应该能够将弹出框定位到“虚拟”元素(即 DOM 中实际不存在的元素)旁边,以便与 `<div>` mousemove、contextmenu`<div>` 等元素一起使用。
问题 8:尺寸
有时即使flip使用修改器也无法解决问题,因为弹出框本身尺寸过大,无法完全显示在屏幕上。虽然有一些变通方法,例如设置max-width: 100vw顶部/左侧位置,但 Popper 允许动态调整弹出框的大小,使其适应屏幕上任何位置的可用视口空间。
虽然默认情况下不包含此功能,但它可以通过几行代码编写的社区软件包获得,这充分展现了 Popper 强大的可扩展性。
问题9:浏览器漏洞
浏览器兼容性问题真是让人头疼😔🤚!以下是我们开发 Popper 2 时发现的部分问题:
- Firefox 对固定元素返回的
<body>是 `<div> `,而根据规范,offsetParent它应该返回 `<div>`。null - Edge 和 IE 始终将元素报告
.scrollTop为0<body> - Safari 的弹性滚动条功能会导致
fixed弹出窗口随滚动条移动而错误移动。 - Safari 的
style.transform更新速度较慢,且并非完全同步。因此,防止页面溢出的效果并不完美。其他浏览器虽然也并非完美,但要好得多。 - 我们
translate3d()仅在高 PPI 设备上使用此功能以提高性能和转换流畅度,但在启用 Windows 缩放的低 PPI 显示器上,可能会导致模糊问题。
总之,我希望您对 Popper 的存在意义以及它为 UI 元素解决的关键问题有了更深入的了解。如果您还没有使用 Popper,我强烈建议您开始使用它!Popper 可以优雅地解决所有这些问题,无需您重复造轮子。我们花费了数百小时开发这个库,并遇到了无数的极端情况,也一一进行了修复。不妨看看我们的视觉测试截图!
文章来源:https://dev.to/atomiks/everything-i-know-about-positioning-poppers-tooltips-popovers-dropdowns-in-uis-3nkl











