弹出式窗口:正在复苏!

开放式界面计划的目标是让开发者更轻松地打造出色的用户体验。为此,我们正努力解决开发者面临的更棘手的问题。为此,我们可以提供更好的平台内置 API 和组件。

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

弹出式窗口长期以来一直存在两极分化的声誉。这在一定程度上是因为它们的构建和部署方式。这种模式并不容易构建得当,但可以引导用户执行特定操作或让他们了解您网站上的内容,从而产生巨大价值,尤其是在以恰当的方式使用时。

构建弹出式窗口时,通常有两个主要问题:

  • 如何确保将其放置在适当的位置,使其位于其他内容之上。
  • 如何使其可访问(键盘友好、可聚焦等)。

内置的 Popup API 具有多种目标,但所有这些目标都具有一个共同的总体目标,即让开发者能够轻松构建此模式。其中一些目标包括:

  • 轻松在文档的其余部分上方显示某个元素及其子元素。
  • 使其可访问。
  • 大多数常见行为(轻量关闭、单例、堆叠等)无需 JavaScript。

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

浏览器兼容性

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

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

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

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

您可以通过以下方式检查弹出式窗口支持情况:

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

当前解决方案

您目前可以做些什么来让自己的内容脱颖而出?如果您的浏览器支持该元素,您可以使用 HTML 对话框元素。您需要在“模态”表单中使用它。此功能需要 JavaScript 才能使用。

Dialog.showModal();

请注意以下无障碍功能注意事项。例如,如果要为版本低于 15.4 的 Safari 用户提供服务,建议使用 a11y-dialog

您还可以使用市面上众多基于弹出式窗口、提醒或提示的库之一。其中许多工具的工作方式都类似。

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

这需要额外的依赖项,并且开发者需要做出更多决策。您还需要进行研究,找到能够提供您所需的一切服务的方案。Popover API 旨在适应许多场景,包括提示。目标是涵盖所有这些常见场景,让开发者无需再做出其他决策,从而专注于构建体验。

您的第一个弹出式窗口

就这么简单。

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

但这里发生了什么?

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

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

  • 您可以直接使用“轻触关闭”功能。也就是说,您可以使用关闭信号关闭弹出式窗口,例如点击弹出式窗口外部、使用键盘导航到其他元素或按 Esc 键。再次打开并试用吧!

借助弹出式窗口,您还可以获得哪些好处?我们继续使用以下示例。请考虑以下在页面上显示了一些内容的演示。

该悬浮操作按钮采用固定定位,z-index 较高。

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

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

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

设置弹出式窗口的样式

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

[popover] { display: block; }

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

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

默认情况下,系统会使用 margin: auto 在视口中心布局弹出式窗口。但在某些情况下,您可能需要明确说明定位。例如:

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

如果您想使用 CSS 网格或 Flexbox 在弹出式窗口中排列内容,不妨将其封装在元素中。否则,您需要声明一个单独的规则,以便在 popover 位于顶层时更改 display。默认设置它会默认显示它,并替换 display: none

[popover]:open {
 display: flex;
}

如果您试用过该演示,就会发现现在,该弹出式窗口会进行进入和退出转换。您可以使用 :open 伪类选择器来实现推弹式窗口的显示和隐藏过渡效果。:open 伪选择器会匹配正在显示(因此位于顶层)的弹出式窗口。

此示例使用自定义属性来驱动转换。您还可以对 popover 的 ::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 显示 popover。如需关闭它,我们将使用“轻量关闭”。不过,您还可以使用 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 属性会定义悬停在某个元素上和离开该元素的时间范围,系统会根据该时间范围来确定是否显示弹出式窗口。在实验中,默认行为是在 :hover 的显式 0.5s 之后显示一个弹出式窗口。然后,需要轻轻关闭或打开另一个弹出式窗口才能关闭(后面会详细介绍)。这是因为我们将 popover 隐藏时长设置为 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 API,您可以指定三种行为不同的 popover。

[popover=auto]/[popover]

  • 嵌套支持。这也不只是指嵌套在 DOM 中。祖先 popover 的定义如下:
    • 通过 DOM 位置(子元素)相关联。
    • 通过在子元素(例如 popovertoggletargetpopovershowtarget 等)上触发属性相关联。
    • 通过 anchor 属性相关联(CSS 锚点 API 正在开发中)。
  • 轻关闭。
  • 打开此类窗口会关闭所有非祖先窗口。请试用下面的演示,了解如何使用祖先 popover 进行嵌套。了解将某些 popoverhidetarget/popovershowtarget 实例更改为 popovertoggletarget 会产生什么影响。
  • 轻触关闭其中一个会关闭所有通知,但关闭堆叠中的某个通知只会关闭堆叠中位于其上方的通知。
这个 Pen

[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 时,我们将无障碍功能放在首位。无障碍功能映射会根据需要将 popover 与其触发器元素相关联。这意味着,假设您使用了 popovertoggletarget 等触发属性,则无需声明 aria-* 属性(例如 aria-haspopup)。

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

您需要打开此演示的全屏版本,才能查看其运作方式。

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

锚定(正在开发中)

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

不过,我们希望您能够通过样式来定义此属性。我们正在开发一个与 Popover API 配套的 API 来解决此问题。借助“CSS 锚点定位”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],因为它允许嵌套的 popover。
  • 对每个下拉菜单的第一个链接使用 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 弹出式窗口,确保其位于顶部。

操作表弹出式窗口

此演示展示了如何将 popover 用作动作条。

  • 默认显示的弹出式窗口会替换 display
  • 使用 popover 触发器打开动作条。
  • 当显示 popover 时,系统会将其提升到顶层并转换为视图。
  • 可以使用轻量关闭功能返回它。

通过键盘激活的弹出式窗口

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

  • 使用 cmd + j 显示弹出式窗口。
  • input 使用 autofocus 进行聚焦。
  • 下拉菜单是位于主输入下方的第二个 popover
  • 如果没有下拉菜单,轻触关闭会关闭 Palette。
  • 另一个 Anchoring API 候选项

定时弹出式窗口

此演示会在四秒钟后显示闲置时间提醒弹出式窗口。此界面模式通常用于存储用户安全信息的应用,用于显示退出模态。

  • 使用 JavaScript 在用户闲置一段时间后显示该弹出式窗口。
  • 在弹出式窗口显示时,重置计时器。
这个 Pen

屏保

与上一个演示类似,您可以为自己的网站增添一些奇思妙想,并添加屏保。

  • 使用 JavaScript 在用户闲置一段时间后显示该弹出式窗口。
  • 轻触关闭即可隐藏和重置计时器。

光标跟随

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

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

悬浮操作按钮菜单

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

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

大功告成!

以上就是有关弹出式窗口的简介,我们将在未来推出的“开放式界面”计划中进一步介绍。只要合理使用,它将成为 Web 平台的绝佳补充。

请务必查看打开界面。随着 API 的不断演变,弹出式窗口说明也会随之更新。以下是所有演示的集合

感谢您光临!


照片由 Madison Oren 拍摄,选自 Unsplash