弹出式窗口:正在复苏!

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

弹出式窗口就属于此类问题,在“开放界面”中称为“弹出式窗口”。

在过去很长一段时间里,弹出式窗口的名声颇具两极分化。这在一定程度上得益于它们的构建和部署方式。构建这种设计并不容易,但是通过引导用户去做某些事情,或者让他们知道您网站上的内容,尤其是以得心应手的方式使用时,它们可以创造很多价值。

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

  • 如何确保将广告放置在其他内容上方合适的位置。
  • 如何使其可访问(支持键盘、可聚焦等)。

内置的 Popover API 有多个目标,所有目标都有一个相同的总体目标,即让开发者能够轻松构建此模式。其中值得注意的目标包括:

  • 让元素及其后代显示在文档的其余部分之上。
  • 增强网站适应性。
  • 对于大多数常见行为(轻度关闭、单例、堆叠等),不要求使用 JavaScript。

您可以访问 OpenUI 网站,查看弹出式窗口的完整规范。

浏览器兼容性

现在,您可以在哪里使用内置的 Popover API?在撰写本文时,Chrome Canary 版会借助“实验性网络平台功能”标记来支持此功能。

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

如果开发者想要在生产环境中进行测试,我们还提供了源试用功能。

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

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

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

当前解决方案

为了将内容放在首位,您目前可以采取哪些措施?如果浏览器支持,您可以使用 HTML Dialog 元素。您需要以“模态”形式使用它。而这需要用到 JavaScript。

Dialog.showModal();

以下是一些无障碍功能注意事项。例如,如果适合使用低于 15.4 版本的 Safari 用户,建议使用 a11y-dialog

您也可以使用众多基于弹出式窗口、提醒或提示的库之一。其中许多工具的运作方式都大同小异。

  • 向正文附加一些容器,以显示弹出式窗口。
  • 设置样式,使其位于一切最上面。
  • 创建一个元素并将其附加到容器,以显示弹出式窗口。
  • 您可以通过从 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 伪元素。此示例为 ::backdrop 设置了较少的 alpha 背景颜色和背景幕滤镜,对底层内容进行模糊处理。

设置弹出式窗口的样式

让我们将注意力转到弹出式窗口的样式上。默认情况下,弹出式窗口具有固定的位置和一些应用的内边距。它还具有 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 属性将定义窗口悬停时间窗口,以及弹出式窗口会做出响应的元素。实验的默认行为在显式 0.5s:hover 后显示弹出式窗口。然后,需要关闭对话框或打开另一个弹出式窗口才能关闭(稍后会详细介绍)。这是由于弹出式窗口隐藏时长设置为 Infinity 所致。

在此期间,您可以使用 JavaScript 对该功能进行 polyfill。

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=auto]/[popover]:

  • 嵌套支持。这并不仅仅意味着要嵌套在 DOM 中。原始弹出式窗口的定义如下:
    • 与 DOM 位置(子)相关联。
    • 通过触发 popovertoggletargetpopovershowtarget 等子元素上的属性来与其关联。
    • anchor 属性相关(正在开发中的 CSS Anchoring API)。
  • 轻关闭。
  • 打开会关闭其他非原始弹出式窗口的弹出式窗口。看看下面的演示,看看使用祖先弹出式窗口进行嵌套的工作原理。了解将某些 popoverhidetarget/popovershowtarget 实例更改为 popovertoggletarget 会如何改变。
  • 轻击关闭一个会全部关闭,但关闭堆栈中的另一个仅关闭堆栈中在其之上的层。

[popover=manual]:

  • 不会关闭其他弹出式窗口。
  • 无指示灯关闭。
  • 要求通过触发器元素或 JavaScript 明确关闭。

JavaScript API

当您需要更好地控制弹出式窗口时,可以使用 JavaScript 来实现。您会同时获得 showPopoverhidePopover 方法。您还有要监听的 popovershowpopoverhide 事件:

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

popoverElement.hidePopover()

监听显示弹出式窗口:

popoverElement.addEventListener('popovershow', doSomethingWhenPopoverShows)

监听显示的弹出式窗口并取消显示:

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-haspopuparia-* 属性。

对于焦点管理,您可以使用自动对焦属性将焦点移到弹出式窗口内的元素上。这与 Dialog 相同,但出现不同之处在于返回焦点时,原因在于灯光关闭。在大多数情况下,关闭弹出式窗口会将焦点返回到之前聚焦的元素。但是,如果点击元素可以获得焦点,则焦点会移至被点击的元素上。请参阅铺垫消息中的关于焦点管理的部分

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

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

锚定(开发中)

就弹出式窗口而言,需要满足一个棘手的需要,那就是将元素锚定到其触发器。例如,如果提示设置为在其触发器上方显示,但文档进行了滚动。该提示可能会被视口截断。有现成的 JavaScript 产品(例如“浮动界面”)可以处理此问题。它们将会调整提示的位置,以便您阻止这种情况发生,并使用所需的排名顺序。

但是,我们希望您能够使用样式定义这一点。有一个与 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 的 Action 显示弹出式窗口。
  • 2000ms 超时后,使用 hidePopover 将其隐藏。

消息框

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

  • 一个类型为 manual 的弹出式窗口可充当容器。
  • 系统会将新通知附加到弹出式窗口,并显示弹出式窗口。
  • 它们会在点击时通过网络动画 API 移除,并从 DOM 中移除。
  • 如果没有可显示的消息框,则会隐藏弹出式窗口。

嵌套菜单

此演示展示了嵌套导航菜单的工作原理。

  • 请使用 [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 弹出式窗口,确保它位于顶部。

Actionsheet 弹出式窗口

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

  • 默认情况下显示弹出式窗口,替换 display
  • Actionsheet 通过弹出式窗口触发器打开。
  • 当显示弹出式窗口时,它会提升到顶层并转换为视图。
  • 轻关闭可用于返回。

键盘已启用的弹出式窗口

此演示版展示了如何为命令调色板样式的界面使用弹出式窗口。

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

定时弹出式窗口

此演示显示了 4 秒后显示非活跃状态的弹出式窗口。一种界面模式,通常用于保存用户安全信息以显示退出模式的应用。

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

屏保

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

  • 使用 JavaScript 在一段时间不活动后显示弹出式窗口。
  • 轻关闭以隐藏和重置计时器。

插入符号跟随

此演示展示了如何使弹出式窗口在文本插入符号后面显示。

  • 根据选择、键事件或特殊字符输入显示弹出式窗口。
  • 使用 JavaScript 通过限定了范围的自定义属性更新弹出式窗口位置。
  • 若采用这种模式,则需要慎重考虑呈现的内容和无障碍设计。
  • 通常会出现在文本编辑界面和可以添加标签的应用中。

悬浮操作按钮菜单

此演示版展示了如何在不使用 JavaScript 的情况下使用弹出式窗口实现悬浮操作按钮菜单。

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

就是这样!

以上就是对弹出式窗口的介绍,我们即将在 Open UI 计划中推出该计划。使用得当,它将成为网络平台的优秀补充。

请务必查看开放式界面弹出式窗口解释器会随着 API 的发展而保持最新状态。此处提供了所有演示的集合

感谢你的“热门”!


照片由 Madison Oren 拍摄,选自 Unsplash 网站