弹出式窗口:正在复苏!

开放界面计划旨在让开发者更轻松地打造出色的用户体验。为此,我们正尝试解决开发者面临的更棘手的问题。我们可以通过提供更好的平台内置 API 和组件来实现这一点。

其中一个问题领域是弹出式窗口,在 Open UI 中称为“Popover”。

长期以来,弹出式窗口的声誉一直褒贬不一。这在一定程度上是因为它们在构建和部署方面的方式。虽然这种模式不易于构建,但如果使用得当,可以引导用户访问特定内容或让用户了解您网站上的内容,从而带来巨大价值。

在构建弹出式窗口时,通常需要考虑两个主要问题:

  • 如何确保它放置在适当的位置,并显示在其余内容上方。
  • 如何使其具有无障碍功能(支持键盘操作、可聚焦等)。

内置的 Popover API 有多种目标,但总体目标都是让开发者能够轻松构建这种模式。这些目标中值得注意的有:

  • 轻松地将元素及其后代显示在文档的其余部分之上。
  • 让 AI 触手可及。
  • 不需要 JavaScript 即可实现最常见的行为(轻触关闭、单例、堆叠等)。

您可以在 OpenUI 网站上查看有关弹出式窗口的完整规范。

浏览器兼容性

现在,您可以在哪些位置使用内置的 Popover API?截至撰写本文时,Chrome Canary 版支持此功能,但需要启用“实验性 Web 平台功能”标志。

如需启用该标志,请打开 Chrome Canary 版并访问 chrome://flags。然后,启用“实验性 Web 平台功能”标志。

我们还为希望在生产环境中测试此功能的开发者提供了源试用

最后,我们正在为该 API 开发 polyfill。请务必查看 github.com/oddbird/popup-polyfill 中的代码库。

您可以使用以下命令检查弹出式窗口支持:

const supported = HTMLElement.prototype.hasOwnProperty("popover");

当前解决方案

目前,您可以通过哪些方式优先推广自己的内容?如果您的浏览器支持,您可以使用 HTML 对话框元素。您需要以“模态”形式使用它。此网站需要启用 JavaScript 才能使用。

Dialog.showModal();

以下是一些无障碍方面的注意事项。建议使用 a11y-dialog,例如,如果需要满足使用低于 15.4 版 Safari 的用户的需求。

您还可以使用许多基于弹出式窗口、提醒或提示的库。其中许多的运作方式都类似。

  • 将一些容器附加到正文中以显示弹出式窗口。
  • 设置样式,使其位于所有其他内容之上。
  • 创建一个元素并将其附加到容器以显示 popover。
  • 通过从 DOM 中移除弹出式信息框元素来隐藏它。

这需要额外的依赖项,并且开发者需要做出更多决定。此外,您还需要进行研究,找到能满足您所有需求的方案。Popover API 旨在满足多种场景的需求,包括工具提示。这样做的目的是涵盖所有常见场景,让开发者无需再做出其他决定,从而专注于打造自己的体验。

您的第一个弹出式窗口

您只需执行这些操作。

<div id="my-first-popover" popover>Popover Content!</div>
<button popovertoggletarget="my-first-popover">Toggle Popover</button>

但这里发生了什么?

  • 您无需将弹出框元素放入容器或其他任何位置,它默认处于隐藏状态。
  • 您无需编写任何 JavaScript 即可显示该按钮。这由 popovertoggletarget 属性处理。
  • 当它出现时,会被提升到顶层。这意味着它会提升到视口中 document 的上方。您无需管理 z-index,也不必担心弹出式窗口在 DOM 中的位置。它可能深层嵌套在 DOM 中,并具有剪切祖先。您还可以通过开发者工具查看哪些元素目前位于顶层。如需详细了解顶层,请参阅这篇文章

演示开发者工具顶层支持的 GIF

  • 您可直接使用“轻触即关”。也就是说,您可以通过关闭信号(例如点击弹出框外部、使用键盘导航到其他元素或按 Esc 键)关闭弹出框。再次打开该应用,然后试用一下!

