构建对话框组件

简要介绍如何使用 <dialog> 元素构建颜色自适应、响应迅速且易于使用的迷你模态和超级模态。

在这篇博文中,我想分享我对于如何使用 <dialog> 元素构建颜色自适应、响应迅速且易于访问的迷你模态和大型模态的看法。试用演示版查看源代码

演示了采用浅色和深色主题的大型对话框和迷你对话框。

如果你更喜欢视频,可以参考本博文的 YouTube 版本:

概览

<dialog> 元素非常适合用于显示页内上下文信息或操作。考虑何时可以从同一页面操作(而不是多页面操作)中改善用户体验:这可能是因为表单很小,或者用户只需要确认或取消。

<dialog> 元素最近已在各个浏览器中稳定:

浏览器支持

  • 37
  • 79
  • 98
  • 15.4

来源

我发现该元素缺少一些内容,因此在这个 GUI 挑战中,我添加了我期望的开发者体验项:其他事件、轻度关闭、自定义动画以及迷你和超级类型。

Markup

<dialog> 元素的基本要素不大。该元素会自动隐藏,并内置样式来叠加您的内容。

<dialog>
  …
</dialog>

我们可以改进此基准。

传统上,对话框元素与模态许多相同,并且名称通常可以互换。我在这里随心所欲地将 dialog 元素用于小型对话框弹出式窗口(迷你对话框)和整页对话框(大型对话框)。我将这两个对话框分别命名为“mega”和“mini”,这两个对话框针对不同的用例进行了细微调整。我添加了 modal-mode 属性,以便您指定类型:

<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>

浅色主题和深色主题下的迷你对话框和超级对话框的屏幕截图。

并非总是如此,但对话框元素通常会用于收集一些互动信息。对话框元素内的表单是组合在一起的。最好使用表单元素封装对话框内容,以便 JavaScript 可以访问用户输入的数据。此外,使用 method="dialog" 的表单内的按钮可以关闭不含 JavaScript 的对话框并传递数据。

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

超级对话框

超级对话框的表单内包含三个元素:<header><article><footer>。它们充当语义容器,也是对话框呈现方式的样式目标。标题为模态窗口的标题,并提供一个关闭按钮。本文介绍表单输入和信息。页脚包含操作按钮的 <menu>

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    <header>
      <h3>Dialog title</h3>
      <button onclick="this.closest('dialog').close('close')"></button>
    </header>
    <article>...</article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

第一个菜单按钮具有 autofocusonclick 内嵌事件处理脚本。当对话框打开时,autofocus 属性会获得焦点,我发现最好是将其放在取消按钮上,而不是放在确认按钮上。这样可以确保确认是有意为之,而非意外确认。

迷你对话框

迷你对话框与大型对话框非常相似,只不过缺少一个 <header> 元素。这样,其更小且更内嵌。

<dialog id="MiniDialog" modal-mode="mini">
  <form method="dialog">
    <article>
      <p>Are you sure you want to remove this user?</p>
    </article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

对话框元素为可收集数据和用户互动的完整视口元素奠定了坚实基础。这些基本功能可以在您的网站或应用中 促成一些非常有趣且有效的互动

无障碍功能

dialog 元素具有非常出色的内置无障碍功能。目前已经添加了许多功能,我并不像我平时那样添加这些功能。

正在恢复焦点

正如我们在构建侧边导航组件时手动所做的那样,正确打开和关闭某些内容时要将焦点放在相关的打开和关闭按钮上,这一点非常重要。当该侧边导航栏打开时,焦点将位于关闭按钮上。当用户按下“关闭”按钮后,焦点会恢复为打开该按钮的按钮。

使用 dialog 元素时,这是内置的默认行为:

遗憾的是,如果您想要为对话框添加动画效果和关闭动画,此功能会丢失。在 JavaScript 部分中,我将恢复该功能。

陷阱专注

dialog 元素会为您管理文档的 inert。在 inert 之前,JavaScript 用于监视焦点离开某个元素,然后拦截焦点并将其放回。

浏览器支持

  • 102
  • 102
  • 112
  • 15.5

来源

inert 之后,文档的任何部分都可以“冻结”,以至于它们不再是焦点目标,或者不再是使用鼠标来互动。系统不会限制焦点,而是将焦点引导到文档中唯一一个可互动的部分。

打开并自动聚焦某个元素

默认情况下,对话框元素会将焦点分配给对话框标记中的第一个可聚焦元素。如果这不是用户默认使用的最佳元素,请使用 autofocus 属性。如前所述,我发现最佳实践是将其放置在取消按钮上,而不是确认按钮上。这样可以确保确认是有意为之,而非意外确认。

