构建对话框组件

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

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

演示了浅色和深色主题下的超级对话框和迷你对话框。

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

概览

通过 <dialog> 元素能够很好地满足页内背景信息或操作要求。考虑 同页面操作比多页操作更能改善用户体验 操作:可能是因为表单较小,或者表单不需要 确认或取消相应用户

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

浏览器支持

  • 37
  • 79
  • 98
  • 15.4

来源

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

Markup

<dialog> 元素的基本功能很简单。该元素将 会自动隐藏,并内置了样式以叠加在内容之上。

<dialog>
  …
</dialog>

我们可以改善这一基准。

从传统意义上讲,对话框元素与模态的很多共同点,通常其名称 可以互换。我在这里自由使用了对话框元素 小对话框弹出式窗口(迷你)和全页对话框(超级)。我已将名称 为大型和迷你型应用,这两个对话框均针对不同的使用情形略作调整。 我添加了 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 元素具有非常好的内置无障碍功能。你无需将这些 许多功能已经应用了

正在恢复焦点

正如我们在构建侧边导航栏时手动制作的 组件,请务必 恰当地打开和结束某些内容,使焦点置于相关的打开和结束处 按钮。该侧边导航栏打开后,焦点会放在关闭按钮上。当 关闭按钮时,焦点会恢复到打开该按钮的按钮。

对于 dialog 元素,这是内置的默认行为:

遗憾的是,如果你想为对话框添加动画效果, 会丢失在JavaScript 部分中,我将恢复 功能

陷阱重点

dialog 元素管理 inert 文档。在 inert 之前,使用 JavaScript 观察焦点 离开某个元素时,它会截获并放回该元素。

浏览器支持

  • 102
  • 102
  • 112
  • 15.5

来源

inert之后,文档的任何部分都可以“冻结”这些都是 不再聚焦于目标或与鼠标互动。不要陷入 焦点,将被引导到文档中唯一的互动部分。

打开元素并自动聚焦

默认情况下,dialog 元素会将焦点分配给第一个可聚焦元素 。如果这不是用户默认使用的最佳元素 使用 autofocus 属性。如前所述,在我看来 将其放在取消按钮(而不是确认按钮)上。这样可以确保 是有意为之,而非无意间确认。

使用 Esc 键结束

请务必让用户能够轻松关闭这一可能造成干扰的元素。 幸运的是,dialog 元素会为您处理 Esc 键,让您腾出时间 让它们摆脱编排负担

样式

设置对话框元素样式的方法很简单,也没有硬性路径。简单的 通过不更改对话框的显示属性和 存在局限性我不忘初心,努力为大家提供自定义动画 打开和关闭对话框,接管 display 属性等。

使用开放道具设置样式

为了加快自适应颜色和整体设计的一致性,我羞辱地说 CSS 变量库 Open Props 提供的。在 除了免费的变量以外,我还导入 归一化文件和一些 按钮,这两个按钮都带有“打开属性” 作为可选导入提供这些导入有助于我专注于自定义 同时又不需要许多样式来支持它,让它看起来 很好。

设置 <dialog> 元素的样式

拥有展示广告媒体资源

对话框元素的默认显示和隐藏行为用于切换显示或隐藏状态 属性从 block 更改为 none。很遗憾,系统无法对其进行动画处理 只不过是进出。我想要为输入和输出添加动画效果,第一步是 设置我自己的 display 属性:

dialog {
  display: grid;
}

更改并因此拥有 display 属性值,如 需要管理大量的样式 以提供良好的用户体验。首先,对话框的默认状态为 已关闭。您可以直观地表示此状态,并阻止对话框 接收与以下样式的互动:

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

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

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

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

color-scheme 可为您的文档启用浏览器提供的服务 根据系统偏好设置选择浅色和深色的自适应颜色主题, dialog 元素的影响。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 元素。

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
  • 79
  • 103
  • 18

来源

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

我还选择了对 backdrop-filter 进行转场,希望浏览器能 将允许在将来转换背景幕元素:

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

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

样式设置 extra

我将此部分称为“extras”因为这与对话框元素有关 与常规的 dialog 元素相比。

滚动包含

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

通常 overscroll-behavior 这是我常用的解决方法,但根据 规范, 它对对话框没有影响 因为它不是滚动端口 因此没有任何要阻止的内容我可以使用 JavaScript 本指南中的新事件,例如“closed”“打开”和“打开”切换按钮 针对文档的 overflow: hidden 权限,或者我可以等待 :has() 稳定, 所有浏览器:

浏览器支持

  • 105
  • 105
  • 121
  • 15.4

来源

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

现在,当一个大型对话框打开时,html 文档将具有 overflow: hidden

<form> 布局