您还可以通过弹出式窗口获得哪些信息?我们再举一个例子。请观看此演示,其中包含网页上的一些内容。

该悬浮操作按钮具有固定的位置和较高的 z-index

.fab {
  position: fixed;
  z-index: 99999;
}

弹出式窗口内容嵌套在 DOM 中,但当您打开弹出式窗口时,它会提升到该固定位置元素之上。您无需设置任何样式。

您可能还会注意到,弹出式窗口现在具有 ::backdrop 伪元素。顶层中的所有元素都会获得可设置样式的 ::backdrop 伪元素。此示例使用降低了 alpha 值的背景颜色和背景滤镜来设置 ::backdrop 的样式,从而模糊处理底层内容。

设置弹出式窗口的样式

接下来,我们来了解如何设置弹出式窗口的样式。默认情况下,弹出式窗口具有固定位置和一些已应用的内边距。它还具有 display: none。您可以替换此设置以显示弹出式界面。但这样不会将其提升到顶层。

[popover] { display: block; }

无论您以何种方式宣传弹出式窗口,一旦将弹出式窗口宣传到顶层,您可能需要对其进行布局或定位。您无法定位到顶层并执行以下操作

:open {
  display: grid;
  place-items: center;
}

默认情况下,弹出式窗口将使用 margin: auto 在视口的中心进行布局。不过,在某些情况下,您可能需要明确指定位置。例如:

[popover] {
  top: 50%;
  left: 50%;
  translate: -50%;
}

如果您想使用 CSS 网格或 flexbox 在弹出式窗口内布局内容,最好将内容封装在元素中。否则,您需要声明一个单独的规则,以便在弹出式窗口位于顶层时更改 display。如果默认设置此属性,则会默认显示该元素,从而替换 display: none

[popover]:open {
 display: flex;
}

如果您试用了该演示,会发现弹出式信息框现在会过渡显示和隐藏。您可以使用 :open 伪选择器使弹出式窗口显示和隐藏。:open 伪选择器用于匹配正在显示(因此位于顶层)的弹出式提示框。

此示例使用自定义属性来驱动过渡。您还可以为弹出式窗口的 ::backdrop 应用过渡效果。

[popover] {
  --hide: 1;
  transition: transform 0.2s;
  transform: translateY(calc(var(--hide) * -100vh))
            scale(calc(1 - var(--hide)));
}

[popover]::backdrop {
  transition: opacity 0.2s;
  opacity: calc(1 - var(--hide, 1));
}


[popover]:open::backdrop  {
  --hide: 0;
}

一个技巧是,将过渡效果和动画效果归入运动媒体查询中。这也有助于保持您的跑步节奏。这是因为您无法通过自定义属性在 popover::backdrop 之间共享值。

@media(prefers-reduced-motion: no-preference) {
  [popover] { transition: transform 0.2s; }
  [popover]::backdrop { transition: opacity 0.2s; }
}

到目前为止,您已经了解了如何使用 popovertoggletarget 显示弹出式窗口。为了关闭它,我们使用了“轻触即关”。不过,您还可以使用 popovershowtargetpopoverhidetarget 属性。我们来向弹出式窗口添加一个按钮,用于隐藏该窗口,并将切换按钮更改为使用 popovershowtarget

<div id="code-popover" popover>
  <button popoverhidetarget="code-popover">Hide Code</button>
</div>
<button popovershowtarget="code-popover">Reveal Code</button>

如前所述,Popover API 不仅涵盖我们之前所说的弹出式窗口。您可以针对所有类型的场景(例如通知、菜单、提示等)进行构建。

其中一些场景需要不同的互动模式。悬停等互动。我们曾尝试使用 popoverhovertarget 属性,但目前尚未实现。

<div popoverhovertarget="hover-popover">Hover for Code</div>