使用 Esc 键关闭

请务必让用户能够轻松关闭这个可能会干扰性的元素。幸运的是,dialog 元素将为您处理 Esc 键,从编排中解脱出来。

风格

设置对话框元素样式的路径有一种简单路径,也有硬路径。通过不更改对话框的显示属性并处理其限制,可以实现简单路径。我沿着硬路线提供用于打开和关闭对话框的自定义动画,接管 display 属性等。

使用开放道具设置样式

为了加快自适应颜色的开发速度和提升整体设计一致性,我不客气地引入了我的 CSS 变量库 Open Props。除了免费提供的变量之外,我还导入了一个标准化文件和一些按钮,这两个按钮均由 Open Props 作为可选导入项提供。这些导入有助于我专注于自定义对话框和演示,而不需要太多样式来支持并使其看起来美观。

设置 <dialog> 元素的样式

拥有 display 属性

对话框元素的默认显示和隐藏行为会将显示属性从 block 切换为 none。很遗憾,这意味着无法播放动画,只能显示进出动画。我想为输入和输出添加动画效果,第一步是设置我自己的 display 属性:

dialog {
  display: grid;
}

如上面的 CSS 代码段所示,通过更改 display 属性值并因此拥有该值,您需要管理大量的样式,以便提供适当的用户体验。首先,对话框的默认状态为关闭。您可以直观地表示此状态,并防止对话框接收以下样式的互动:

dialog:not([open]) {
  pointer-events: none;
  opacity: 0;
}

现在,对话框不可见,并且在未打开时无法与其交互。稍后,我将添加一些 JavaScript 来管理对话框上的 inert 属性,以确保键盘和屏幕阅读器用户也无法访问隐藏的对话框。

为对话框提供自适应颜色主题

显示浅色和深色主题的大型对话框,演示 Surface 颜色。

虽然 color-scheme 会为您的文档选择启用浏览器提供的自适应颜色主题,以适应浅色和深色系统偏好设置,但我更希望自定义对话框元素。Open Props 提供几种表面颜色,可自动适应浅色和深色系统偏好设置,与使用 color-scheme 类似。这些图层非常适合在设计中创建图层,我喜欢使用颜色来帮助直观地支持图层表面的这种外观。背景颜色为 var(--surface-1);如需位于该图层的顶部,请使用 var(--surface-2)

dialog {
  …
  background: var(--surface-2);
  color: var(--text-1);
}

@media (prefers-color-scheme: dark) {
  dialog {
    border-block-start: var(--border-size-1) solid var(--surface-3);
  }
}

系统稍后将为子元素(例如页眉和页脚)添加更多自适应颜色。我认为它们对于对话元素来说是额外的,但在设计富有吸引力且精心设计的对话设计中尤为重要。

自适应对话框大小调整

该对话框默认将其大小委托给其内容,通常非常棒。我的目标是将 max-inline-size 限制为可读大小 (--size-content-3 = 60ch) 或视口宽度的 90%。这样可以确保对话框在移动设备上不会并排显示,而且在桌面设备屏幕上也不会因宽度过宽而难以阅读。然后,我添加一个 max-block-size,使对话框不会超过页面的高度。这也意味着,我们需要指定对话框的可滚动区域的位置,以防对话框元素较高。

