场景几乎总是相同的,即可滚动容器内的数据表。每行都有一个操作菜单,一个带有一些选项的小下拉菜单,例如编辑、复制和删除。你构建它,它似乎可以完美地独立工作,然后有人将它放入可滚动的 div 中,事情就崩溃了。我在三个不同的代码库中看到了这个确切的错误:容器、堆栈和框架,全都不同。不过,这个错误是完全相同的。 下拉菜单被剪切在容器的边缘。或者它出现在逻辑上应该位于其下方的内容后面。或者它工作得很好,直到用户滚动,然后它就漂移了。 您可以使用 z-index: 9999。有时它会有所帮助,但有时它完全没有任何作用。这种不一致是表明更深层次的事情正在发生的第一个线索。 它不断出现的原因是涉及三个独立的浏览器系统,大多数开发人员都单独了解每个浏览器系统,但从未考虑过当这三个系统发生冲突时会发生什么:溢出、堆叠上下文和包含块。
一旦您了解了这三者如何相互作用,故障模式就不再感觉是随机的。事实上,它们变得可以预测。 实际上导致这种情况的三件事 让我们详细看看每一项。 溢出问题 当您在元素上设置溢出:隐藏、溢出:滚动或溢出:自动时,浏览器将剪切超出其边界的任何内容,包括绝对定位的后代。 .scroll-container { 溢出:自动; 高度:300px; /* 这将剪辑下拉菜单,句号 */ }
.dropdown { 位置:绝对; /* 没关系——仍然被 .scroll-container 剪切 */ }
当我第一次遇到它时,这让我很惊讶。我假设position:absolute会让元素逃脱容器的裁剪。事实并非如此。 实际上,这意味着绝对定位的菜单可以被具有不可见溢出值的任何祖先切断,即使该祖先不是菜单的包含块。剪切和定位是独立的系统。它们只是碰巧以看起来完全随机的方式发生碰撞,直到你理解两者为止。
这是一个使用 createPortal 的 React 示例:
从'react-dom'导入{createPortal}; 从 'react' 导入 { useState, useEffect, useRef };
函数 Dropdown({anchorRef, isOpen, 儿童 }) { const [位置,setPosition] = useState({ 顶部:0,左侧:0 });
使用效果(()=> { if (isOpen &&anchorRef.current) { const rect =anchorRef.current.getBoundingClientRect(); 设置位置({ 顶部:矩形.底部+窗口.scrollY, 左: rect.left + window.scrollX, }); } }, [isOpen,anchorRef]);
if (!isOpen) 返回 null;
返回创建门户(
当然,我们不能忽视可访问性。出现在内容上方的固定元素必须仍然可以通过键盘访问。如果焦点顺序没有自然地移动到固定下拉列表中,您需要使用代码来管理它。还值得检查的是它不会凌驾于其他交互式内容之上而无法将其忽略。这在键盘测试中会咬你一口。 CSS 锚点定位:我认为这是未来的发展方向 CSS Anchor Positioning 是我目前最感兴趣的方向。当我第一次看到它时,我不确定该规范中有多少内容是实际可用的。它允许您直接在 CSS 中声明下拉菜单及其触发器之间的关系,并由浏览器处理坐标。 .trigger { 锚点名称:--my-trigger; }
.dropdown-menu { 位置:绝对; 位置锚点:--my-trigger; 顶部:锚点(底部); 左:锚点(左); position-try-fallbacks:翻转块、翻转内联; }
position-try-fallbacks 属性使得它比手动计算更值得使用。浏览器在放弃之前会尝试其他位置,因此视口底部的下拉菜单会自动向上翻转而不是被切断。 基于 Chromium 的浏览器对浏览器的支持非常稳定,并且 Safari 的支持也在不断增强。 Firefox 需要一个polyfill。 @oddbird/css-anchor-positioning 包涵盖了核心规范。我遇到了布局边缘情况,需要我没有预料到的回退,所以将它视为渐进增强或将其与Firefox 的 JavaScript 后备。 简而言之,有希望但尚未普及。在目标浏览器中进行测试。 就可访问性而言,在 CSS 中声明视觉关系并不会告诉可访问性树任何信息。 aria-controls、aria-expanded、aria-haspopup——这部分仍然由你负责。 有时解决方法只是移动元素 在到达门户或进行坐标计算之前,我总是先问一个问题:这个下拉列表实际上需要位于滚动容器内吗? 如果没有,将标记移动到更高级别的包装器可以完全消除问题,无需 JavaScript,也无需进行坐标计算。 这并不总是可能的。如果按钮和下拉菜单封装在同一个组件中,则移动一个组件而不移动另一个组件意味着重新考虑整个 API。但当你能做到的时候,就没有什么可调试的了。这个问题根本不存在。 现代 CSS 仍未解决的问题 CSS 在这方面已经取得了长足的进步,但仍有一些地方让您失望。 立场:固定和改造问题仍然存在。它是故意出现在规范中的,这意味着不存在 CSS 解决方法。如果您使用的动画库将布局包装在转换后的元素中,那么您又需要门户或锚点定位。 CSS 锚点定位很有前途,但却是新事物。正如前面提到的,在我写这篇文章时,Firefox 仍然需要一个 polyfill。我遇到了布局边缘情况,需要我没有预料到的回退。如果您现在需要在所有浏览器上保持一致的行为,那么您仍然需要使用 JavaScript 来处理棘手的部分。 我实际上改变了我的工作流程的附加功能是 HTML Popover API,现在可在所有现代浏览器中使用。具有 popover 属性的元素呈现在浏览器的顶层,位于所有内容之上,无需 JavaScript 定位。
对于工具提示、公开小部件和简单的覆盖等内容,转义处理、点击外部关闭和可靠的可访问性语义都是免费的。这是我目前使用的第一个工具。 也就是说,它并不能解决定位问题。它解决了分层问题。您仍然需要锚点定位或 JavaScript 将弹出窗口与其触发器对齐。 Popover API 处理分层。锚定位处理放置。一起使用,它们涵盖了您以前需要图书馆做的大部分事情。 适合您情况的决策指南 在经历了这一切之后,我现在对这个选择的实际想法是这样的。
使用门户。当触发器位于嵌套滚动容器深处时,我会使用它。我将此模式用于表格操作菜单,并将其与焦点恢复和可访问性检查配对。这是最可靠的选择,但需要预留额外接线的时间。 使用固定定位。这适用于当您使用普通 JavaScript 或轻量级框架并且可以验证没有祖先应用转换或过滤器时。只要满足这一限制,它的设置和调试都很简单。 当您的浏览器支持允许时,请使用 CSS Anchor Positioning.Reach 来实现此目的。如果需要 Firefox 支持,请将其与 @oddbird polyfill 配对。这是该平台最终的发展方向,并将最终成为您的首选方法。 重构 DOM。当架构允许并且您希望运行时复杂性为零时使用此功能。我相信这可能是最被低估的选择。 组合模式。当您希望将锚点定位作为主要方法,并与不受支持的浏览器的 JavaScript 后备配对时,请执行此操作。或者用于 DOM 放置的门户与 getBoundingClientRect() 配对以提高坐标精度。
结论 我曾经将这个错误视为一次性问题——需要修补并继续前进。但一旦我用它足够长的时间来理解所涉及的所有三个系统——溢出裁剪、堆叠上下文和包含块——它就不再感觉随机了。我可以查看损坏的下拉列表并立即追踪出哪个祖先负责。我阅读 DOM 方式的转变才是真正的收获。 没有单一的正确答案。我所达到的目标取决于我可以在代码库中控制什么:当祖先树不可预测时的门户;干净简单时固定定位;当没有什么能阻止我时移动元素;现在锚定位,我可以的地方。 无论您最终选择什么,都不要将可访问性视为最后一步。根据我的经验,这正是它被跳过的时候。 ARIA 关系、焦点管理、键盘行为——这些都不够完美。它们是让事情真正发挥作用的一部分。 查看我的 GitHub 存储库中的完整源代码。 进一步阅读 这些是我在处理此问题时不断回顾的参考资料:
堆栈上下文 (MDN) 《CSS 锚点定位指南》,Juan Diego Rodriguez “Popover API 入门”,Godstime Aburu 浮动用户界面 (floating-ui.com) CSS 溢出 (MDN)