其理念是,当您将鼠标悬停在某个元素上时,系统会显示目标。此行为可通过 CSS 属性进行配置。这些 CSS 属性将定义悬停在元素上和离开元素时弹出式窗口的响应时间窗口。实验中使用的默认行为是在明确点击 :hover0.5s 后显示弹出式窗口。然后,它需要通过轻触关闭或打开另一个弹出式窗口来关闭(稍后会详细介绍)。这是因为弹出式窗口的隐藏时长设置为 Infinity

在此期间,您可以使用 JavaScript 来填充该功能。

let hoverTimer;
const HOVER_TRIGGERS = document.querySelectorAll("[popoverhovertarget]");
const tearDown = () => {
  if (hoverTimer) clearTimeout(hoverTimer);
};
HOVER_TRIGGERS.forEach((trigger) => {
  const popover = document.querySelector(
    `#${trigger.getAttribute("popoverhovertarget")}`
  );
  trigger.addEventListener("pointerenter", () => {
    hoverTimer = setTimeout(() => {
      if (!popover.matches(":open")) popover.showPopover();
    }, 500);
    trigger.addEventListener("pointerleave", tearDown);
  });
});

设置明确的悬停窗口的好处在于,它可以确保用户的操作是有意为之(例如,用户将指针移到目标上)。除非用户有意显示该弹出式窗口,否则我们不希望显示它。

不妨试用此演示,其中您可以将窗口设置为 0.5s,然后将鼠标悬停在目标上。


在探讨一些常见的使用场景和示例之前,我们先来了解一些事项。


弹出式窗口的类型

我们已介绍过非 JavaScript 互动行为。但从整体来看,Popover 行为又如何呢?如果您不想使用“轻触即关闭”功能,该怎么办?或者您想将单例模式应用于弹出式窗口?

借助 Popover API,您可以指定三种行为不同的弹出式窗口。

[popover=auto]/[popover]

  • 嵌套支持。这不仅意味着嵌套在 DOM 中。祖代弹出式窗口的定义是:
    • 按 DOM 位置(子级)相关联。
    • 通过子元素(例如 popovertoggletargetpopovershowtarget 等)上的触发属性相关联。
    • 通过 anchor 属性相关联(正在开发的 CSS Anchoring API)。
  • 轻关闭。
  • 打开时会关闭其他非祖先 popover 的 popover。不妨试用下面的演示,了解如何使用祖先弹出式窗口进行嵌套。了解将部分 popoverhidetarget/popovershowtarget 实例更改为 popovertoggletarget 会带来哪些变化。
  • 如果关闭一个通知,系统会关闭所有通知;但如果关闭堆栈中的一个通知,系统只会关闭堆栈中位于该通知上方的通知。

[popover=manual]

  • 不关闭其他弹出式窗口。
  • 无灯光关闭。
  • 需要通过触发元素或 JavaScript 明确关闭。

JavaScript API

如果您需要更好地控制弹出式窗口,可以使用 JavaScript 来实现。您将获得 showPopoverhidePopover 方法。您还可以监听 popovershowpopoverhide 事件:

显示弹出式窗口 js popoverElement.showPopover() 隐藏弹出式窗口:

popoverElement.hidePopover()

监听正在显示的弹出式窗口:

popoverElement.addEventListener('popovershow', doSomethingWhenPopoverShows)

监听正在显示的 popover 并取消其显示:

popoverElement.addEventListener('popovershow',event => {
  event.preventDefault();
  console.warn(We blocked a popover from being shown);
})

监听弹出式窗口是否被隐藏:

popoverElement.addEventListener('popoverhide', doSomethingWhenPopoverHides)

您无法取消正在隐藏的弹出式窗口:

popoverElement.addEventListener('popoverhide',event => {
  event.preventDefault();
  console.warn("You aren't allowed to cancel the hiding of a popover");
})

检查弹出式窗口是否位于顶层:

popoverElement.matches(':open')

这可为一些不太常见的场景提供额外的动力。例如,在用户闲置一段时间后显示弹出式提示框。

此演示包含带有可听到的爆破音的弹出式窗口,因此我们需要使用 JavaScript 来播放音频。点击时,我们会隐藏弹出式窗口,播放音频,然后再次显示弹出式窗口。