dialog {
  …
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

注意到 max-block-size 出现了两次吗?第一种方法使用物理视口单元 80vh。我真正想要的是让对话保持在相对流动中,以便面向国际用户。因此,当它变得更稳定时,我在第二个声明中使用逻辑更新且仅部分支持的 dvb 单元。

超级对话框定位

为了帮助定位对话框元素,建议拆分其两个部分:全屏背景幕和对话框容器。背景必须覆盖所有内容,提供阴影效果,以帮助确保对话框在前面,而后面的内容无法访问。对话框容器可以自由地将自身置于此背景幕上,并采用其内容所需的任何形状。

以下样式会将对话框元素固定到窗口,将其拉伸到每个角,并使用 margin: auto 将内容居中:

dialog {
  …
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
移动设备大型对话框样式

在小视口上,这个整页超级模态的样式会稍有不同。我将下外边距设为 0,这会将对话框内容置于视口底部。通过进行几项样式调整,我可以将对话框变成操作表,使其更靠近用户的拇指:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

处于打开状态时,开发者工具在桌面设备和移动设备的大型对话框上叠加外边距间距的屏幕截图。

迷你对话框定位

使用较大的视口时(例如在桌面设备上),我选择将迷你对话框放置在调用它们的元素上方。为此,我需要 JavaScript。您可以在此处找到我使用的方法,但我认为这超出了本文的范围。如果没有 JavaScript,迷你对话框像大型对话框一样显示在屏幕中心。

让应用变得流行

最后,为对话框添加一些装饰,使其看起来像是位于页面上方的柔和表面。柔化度是通过对对话框的角进行圆角处理来实现的。深度是通过 Open Props 精心设计的一个影子道具来实现的:

dialog {
  …
  border-radius: var(--radius-3);
  box-shadow: var(--shadow-6);
}

自定义背景幕伪元素

我选择非常谨慎地处理背景幕,仅使用 backdrop-filter 向超级对话框添加模糊效果:

浏览器支持

  • 76
  • 17
  • 103
  • 9

来源

dialog[modal-mode="mega"]::backdrop {
  backdrop-filter: blur(25px);
}

我还选择了在 backdrop-filter 上设置过渡,希望浏览器将来允许对背景幕元素进行过渡:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

超级对话框的屏幕截图,叠加了彩色头像的模糊处理背景。

样式设置 extra

我将此部分称为“extra”,因为它与对话框元素演示有关,而不是与一般的对话框元素有关。

滚动包含

显示该对话框时,用户仍可滚动其后面的页面,我并不希望这样:

通常,overscroll-behavior 是我常用的解决方案,但根据相关规范,它对对话框没有影响,因为它不是滚动端口,也就是说,它不是滚动器,因此没有可阻止的措施。我可以使用 JavaScript 监控本指南中的新事件(例如“closed”和“opened”),并开启/关闭文档上的 overflow: hidden,也可以等待 :has() 在所有浏览器中都保持稳定:

浏览器支持

  • 105
  • 105
  • 121
  • 15.4

来源

html:has(dialog[open][modal-mode="mega"]) {
  overflow: hidden;
}

现在,当大型对话框打开时,html 文档会包含 overflow: hidden

<form> 布局

它不仅是收集用户互动信息的重要元素,在这里我还使用了它来布置页眉、页脚和文章元素。使用此布局,我打算将文章子级表示为可滚动区域。我使用 grid-template-rows 来实现这一点。将 article 元素指定为 1fr,并且表单本身与 dialog 元素的最大高度相同。设置这一固定高度和固定行大小后,文章元素可以受约束并在溢出时滚动:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

开发者工具将网格布局信息叠加在行上的屏幕截图。

设置对话框 <header> 的样式

此元素的作用是提供对话框内容的标题,并提供易于找到的关闭按钮。还指定了 Surface 颜色,使其看起来位于对话框文章内容的后面。满足这些要求后,我们创建了 Flexbox 容器、垂直对齐各项以让其边缘间隔开来,并留出一些内边距和间隙,以便为标题和关闭按钮提供一些空间:

dialog > form > header {
  display: flex;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  background: var(--surface-2);
  padding-block: var(--size-3);
  padding-inline: var(--size-5);
}

@media (prefers-color-scheme: dark) {
  dialog > form > header {
    background: var(--surface-1);
  }
}

Chrome Devtools 将 flexbox 布局信息叠加在对话框标题上的屏幕截图。

设置标题关闭按钮的样式

由于演示使用的是“Open Props”按钮,因此“关闭”按钮已自定义为以圆形图标为中心的按钮,如下所示:

dialog > form > header > button {
  border-radius: var(--radius-round);
  padding: .75ch;
  aspect-ratio: 1;
  flex-shrink: 0;
  place-items: center;
  stroke: currentColor;
  stroke-width: 3px;
}

Chrome 开发者工具的屏幕截图,其中显示了标头关闭按钮的尺寸和内边距信息。

设置对话框 <article> 的样式

文章元素在此对话框中具有特殊的作用:当对话框较长或较长时,文章元素是一个需要滚动的空间。

为此,父表单元素为自己设定了一些最大值,这些最大值为本文元素设定了当其过高时要达到的约束条件。设置 overflow-y: auto,这样滚动条就会仅在需要时显示,其中包含通过 overscroll-behavior: contain 进行的滚动,其余部分将采用自定义呈现样式:

dialog > form > article {
  overflow-y: auto; 
  max-block-size: 100%; /* safari */
  overscroll-behavior-y: contain;
  display: grid;
  justify-items: flex-start;
  gap: var(--size-3);
  box-shadow: var(--shadow-2);
  z-index: var(--layer-1);
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: light) {
  dialog > form > article {
    background: var(--surface-1);
  }
}

页脚的作用是包含操作按钮菜单。Flexbox 用于将内容与页脚内联轴的末端对齐,然后留出一些间距,以便为按钮留出一些空间。

dialog > form > footer {
  background: var(--surface-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: dark) {
  dialog > form > footer {
    background: var(--surface-1);
  }
}

Chrome Devtools 将 Flexbox 布局信息叠加在页脚元素上的屏幕截图。

menu 元素用于包含对话框的操作按钮。它使用带有 gap 的封装 Flexbox 布局来在按钮之间提供空间。菜单项具有内边距,例如 <ul>。我也移除了该样式,因为我不需要它。

dialog > form > footer > menu {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  padding-inline-start: 0;
}

dialog > form > footer > menu:only-child {
  margin-inline-start: auto;
}

Chrome Devtools 将 flexbox 信息叠加在页脚菜单元素上的屏幕截图。

动画

对话框元素通常带有动画效果,因为它们用于进入和退出窗口。为对话框提供一些用于进入和退出的辅助性动作,有助于用户在流程中确定自己的方向。

通常,dialog 元素只能以动画形式呈现,不能呈现出动画效果。这是因为浏览器会切换元素的 display 属性。之前,指南会将显示设置为网格,并且从未将其设置为无。这会解锁动画进出的功能。

Open Props 提供许多可用的关键帧动画,让编排变得简单清晰。下面是我采用的动画目标和分层方法:

  1. 减少动作是默认过渡,简单的不透明度淡入和淡出。
  2. 如果动态效果良好,则添加滑动和缩放动画。
  3. 超级对话框的自适应移动设备布局会调整为滑出。

安全且有意义的默认过渡

虽然 Open Props 附带用于淡入和淡出的关键帧,但我更喜欢这种分层过渡方法作为默认方式,并有可能将关键帧动画作为潜在的升级。之前,我们已使用不透明度设置对话框的可见性样式,并根据 [open] 属性编排了 10。如需在 0% 和 100% 之间转换,请告诉浏览器您想要的缓和时长以及类型:

dialog {
  transition: opacity .5s var(--ease-3);
}

向转场效果添加动画

如果用户对动作没有问题,大型对话框和迷你对话框都应作为进入屏幕而向上滑动,在用户退出时缩小。您可以使用 prefers-reduced-motion 媒体查询和一些 Open Props 来实现此目的:

@media (prefers-reduced-motion: no-preference) {
  dialog {
    animation: var(--animation-scale-down) forwards;
    animation-timing-function: var(--ease-squish-3);
  }

  dialog[open] {
    animation: var(--animation-slide-in-up) forwards;
  }
}

调整移动设备的退出动画

在样式部分的前面部分,超级对话框样式经过了调整,使移动设备更像是操作工作表,就好像一张小张纸从屏幕底部向上滑动后仍然贴在底部。横向扩容退出动画并不适合这一新设计,我们可以通过几项媒体查询和一些 Open Prop 进行调整:

@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    animation: var(--animation-slide-out-down) forwards;
    animation-timing-function: var(--ease-squish-2);
  }
}

JavaScript

使用 JavaScript 可以添加很多项:

// dialog.js
export default async function (dialog) {
  // add light dismiss
  // add closing and closed events
  // add opening and opened events
  // add removed event
  // removing loading attribute
}

这些新增功能源于对轻度关闭(点击对话框背景)的期望、动画和一些其他事件,以便更好地把握表单数据。

添加浅色关闭

此任务很简单,可以很好地补充无动画的对话框元素。互动的实现方式是观察对 dialog 元素的点击次数并利用事件气泡来评估用户点击的内容,并且仅当它是最顶部的元素时才会使用 close()

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

请注意 dialog.close('dismiss')。调用该事件并提供字符串。 其他 JavaScript 可以检索此字符串,以深入了解对话框的关闭方式。您会发现,每次从各种按钮调用该函数时,我都提供了结束字符串,以便为我的应用提供有关用户互动的上下文。

添加结束活动和已关闭事件

dialog 元素附带一个关闭事件:在调用对话框 close() 函数时,它会立即发出。由于我们要为此元素添加动画效果,因此最好在动画前后设置事件,以便进行更改以获取数据或重置对话框表单。在这里,我使用它来管理在关闭的对话框上添加 inert 属性的操作;在演示中,我会使用这些属性来在用户提交了新图片时修改头像列表。

为此,请创建两个分别名为 closingclosed 的新事件。然后监听对话框上的内置关闭事件。在此处,将对话框设置为 inert 并分派 closing 事件。下一项任务是等待对话框上的动画和过渡完成运行,然后分派 closed 事件。

const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')

export default async function (dialog) {
  …
  dialog.addEventListener('close', dialogClose)
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

构建消息框组件中使用的 animationsComplete 函数会根据动画和过渡 promise 的完成情况返回一个 promise。正因如此,dialogClose 是一个异步函数;然后,该函数可以 await 返回的 promise 并放心地继续处理已关闭的事件。

添加开业和已开业事件

这些事件不那么容易添加,因为内置对话框元素不像使用关闭时那样提供打开事件。我使用 MutationObserver 来深入了解对话框属性变化。在此观察器中,我将观察 open 属性的更改,并相应地管理自定义事件。

与我们启动关闭事件和关闭事件的方式类似,请创建两个名为 openingopened 的新事件。在我们之前监听对话框关闭事件的位置,这次使用已创建的突变观察器来监视对话框的属性。

…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')

export default async function (dialog) {
  …
  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })
}