不仅是收集互动数据的重要元素 我在这里使用这些信息来设置页眉、页脚和 报道元素。采用这种布局时,我打算 可滚动区域中。为了实现这一目标 grid-template-rows。 文章元素被指定为 1fr,并且表单本身也具有相同的上限 高度为 dialog 元素。通过设置固定高度和固定行大小, 允许文章元素受到约束,并在其溢出时滚动:

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

开发者工具的屏幕截图,其中叠加了行上的网格布局信息。

设置对话框样式 <header>

此元素的作用是为对话框内容和优惠提供标题 一个易于找到的关闭按钮它也被赋予了表面颜色,使其得以显示 隐藏在对话框文章内容之后。这些要求将指向 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 开发者工具的屏幕截图,其中叠加了页脚菜单项上的 Flexbox 信息。

动画

对话框元素通常是动画形式,因为它们会进入和退出窗口。 针对这种进入和退出操作提供一些辅助性对话框有助于用户 确定自己在音乐流中的方向

通常,对话框元素只能以动画方式移入,不能移出。这是因为 浏览器会切换该元素的 display 属性。之前,本指南 将 display 设置为 grid,但绝不会将其设为 none。这样一来,您就可以 添加动画效果。

Open Props 带有许多关键帧 使用动画 简单易读这些是动画目标 我采取的方法:

  1. 减少动作为默认转场效果,即简单的不透明度淡入和淡出。
  2. 如果允许移动,则添加滑动和缩放动画。
  3. 将超级对话框的自适应移动设备布局调整为滑出。

安全且有意义的默认过渡

虽然 Open Props 带有淡入和淡出的关键帧,但我更喜欢 默认采用分层式过渡方式,采用关键帧动画 可能的升级之前,我们已经使用 不透明度、编排 10,具体取决于 [open] 属性。接收者 在 0% 到 100% 之间转换,告知浏览器需要多长时间以及 您想要的加/减速选项:

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

为过渡效果添加动画

如果用户可以移动,则超级对话框和迷你对话框都应滑动 向上作为入口,横向延伸为退出。为此,您可以使用 prefers-reduced-motion 媒体查询和几个 Open Prop:

@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;
  }
}
<ph type="x-smartling-placeholder">
</ph>

针对移动设备调整退出动画

在前面的“样式设置”部分中,超级对话框样式已针对移动设备进行了调整 让设备更像是一张操作表格,就像一张小纸张滑了一样 而且依然贴着在屏幕底部体重秤 退出动画与这种新设计不太契合,我们可以 几个媒体查询和一些 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
}

这些新功能的初衷是希望应用能够轻松关闭(点击对话框 动画和其他一些事件 表单数据。

添加轻度关闭

这项任务很简单,能够很好地补充 动画效果互动是通过观察用户对对话框的点击来实现的 元素并利用事件 冒泡 评估用户点击的内容, 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 可以检索该字符串,以深入了解 对话框已关闭。您会发现我在每次调用时都提供了接近的字符串。 功能,为我的应用提供 用户互动

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

对话框元素附带一个关闭事件: 对话框 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 的完成。正因如此,dialogClose异步 函数; 然后它可以 await promise 返回,并自信地推进到已关闭的事件。

添加打开活动和已打开事件

添加这些事件并不容易,因为内置对话框元素 像调用 close 一样提供打开事件。我使用 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 元素。最后,类似于 和关闭事件,立即分派打开事件,等待动画 完成,然后分派打开的事件。

添加已移除的事件

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

您可以使用另一个变更观察器来实现这一点。这一次, 我们将观察对话框元素的子元素 元素,并观察是否有对话框元素被移除。

…
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)
      }
    })
  })
})

每当添加或移除子项时,都会调用变更观察器回调 。所监控的特定更改 removedNodes,其中包含 nodeName/ 对话框。如果对话框已移除,点击和关闭事件也会一并移除, 释放内存,且系统会分派自定义的已移除事件。

移除加载属性

要阻止对话框动画在添加到 时播放其退出动画,请执行以下操作: 加载时,已向对话框中添加加载属性。通过 以下脚本会等待对话框动画运行完毕, 属性。现在对话框可以随意以动画的形式进出 从而有效隐藏了原本会分散注意力的动画。

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.returnValue:其中包含在调用 对话框 close() 事件被调用。在dialogClosed事件中,务必要 知道对话框是关闭、取消还是确认的。如果请求得到确认 然后,脚本会抓取表单值并重置表单。重置操作非常有用 再次显示对话框时,此对话框是空白的,可供提交新内容。

总结

现在您已经知道我是怎么做到的了,您该怎么做 ‽ 🙂?

让我们一起采用多样化的方法,学习所有在 Web 上构建应用的方法。

创建演示,在 Twitter 微博上添加链接,然后我会添加 到下面的社区混剪部分!

社区混剪作品

资源