无障碍

Popover API 的设计理念是将无障碍功能放在首位。无障碍功能映射可根据需要将弹出式窗口与其触发元素相关联。这意味着,如果您使用 popovertoggletarget 等触发属性,则无需声明 aria-* 属性(例如 aria-haspopup)。

对于焦点管理,您可以使用 autofocus 属性将焦点移至弹出框内的元素。这与对话框相同,但返回焦点时有所不同,这是因为轻触即可关闭。在大多数情况下,关闭弹出式窗口会将焦点返回到之前获得焦点的元素。但如果点击的元素可以获得焦点,则在轻触关闭时,焦点会移至该元素。请参阅说明中的焦点管理部分

您需要打开此演示的“全屏版本”才能看到它的运行效果。

在此演示中,获得焦点的元素会显示绿色轮廓。尝试使用键盘在界面中切换。请注意,当 popover 关闭时,焦点会返回到哪里。您可能还会注意到,如果您按 Tab 键四处移动,弹出式窗口会关闭。这是设计所致。尽管弹出式窗口具有焦点管理功能,但它们不会捕获焦点。当焦点移出弹出式窗口时,键盘导航会识别到关闭信号。

锚定(开发中)

对于弹出式窗口,需要考虑的一个棘手模式是将元素锚定到其触发器。例如,如果工具提示设置为显示在其触发器上方,但文档被滚动。该提示可能会被视口截断。目前有 JavaScript 产品可用于处理此问题,例如“Floating UI”。它们会重新定位提示,以防止这种情况发生,并依赖于所需的定位顺序。

不过,我们希望您能够通过样式来定义这一点。我们正在开发一个与 Popover API 并行的配套 API 来解决此问题。借助“CSS Anchor Positioning”API,您可以将元素绑定到其他元素,并且会以重新定位元素的方式来确保元素不会被视口截断。

此演示使用处于当前状态的 Anchoring API。船的位置会根据锚点在视口中的位置而变化。

以下是使此演示正常运行的 CSS 代码段。无需 JavaScript。

.anchor {
  --anchor-name: --anchor;
}
.anchored {
  position: absolute;
  position-fallback: --compass;
}
@position-fallback --compass {
  @try {
    bottom: anchor(--anchor top);
    left: anchor(--anchor right);
  }
  @try {
    top: anchor(--anchor bottom);
    left: anchor(--anchor right);
  }
}

您可以点击此处查看规范。此外,还将为此 API 提供 Polyfill。

示例

现在,您已经了解了弹出式窗口的功能和使用方法,接下来我们来深入了解一些示例。

通知

此演示显示了“复制到剪贴板”通知。

  • 使用 [popover=manual]
  • 在操作时显示包含 showPopover 的弹出式窗口。
  • 2000ms 超时后,使用 hidePopover 将其隐藏。

消息框

此演示使用顶层来显示消息框样式的通知。

  • 一个类型为 manual 的弹出式窗口充当容器。
  • 新通知会附加到弹出式窗口,并显示该弹出式窗口。
  • 它们会在点击时通过 Web 动画 API 移除,并从 DOM 中移除。
  • 如果没有要显示的 Toast,则隐藏弹出式窗口。

嵌套菜单

此演示展示了嵌套导航菜单的运作方式。

  • 使用 [popover=auto],因为它支持嵌套的弹出式信息框。
  • 在每个下拉菜单的第一个链接上使用 autofocus,以便通过键盘进行导航。
  • 这非常适合使用 CSS Anchoring API。不过,在此演示中,您可以使用少量 JavaScript 通过自定义属性更新位置。
const ANCHOR = (anchor, anchored) => () => {
  const { top, bottom, left, right } = anchor.getBoundingClientRect();
  anchored.style.setProperty("--top", top);
  anchored.style.setProperty("--right", right);
  anchored.style.setProperty("--bottom", bottom);
  anchored.style.setProperty("--left", left);
};

PRODUCTS_MENU.addEventListener("popovershow", ANCHOR(PRODUCT_TARGET, PRODUCTS_MENU));