const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

当对话框属性发生更改时,系统会调用变更观察器回调函数,以数组形式提供更改列表。遍历属性更改,查找 attributeName 处于打开状态。接下来,检查该元素是否具有该属性:这会通知对话框是否已打开。如果焦点已打开,请移除 inert 属性,将焦点设置为请求 autofocus 的元素或对话框中的第一个 button 元素。最后,与关闭事件和关闭事件类似,立即分派打开事件,等待动画结束,然后分派打开事件。

添加已移除的活动

在单页应用中,系统通常会根据路由或其他应用需求和状态添加和移除对话框。在对话框被移除时清理事件或数据会很有用。

您可以使用另一个变更观察器来实现此目的。这一次,我们将观察 body 元素的子元素,并观察对话框元素是否被移除,而不是观察 dialog 元素上的属性。

…
const dialogRemovedEvent = new Event('removed')

export default async function (dialog) {
  …
  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })
}

const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

每当在文档正文中添加或移除子项时,系统都会调用变更观察器回调。所监控的特定变更针对的是具有对话框 nodeNameremovedNodes。如果对话框被移除,系统会移除点击和关闭事件以释放内存,并分派已移除的自定义事件。

移除加载属性

为防止对话框动画在添加到网页或网页加载时播放其退出动画,系统已向对话框中添加了加载属性。以下脚本会等待对话框动画运行完毕,然后移除该属性。现在,对话框可以随意添加动画进出动画,并且我们有效隐藏了原本会分散注意力的动画。

