构建下拉菜单
本指南涵盖所有弹出式菜单:下拉菜单、上下文菜单、溢出菜单以及打开这些菜单的菜单栏。此示例是一个应用命令菜单,其中的各项会更改界面(创建、保存、偏好设置)。
命令、链接和菜单的构成要素
两个独立的选择。此处的其他所有内容均适用于这两种情况。
- 元素,每件商品。命令是
<button type="button">;目标页面是<a href>,仍会在新标签页中打开、作为网址复制,并且无需脚本即可运行。下拉菜单可以自由混合使用这两种类型:两个链接和六个命令,或一个命令和六个链接,都不会改变以下内容。 - 互动模型,每个下拉菜单。方向键、
Home/End和Esc使其成为菜单:添加 focusgroup,让它推断每个项(无论是按钮还是链接)上的menuitem,并使其看起来像一个菜单(连续的栏或带有下拉指示器的触发器;请参阅下文的“菜单栏或单独的菜单按钮”)。用户通过Tab浏览的简短链接列表属于导航:没有 focusgroup、role="menu"、role="menuitem"或aria-haspopup="menu";保留 popover、Esc和焦点恢复,并参阅导航栏指南。
内容永远无法决定角色。“产品”下拉链接列表可以作为菜单;“账号”下拉命令列表不能作为导航。
仅对具有一个 Tab 键停靠位和箭头键导航功能的复合 widget 使用菜单模式。请使用 <div focusgroup="menu nomemory">,而不是 HTML <menu> 元素。请勿在共享标记中添加 role="menu" 或 role="menuitem":原生 focusgroup 会推断这些角色,而显式角色会阻止这种推断。请仅在下方的“不支持的浏览器后备”部分中添加这些内容。nomemory 会使重新打开时从第一项开始。焦点小组负责移动焦点;脚本负责处理预览、激活和焦点恢复。
打开并定位菜单
使用 commandfor 和 command="toggle-popover" 将每个触发器设置为命令调用方。浏览器会在用户点击、按 Enter 键和按空格键时打开和关闭弹出式窗口,并将触发器保留在其弹出式窗口的轻度关闭组中,因此点击打开的触发器会将其关闭。请勿使用 popovertarget 或 popovertargetaction;此模式改用 commandfor。
将每个弹出式窗口放置在其触发器之后。只有当弹出式窗口不是下一个同级元素时,浏览器才会添加隐式 aria-details 关系,因此这种放置方式无需额外属性即可避免这种情况。
<!-- Command invoker: the browser toggles the popover on click, Enter, and Space -->
<button id="file-trigger" class="trigger" type="button" aria-haspopup="menu" aria-expanded="false"
commandfor="file-menu" command="toggle-popover">
File
</button>
<!-- Immediately after its trigger, so browsers add no implicit aria-details -->
<div id="file-menu" popover="auto">
<!-- Nested, not on the popover: Chromium otherwise drops focusgroup keyboard behavior -->
<div focusgroup="menu nomemory" aria-labelledby="file-trigger">
<button type="button">New file</button>
<button type="button">Save</button>
<button id="export-trigger" type="button" aria-haspopup="menu" aria-expanded="false"
commandfor="export-menu" command="toggle-popover">
Export <span aria-hidden="true">›</span>
</button>
<!-- Submenu: popover="auto" directly after its trigger, inside the parent focusgroup -->
<div id="export-menu" popover="auto">
<div focusgroup="menu nomemory" aria-labelledby="export-trigger">
<button type="button">PDF</button>
<button type="button">Plain Text</button>
</div>
</div>
</div>
</div>
为每个菜单命名,并使用 aria-labelledby 指向其触发器。在 CSS 中保留装饰性子菜单指示器,或添加 aria-hidden="true"。为具有 aria-expanded="true" 的触发器提供可见的活动样式,以便用户可以分辨哪个菜单处于打开或预览状态。
菜单栏或单独的菜单按钮
为顶级触发器选择一种形式。角色、键盘模型和样式必须匹配:视觉上分隔的按钮表示每个按钮都有一个 Tab 停止点。
- 菜单栏。
role="menubar"位于容器上(绝不位于弹出式窗口上),样式为一条连续的栏,触发器之间没有间隙。Tab:停止;ArrowLeft/ArrowRight:在触发器之间移动;悬停:切换打开菜单。用于应用命令栏。 - 单独的菜单按钮。每个触发器都是一个单独的
Tab停止点,看起来像一个独立的按钮。没有role="menubar",触发器之间没有箭头键,焦点上没有预览,没有悬停切换。如果它们用于导航,请用<nav>封装,否则不使用角色。用于工具栏和页面标题。
本指南的其余部分将介绍菜单栏形式。对于单独的按钮,请将所有内容保留在每个菜单中,并放弃菜单栏级行为。
使用 CSS 锚点定位功能定位菜单
使用 CSS 锚点定位,而不是 JavaScript 坐标。command="toggle-popover" 和 showPopover({ source: trigger }) 都会设置隐式锚点,因此请勿添加每个菜单的 anchor-name 或 position-anchor 规则。菜单行为不得依赖于锚点定位。
/* Top-level menus sit below their trigger. The implicit anchor comes from the invoker or source. */
[popover] {
position-area: block-end span-inline-end;
inset: auto;
}
/* A popover nested in another popover is a submenu: beside its parent item, flipping when it would overflow. */
[popover] [popover] {
position-area: inline-end span-block-end;
position-try-fallbacks: flip-inline, flip-block, --below;
}
/* Last resort for narrow viewports: directly below the trigger, indented, so the submenu never
covers its trigger. */
@position-try --below {
position-area: block-end span-inline-end;
margin-inline-start: 1rem;
/* Fit the area: the submenu's minimum width must not stop this fallback from fitting. */
min-inline-size: 0;
max-inline-size: calc(100% - 1rem);
}
将 --below 放在最后,以便仅在两侧都不合适时应用。它会在触发器正下方打开,因此在窄屏幕上,它会覆盖触发器下方的同级项;这是为了保持在触发器旁边而做出的可接受的权衡。此外,还应让子菜单缩小到该区域(min-inline-size: 0、max-inline-size 减去缩进)。不合适的后备位置会被跳过,如果都不合适,浏览器会保留基本位置,因此,如果子菜单太宽(哪怕只是宽出几个像素),就会悄无声息地覆盖其触发器。
如需了解翻转后备在普通弹出式窗口中的运作方式,请参阅视口感知下拉菜单。该指南定位的是按钮组,而不是菜单;对于菜单,子菜单放置和上述 --below 回退优先。
打开和关闭动画
可选。在 @media (prefers-reduced-motion: no-preference) 内,仅 opacity 和小 translate 需在 150 毫秒内完成,子菜单也是如此。焦点会在 toggle 上移动,因此过渡效果绝不会延迟键盘用户。
@media (prefers-reduced-motion: no-preference) {
/* Closing: this rule is in effect once :popover-open is removed. */
[popover] {
opacity: 0;
translate: 0 -.25rem;
transition: opacity .15s, translate .15s, display .15s allow-discrete, overlay .15s allow-discrete;
}
/* Opening: faster. */
[popover]:popover-open {
opacity: 1;
translate: 0 0;
transition-duration: .1s;
@starting-style { opacity: 0; translate: 0 -.25rem; }
}
/* Switching between open menubar menus is instant, as in native menubars. */
[role="menubar"]:has(> [popover]:popover-open) > [popover] {
transition: none;
}
}
@starting-style 不是 [popover] 的重复:关闭的弹出式窗口为 display: none,并且过渡不会从非渲染状态运行,因此它会提供打开前的状态值。如果没有它,打开操作会瞬间完成,只有关闭操作会有动画效果。display 和 overlay 与 allow-discrete 搭配使用,可让弹出式窗口保持渲染状态并位于顶层,直到关闭过渡结束。
键盘和焦点
请勿在 focusgroup 中添加第二个 roving-tabindex 系统。脚本仅涵盖调用器和 focusgroup 未涵盖的内容:
- 菜单栏有一个 Tab 键停靠位。
Tab会在第一个顶级触发器处进入。ArrowLeft和ArrowRight可在顶级触发器之间移动,并在任一端换行。在触发器上按Home和End可进入其菜单的第一项或最后一项。- 注意:
Tab会返回到哪个触发器。APG 允许使用第一个触发器或最后一个触发器。指南中指定的是第一个,但演示中通过ArrowLeft和ArrowRight移动了制表位,因此在键盘导航后,Shift+Tab会返回到最后到达的触发器。使用指针不会移动它,因此相同的序列可能会落在不同的触发器上。这两种行为都是可以接受的;选择一种并保持一致。
- 注意:
- 预览版。聚焦于顶层触发器会显示其菜单并设置
aria-expanded="true"。焦点仍停留在触发器上,没有任何内容获得焦点。模糊处理触发器会关闭仅预览菜单。 - 在预览中按 Enter 键和空格键。他们关闭预览,并继续聚焦于触发器。原生
toggle-popover操作会处理此问题,因此脚本不得拦截这些键。 - 在预览上按向下键。将其转换为打开的菜单,并聚焦于第一项。
- 指针点击已打开或预览的触发器。直接关闭,不进入菜单。
- 子菜单触发器的已知差异。点击子菜单触发器也会原生切换,因此可以关闭悬停时已预览的子菜单,或打开尚未显示的子菜单。原生菜单只能打开它,而不能关闭它。这是无害的,因此本指南接受了它。
- 指针点击已关闭的触发器。以原生方式打开。焦点仍停留在触发器上;没有任何内容获得焦点。只有通过键盘打开的操作才会进入菜单。
- 指针悬停在某个内容上。聚焦时没有聚焦环,因此悬停和聚焦是同一个项。
- 菜单打开时,指针悬停在另一个顶级触发器上。关闭当前预览,并预览光标悬停的预览,聚焦其触发器(无环)。未打开任何内容:悬停不会触发任何操作。
- 指针悬停在子菜单触发器上。在短暂延迟后预览其子菜单(指针焦点会运行相同的“聚焦时预览”)。同级项会将其关闭。
- 在菜单中。
ArrowUp和ArrowDown可在已启用的内容项之间移动。Home和End会移至第一个和最后一个已启用的项,由脚本处理。 - 在打开的顶级菜单中按向左键或向右键。关闭该菜单并预览相邻菜单,焦点位于其触发器上,在任一端换行。
ArrowDown随后进入该模式。“向左”和“向右”操作绝不会进入菜单。 - 在子菜单触发器上按 ArrowRight。打开子菜单并将焦点置于其第一项。
- 在没有子菜单的子菜单项上按 ArrowRight。关闭层次结构并预览下一个顶级菜单,同时焦点仍停留在其触发器上。
ArrowDown随后进入该状态。 - 在子菜单中按向左键或 Esc 键。仅关闭该子菜单,并将焦点恢复到其触发器。父级菜单保持打开状态。
- 在命令上按 Enter 键或空格键。激活该项并关闭所有打开的菜单。
- Tab。退出菜单系统。请勿捕获焦点或逐步执行每个命令。关闭后,焦点不得留在隐藏的内容中。
command 事件会在 toggle-popover 打开菜单之前触发,因此焦点无法移至该菜单。而应在 toggle 事件中移动焦点。HTML 仅为堆栈中的第一个 popover="auto" 恢复焦点,因此嵌套层级必须明确地将焦点恢复到其触发器。
实现互动
为自己的标记实现上述行为。箭头、Home/End、换行和 Tab 退出处理是普通的 keydown 代码。下面按输入类型分组列出了几个容易出错的部分。
键盘和焦点处理
- 一条开放路径。通过键盘打开会调用
trigger.click(),因此调用器的toggle-popover是打开菜单的唯一方式。标记菜单,以便toggle处理程序进入该菜单,仅打开键盘。在指针打开时,toggle会聚焦于没有环的触发器,因为pointerdown阻止了聚焦,而箭头键仍必须正常工作。 - 聚焦时预览。聚焦于触发器会调用
showPopover({ source: trigger }),以便菜单锚定到该触发器。忽略从触发器自己的菜单内或从另一个窗口返回的焦点(窗口blur上的标志,在窗口focus之后清除勾选,或应用切换重新打开上一个菜单)。在blur上隐藏预览的菜单,除非焦点移入其中。 - 在
toggle(而非command)中移动焦点。command会在菜单打开之前触发。 - 自行将焦点恢复到触发器。在
hidePopover()之前聚焦于触发器。指针激活后,恢复焦点而不显示焦点环 (focus({ focusVisible: false })),或者触发器在鼠标点击后显示键盘焦点轮廓;键盘激活会保留焦点环。event.detail === 0的点击来自键盘。 - 一次关闭一个层级。在子菜单中对
Escape和ArrowLeft调用stopPropagation(),否则父菜单的监听器也会将其关闭。
指针处理
- 指针点击切换开关。在
pointerdown上调用preventDefault(),这样点击就不会移动焦点,也不会在点击过程中启动预览。记住按下了哪个扳机键。点击已关闭或预览的触发器会以原生方式打开或关闭该触发器,而不会进入菜单;只有通过键盘打开才会进入菜单。请勿拦截点击。 - 悬停跟随焦点。分别设置
:hover和:focus-visible的样式。在某个项上执行pointermove时,调用focus({ focusVisible: false });忽略该选项的浏览器无论如何都不会为指针焦点绘制圆环。 - 悬停开关可打开菜单。在另一个顶级菜单打开时,
pointerenter顶级触发器:隐藏该顶级菜单,将焦点移至悬停的触发器(不显示焦点环),并预览焦点所在的内容(见上文)。没有打开任何内容:不执行任何操作。 - 安全三角区。固定的 CSS 桥接太窄:只有当指针从父级边缘附近开始移动时,它才会起作用。让 CSS 绘制桥梁,并仅使用脚本来报告指针:
- 仅在
matchMedia('(hover: hover)').matches时运行此测试;它与触摸和键盘无关。 - 当指针离开已打开子菜单的子菜单触发器时启动。跳过没有侧边边缘的
--below子菜单。 - 在子菜单上设置
--bridge-x和--bridge-y(指针,以相对于子菜单的像素为单位)、--bridge-edge(子菜单的近边缘 x:当指针位于其左侧时为0,当其翻转时为其宽度)和--bridge-height,并设置data-bridge以开启桥接。 - 在大约 300 毫秒后、指针到达子菜单中的某一项时或子菜单关闭时停止。移除监听器和属性。之后不会运行任何内容。
- CSS 为子菜单提供了一个
::before,该背景被剪裁成一个三角形,从指针延伸到靠近边缘的角落。它位于指针路径上的同级项上方,因此这些同级项永远不会收到会关闭子菜单的悬停事件。clip-path还会限制点击测试,以便家长专用菜单的其余部分保持互动性。 - 子菜单需要
overflow: visible(默认情况下,弹出式窗口会剪裁),而伪元素会超出该范围,因此需要将形状的点偏移该范围。 - 后续:弯曲路径(
shape()曲线命令)比直线三角形更符合实际指针弧线。
- 仅在
[popover] [popover] {
overflow: visible;
&[data-bridge]::before {
content: "";
position: absolute;
inset: -100vh -100vw; /* Room for the triangle, which can extend far outside the submenu. */
clip-path: shape(
from calc(100vw + var(--bridge-x)) calc(100vh + var(--bridge-y)),
line to calc(100vw + var(--bridge-edge)) 100vh,
line to calc(100vw + var(--bridge-edge)) calc(100vh + var(--bridge-height)),
close
);
}
}
const canHover = matchMedia('(hover: hover)');
// Call on pointerleave of a submenu trigger, with that trigger's submenu.
function startBridge(submenu, event) {
if (!canHover.matches || !submenu.matches(':popover-open')) return;
const rect = submenu.getBoundingClientRect();
if (event.clientX >= rect.left && event.clientX <= rect.right) return; // --below: no side edge.
const style = submenu.style;
style.setProperty('--bridge-edge', `${event.clientX < rect.left ? 0 : rect.width}px`);
style.setProperty('--bridge-height', `${rect.height}px`);
const update = ({ clientX, clientY, target }) => {
if (target !== submenu && submenu.contains(target)) return stop(); // Reached an item.
style.setProperty('--bridge-x', `${clientX - rect.left}px`);
style.setProperty('--bridge-y', `${clientY - rect.top}px`);
};
const timer = setTimeout(() => stop(), 300);
const stop = () => {
clearTimeout(timer);
document.removeEventListener('pointermove', update);
submenu.removeEventListener('toggle', stop);
submenu.removeAttribute('data-bridge');
};
update(event);
submenu.dataset.bridge = '';
document.addEventListener('pointermove', update);
submenu.addEventListener('toggle', stop, { once: true });
}
“悬停跟随焦点”(第 2 项)仍是一个简单的 pointermove 处理程序:它不需要三角形逻辑。
另请注意:
- 焦点之外。当焦点落在菜单系统之外时,关闭每个打开的菜单(
documentfocusin监听器)。 - 命令激活。播报该操作,然后隐藏根菜单并将焦点恢复到其触发器,如果激活来自指针,则不显示焦点环。对于可勾选的项(
menuitemcheckbox、menuitemradio),请等待大约 300 毫秒再关闭,以便用户看到更改后的对勾标记。将关闭的触发器样式设置为仅在:focus-visible上处于活动状态;否则,在指针选择后,恢复的(无环)焦点会使其保持突出显示。 aria-expanded。请仅在toggle监听器中设置。它涵盖预览和打开状态,并且在预览或打开菜单时,触发器的aria-expanded会保持"true"。- 帮助程序。以下后备方案假定您为标记定义了辅助函数:
triggers(所有[commandfor]按钮)、menuFor(trigger)、isOpen(menu)、closeAndRestore(menu, { fromPointer })(聚焦触发器,fromPointer时不显示环,然后menu.hidePopover())、enterOnOpen(打开后要进入的菜单WeakSet)和pointerTrigger(指针最后按下的触发器)。
子菜单
- 将
popover="auto"用于子菜单,并将其放置在父级focusgroup内的子菜单触发器之后。永不popover="manual"。 - 嵌套功能可让浏览器保持父级菜单打开、关闭同级子菜单、在根菜单关闭时关闭子菜单,以及处理外部点击轻触关闭。
- 子菜单触发器会保留
aria-haspopup="menu"和aria-expanded;toggle监听器会使aria-expanded保持同步。 - 键盘脚本会处理嵌套的
Escape和ArrowLeft,如上所述。 - 子菜单触发器绝不会同时运行命令:一个控件不能同时执行激活和打开操作,
Enter也不能同时表示这两种操作。将默认操作作为第一个子菜单项,或在触发器旁边添加一个单独的命令项。如果必须使用组合控件,请将其记录为无障碍功能方面的折衷方案。
分隔线和可勾选的内容
- 在 focusgroup 中使用
<hr>分隔商品组:原生separator,不可聚焦,会被 focusgroup 和后备机制跳过。切勿使用带样式的<div>。 - 布尔值选项是具有
aria-checked和显式role="menuitemcheckbox"的<button type="button">。Focusgroup 不会推断这些角色,因此这是共享标记设置项角色的唯一位置。 - 其中一个集合是
<button type="button">,它包含role="menuitemradio"和aria-checked,位于role="group"中,包含aria-label,它们位于同一个 focusgroup 中。 - 脚本切换开关
aria-checked。 - 在复选框上按
Enter会切换状态并关闭菜单;按Space会切换状态并保持打开状态 (APG)。按任一键即可关闭单选按钮。 - 从
[aria-checked="true"]::before渲染对勾标记,而不是单独的图标元素(请参阅下面的 CSS)。
此处未涵盖
- 激活反馈。原生菜单会在关闭前闪烁所选项。如果需要,请在
hidePopover()之前显示约 100 毫秒的视觉确认;请勿延迟焦点恢复或语音播报。 - 预输入。焦点小组没有。如有需要,在可打印的
keydown上,匹配当前层级已启用的内容标签,并聚焦于当前内容之后的下一个匹配项,然后换行。请勿拦截Space。
完成前先验证
指令无法保证行为,因此请测试结果。打开页面,使用真实的键盘和鼠标检查以下各项;修复并重新测试所有失败的项:
- 键盘。
Tab落在第一个触发器上一次;ArrowDown进入;Escape关闭一个层级并将焦点返回到其触发器;Tab关闭每个菜单。 - 指针打开。点击触发器会打开其菜单,焦点位于触发器上,且没有圆环;再次点击会关闭菜单。
- 指针命令。点击命令会关闭所有菜单,并且触发器上不会显示焦点环。
- 子菜单。从子菜单触发器的中间沿对角线移动到子菜单项,可使子菜单保持打开状态;移动到不相关的同级项则会关闭子菜单。
- 窄屏。在 375 像素宽度下,没有任何子菜单会遮挡其自身的触发器。
- 对勾标记。已勾选的内容会显示对勾标记。
- 不留剩饭。没有控制台错误,触发器上也没有
popovertarget、popovertargetaction或aria-details。
对齐菜单项内容
如果某行包含图标、标签和快捷键或子菜单指示器,请为图标或对勾标记预留一个固定大小的前导槽位,并将尾随内容推送到内嵌末尾。图标的大小是已知的,因此无论内容项是否包含图标,固定槽位都能使标签保持对齐。请勿对行使用 subgrid:任何额外的内嵌子项(例如标记)都会成为其自己的网格项,并破坏布局。如果快捷键文本只是重复已操作该项的键,请向辅助技术隐藏快捷键文本。
[focusgroup~="menu"] {
--menu-icon-size: 1.25rem;
}
/* Target items only. A closed submenu popover inside the focusgroup must keep display: none. */
[focusgroup~="menu"] > button,
[focusgroup~="menu"] > [role="group"] > button {
display: flex;
align-items: center;
gap: .5rem;
}
/* The leading slot holds the icon or check mark. */
[focusgroup~="menu"] button::before {
content: "";
flex: none;
inline-size: var(--menu-icon-size);
}
/* Check mark for checked items. This selector must be at least as specific as the base
::before rule above, or the empty content wins and the check mark never shows. */
[focusgroup~="menu"] button[aria-checked="true"]::before {
content: "✓";
}
/* Shortcut or submenu indicator at the inline end. */
[focusgroup~="menu"] .hint {
margin-inline-start: auto;
}
在按钮内的 <span class="hint" aria-hidden="true"> 中放置尾随指示器或快捷键;子菜单触发器已播报其弹出式窗口。
渐进增强和后备
focusgroup 的后备方案和浏览器支持
在标记中保留 focusgroup="menu nomemory"。如果 'focusGroup' in HTMLElement.prototype 为 false,则安装 Up、Down、Home 和 End,在每个菜单容器上设置 role="menu",在没有角色的按钮上设置 role="menuitem";不处理可勾选项、<hr> 和 role="group"。如果存在原生 focusgroup,请勿安装该 polyfill。无论哪种方式,左、右、预览、打开和关闭都是作者脚本。
if (!('focusGroup' in HTMLElement.prototype)) {
document.querySelectorAll('[focusgroup~="menu"]').forEach(group => {
group.setAttribute('role', 'menu');
group.querySelectorAll('button:not([role])').forEach(item => {
item.setAttribute('role', 'menuitem');
});
group.addEventListener('keydown', event => {
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
// Own items only: a nested submenu has its own focusgroup, a role="group" wrapper does not.
const items = [...group.querySelectorAll('button')]
.filter(item => item.closest('[focusgroup]') === group)
.filter(item => !item.disabled && item.getAttribute('aria-disabled') !== 'true');
const current = items.indexOf(document.activeElement);
if (current < 0) return;
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 :
(current + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
event.preventDefault();
event.stopImmediatePropagation();
items[next].focus({ preventScroll: true });
}, true);
});
}
Popover 的回退和浏览器支持
Popover API 主要是一种渐进增强技术,但其核心行为(顶层提升、轻触关闭和 popovertarget 调用)没有仅限 CSS 的等效项。旧版浏览器需要 polyfill,如果您不想提供 polyfill,则需要手动后备方案。
Polyfill:如需在旧版浏览器中支持 popover 属性,请有条件地加载 @oddbird/popover-polyfill。强制性要求:通过检查 HTMLElement.prototype 上的 popover 属性来检测功能,并且仅在缺少原生支持时加载 polyfill,请勿无条件加载。
使用打包工具或导入映射:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
如果没有打包器,请从 <script type="module"> 内的 CDN 导入:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
样式注意事项:polyfill 无法定义真正的 :popover-open 伪类,因此它会改为应用 .\:popover-open 类。强制性:使用 :is() 或 :where() 将两者结合,否则缺少 :popover-open 的浏览器会舍弃整个规则:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
或者,对于没有 polyfill 的旧版后备方案,请使用 position: fixed 并通过 getBoundingClientRect() 手动计算坐标,或依赖 inset: auto 的默认定位(如果该用例可以接受)。
锚点定位的后备方案和浏览器支持
如果没有 CSS 锚点定位,弹出式窗口会回退到 UA 的居中视口放置。切勿在菜单中使用该值:它会使菜单与其触发器分离,并且居中的子菜单会覆盖其父级菜单。在 @supports (position-area: block-end) 后面限制锚定规则,并在失败时通过脚本定位菜单:
- 将菜单放置在其触发器下方(顶层)或旁边(子菜单),在溢出时翻转到另一侧并固定到视口。
- 在菜单打开时,在
toggle处理程序中运行该函数,并将结果作为left和top应用于菜单。将position: fixed与inset: auto和margin: 0搭配使用,使坐标相对于视口。 - 请将这些替换项保留在
@supports not (position-area: block-end)中,这样支持锚点定位的浏览器就不会受到影响。
如果目标是 showPopover({ source }) 不会建立隐式锚点(ToggleEvent.prototype 上没有 source)的早期实现,请通过脚本为每个触发器指定唯一的 anchor-name,并为每个菜单指定匹配的 position-anchor,而不是为每个菜单编写 CSS 规则。
对于完全不支持锚点定位的浏览器,请参阅视口感知下拉菜单,了解 polyfill 方案。
调用器命令的后备方案和浏览器支持
如果没有调用程序,command="toggle-popover" 将不会执行任何操作,因此脚本必须切换菜单。
- 检测接口(而非实例)的功能:
'commandForElement' in HTMLButtonElement.prototype。 - 在每个触发器
click上,调用preventDefault(),以便具有调用器的浏览器无法切换两次。 - 如果菜单已打开,请使用
closeAndRestore(menu)将其关闭。否则,请致电menu.showPopover({ source: trigger })。 - 在
enterOnOpen中标记菜单,除非点击来自按下的触发器 (pointerTrigger),因此只有键盘打开操作会进入菜单。 Enter和Space仍然有效,因为它们会生成对按钮的点击。