请注意,由于此演示使用 autofocus,因此需要以“全屏视图”打开,才能使用键盘进行导航。

媒体弹出式窗口

此演示展示了如何弹出媒体。

  • 使用 [popover=auto] 进行轻触关闭。
  • JavaScript 会监听视频的 play 事件,并弹出视频。
  • 弹出式窗口的 popoverhide 事件会暂停视频。

Wiki 风格的弹出式提示框

此演示展示了如何创建包含媒体的内嵌内容提示。

  • 使用[popover=auto]。显示一个会隐藏其他,因为它们不是祖先。
  • 使用 JavaScript 在 pointerenter 上显示。
  • CSS Anchoring API 的另一个完美候选对象。

此演示使用弹出式窗口创建抽屉式导航栏。

  • 使用 [popover=auto] 进行轻触关闭。
  • 使用 autofocus 将焦点放在第一个导航项上。

管理背景

此演示展示了如何管理多个弹出框的背景,其中您只希望一个 ::backdrop 可见。

  • 使用 JavaScript 维护可见的弹出式窗口的列表。
  • 将类名称应用于顶层中的最低弹出式窗口。

自定义光标弹出式窗口

此演示展示了如何使用 popovercanvas 提升到顶层,并使用它来显示自定义光标。

  • 使用 showPopover[popover=manual]canvas 提升到顶层。
  • 当其他弹出式窗口打开时,隐藏并显示 canvas 弹出式窗口,以确保它位于顶部。

操作表弹出式窗口

此演示展示了如何将弹出式窗口用作操作表。

  • 默认显示弹出式窗口,覆盖 display
  • 使用弹出式窗口触发器打开操作表单。
  • 当弹出式窗口显示时,它会被提升到顶层并平移到视图中。
  • 可以使用轻触关闭手势返回。

键盘已激活的弹出式窗口

此演示展示了如何将弹出式窗口用于命令面板样式的界面。

  • 使用 cmd + j 显示弹出式窗口。
  • 使用 autofocus 聚焦 input
  • 组合框是位于主输入下方的第二个 popover
  • 如果不存在下拉菜单,则轻触关闭会关闭调色板。
  • 锚定 API 的另一个候选对象

定时浮层

此演示会在 4 秒后显示闲置弹出式窗口。一种界面模式,通常用于包含用户安全信息的应用,以显示退出模式。

  • 使用 JavaScript 在一段时间不活动后显示弹出式提示框。
  • 在显示弹出式窗口时,重置计时器。

屏保

与之前的演示版类似,您可以在网站上添加一些奇思妙想,并添加屏保。

  • 使用 JavaScript 在一段时间不活动后显示弹出式提示框。
  • 轻触关闭即可隐藏并重置计时器。

光标跟随

此演示展示了如何让弹出式窗口跟随输入光标。

  • 根据选择、按键事件或特殊字符输入显示弹出式窗口。
  • 使用 JavaScript 通过范围限定的自定义属性更新弹出式窗口位置。
  • 此模式需要仔细考虑所显示的内容和无障碍功能。
  • 在文本编辑界面和可添加标记的应用中,此功能非常常见。

悬浮操作按钮菜单

此演示展示了如何使用 popover 实现不含 JavaScript 的悬浮操作按钮菜单。

  • 使用 showPopover 方法推广 manual 类型弹出式窗口。这是主按钮。
  • 菜单是另一个弹出式界面,也是主按钮的目标。
  • 使用 popovertoggletarget 打开菜单。
  • 使用 autofocus 将焦点置于显示中的第一个菜单项上。
  • 轻触关闭菜单。
  • 图标扭曲使用 :has()。如需详细了解 :has(),请参阅这篇文章

大功告成!

以上是有关弹出式窗口的简介,该功能即将推出,是开放式界面计划的一部分。如果合理使用,它将成为 Web 平台的绝佳补充。

请务必查看开放界面。随着 API 的发展,弹出式信息说明也会随之更新。以下是所有演示的集合

感谢您的光临!