export default async function (dialog) {
  …
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

如需详细了解在网页加载时阻止关键帧动画的问题,请点击此处。

所有产品

此处提供了完整的 dialog.js,现在我们已分别解释了每个部分:

// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')
const dialogRemovedEvent = new Event('removed')

// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

// wait for all dialog animations to complete their promises
const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

// page load dialogs setup
export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
  dialog.addEventListener('close', dialogClose)

  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })

  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })

  // remove loading attribute
  // prevent page load @keyframes playing
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

使用 dialog.js 模块

从模块中导出的函数应被调用并传递一个希望添加下列新事件和功能的对话框元素:

import GuiDialog from './dialog.js'

const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

就像这样,这两个对话框进行了升级,增加了轻度关闭、动画加载修复和更多支持事件。

监听新的自定义事件

每个升级后的对话框元素都可以监听五个新事件,如下所示:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

以下是处理这些事件的两个示例:

const dialogOpening = ({target:dialog}) => {
  console.log('Dialog opening', dialog)
}

const dialogClosed = ({target:dialog}) => {
  console.log('Dialog closed', dialog)
  console.info('Dialog user action:', dialog.returnValue)

  if (dialog.returnValue === 'confirm') {
    // do stuff with the form values
    const dialogFormData = new FormData(dialog.querySelector('form'))
    console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))

    // then reset the form
    dialog.querySelector('form')?.reset()
  }
}

在我使用 dialog 元素构建的演示中,我使用该关闭事件和表单数据向列表中添加新的头像元素。时机是这样,对话框已完成其退出动画,然后某些脚本以新头像添加动画效果。得益于这些新事件,编排用户体验可以更加顺畅。

请注意 dialog.returnValue:它包含调用对话框 close() 事件时传递的关闭字符串。在 dialogClosed 事件中,了解对话框是关闭、取消还是已确认至关重要。如果确认成功,脚本随后会抓取表单值并重置表单。重置非常有用,这样一来,当系统再次显示该对话框时,它会显示为空白,并可用于新的提交内容。

总结

现在你已经知道我是怎么做的,希望你怎么办 ‽ 🙂?

让我们来了解一下我们采用的方法多样化,并了解在 Web 上构建网站的所有方法。

只需创建一个演示,点击 tweet me 链接,我就会将其添加到下方的“社区混剪”部分中!

社区混剪作品

资源