Libon

更为简单易用的原生对话框

这一次彻底学会HTML中 <dialog /> 标签的使用。

其实每个人在做前端开发的时候都应该接触过 DialogModal 这种类似的对话框组件。而无一例外,作为组件库来说,为了照顾到多种适用场景,他们都是通过 div 来模拟实现的。其实在浏览器原生的 <dialog /> 标签元素就足够应付绝大多数的使用场景了。

浏览器兼容性

caniuse.com dialog 上列出了这个元素在所有浏览器中的运行情况,同时也可以在 MDN Dialog 上查看对应的属性,MDN 上具有更多对于 Dialog 元素的描述及使用方法,接下来我们来学习一下文档中没有直接列举出的知识点及额外的知识点。

使用方法

使用起来很简单,只需要在页面中写下 <dialog open></dialog> 就够了,现在你应该能在页面的下方看到一个带有黑色边框的小方块了,这个就是我们今天的主题。接下来我们用更完整的例子来学习如何更好得使用这个原生的对话框元素。

打开对话框

想要打开这个对话框有三种方法:

  1. <dialog /> 标签上设置 open 属性:<dialog open />
  2. 通过调用 <dialog /> 上的 show() 方法:dialog.show()
  3. 通过调用 <dialog /> 上的 showModal() 方法:dialog.showModal()

其中 选项1选项2 的行为表现是一模一样的,唯一区别是开启的位置不同,一个是在 HTML,一个是在 JavaScript 中。而 选项3 则与 选项1/选项2 有些许不同,它包含了一些其他特性,比如:焦点捕获水平垂直居中并添加半透明遮罩层键盘的可访问性 等。你可以在下面亲自观察一下两者的区别。

可以看到不管是哪种方式唤起的弹窗都会主动聚焦到第一个输入框中,当然也可以通过设置 autofocus 属性来手动指定打开弹窗后需要聚焦到哪一个元素,如果同时给多个元素设置了 autofocus 时,只有第一个设置的元素会生效。

关闭对话框

其实不仅仅是打开有两种方式,关闭弹窗也有两种方式:

  1. 通过在 <dialog /> 内部嵌套一层 <form />,同时设置 method="dialog" 属性,再搭配上一个不设置 type 属性或者 type 属性的值不为 resetsubmit<button /> 就可以关闭了,示例如下:
<dialog open>
  <form method="dialog">
    这是弹窗内容
    <button>关闭弹窗</button>
  </form>
</dialog>
  1. 通过调用 dialog.close() 方法关闭

我相信大多数人都会选择第二种,因为第一种不光有局限性,还会增加额外的 DOM 元素,有点得不偿失。

对话框事件

对话框的事件有两个,分别为:cancelclose,是的,没有类似于 open/show 等这种监听弹窗展示的事件,可能是考虑到弹窗展示这种行为大多数是由程序触发的,在程序内部知道什么时候会打开这个弹窗,而关闭的时机确是不可知的,所以只有关闭的事件而没有打开的事件。

dialog.addEventListener('close', console.log)
dialog.addEventListener('cancel', console.log)

close

顾名思义,当对话框被关闭的时候触发,而不论是以哪种方式关闭的弹窗。

cancel

当使用 .showModel() 方法打开弹窗,并按下 Esc 按键时则会触发 cancel 事件,同时还将触发 close 事件,而 cancel 事件会在 close 事件之前触发,所以可以在 cancel 事件内部通过调用 event.preventDefault() 来阻止对话框的默认行为(关闭弹窗)。

对话框样式

最后是样式相关的部分,可以给弹窗进入时设置动画样式,因为原生的展示实在是太生硬了,以下代码实现了一个位移显示的动画:

dialog {
  position: fixed;
  margin: auto;
  inset: 0;
  width: fit-content;
  height: fit-content;
  display: block;
  visibility: hidden;
  opacity: 0;
  transform: translateY(100px);
  transition: .2s;
}

dialog[open] {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
}

除此之外,你还可以通过 ::backdrop 伪类来设置遮罩层的样式,通过 :modal 和使用 :not() 结合 :modal 来设置样式以区分是否包含遮罩层:

dialog:modal{
  /*模态弹窗, 调用 .showModal() 方法*/
  border-color: #f00
}

dialog:not(:modal){
  /*普通弹窗, 调用 .show() 方法*/
  border-color: #0f0
}

/* 可以使用 ::backdrop 来修改遮罩层的样式 */
dialog::backdrop {
  background-color: rgba(255, 0, 0, .5);
}

完整代码

最后,你还可以使用以下代码结合文中所有的知识点进行学习:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <style>
    /* 如果设置了打开动画,自动聚焦的特性就会失效 */
    /* dialog {
      position: fixed;
      margin: auto;
      inset: 0;
      width: fit-content;
      height: fit-content;
      display: block;
      visibility: hidden;
      opacity: 0;
      transform: translateY(100px);
      transition: .2s;
    }

    dialog[open] {
      visibility: visible;
      opacity: 1;
      transform: translateY(0);
    } */

    dialog:modal{
      /*模态弹窗, 调用 .showModal() 方法*/
      border-color: #f00
    }

    dialog:not(:modal){
      /*普通弹窗, 调用 .show() 方法*/
      border-color: #0f0
    }

    /* 可以使用 ::backdrop 来修改遮罩层的样式 */
    dialog::backdrop {
      background-color: rgba(255, 0, 0, .5);
    }
  </style>
</head>

<body>
  <input type="text">
  <button onclick="onOpenClick()">打开</button>

  <!--
    tabindex 对于 <dialog /> 元素来说是无效的
    或者通过 open 设置为默认打开, 和主动调用 .show() 方法行为一致, 没有模态框
    建议使用 show()/showModal() 来打开模态框, 而不是指定 open 属性,因为对于可访问性来讲,
    调用了 showModal() 的时候, 将会具有隐式的 aria-modal="true" 属性
    当以 showModal() 方式打开弹窗时,可以使用 esc 键来关闭弹窗, 如果打开了多个, 则会依次关闭(关闭最后一个)
  -->
  <!-- <dialog id="dialog" open> -->
  <dialog id="dialog" open>
    <!-- 如果是通过 showModal() 方法打开的弹窗,则会自动聚焦到第一个可交互的元素上 -->
    <!-- 当有多个可交互元素的时候,可在对应的输入框上增加 autofocus 来手动指定需要聚焦到哪个 -->
    <!-- <h1>测试</h1>
    <input type="text" name="input" value="123">
    <input type="text" name="input" value="123">
    <button value="close" onclick="onCloseClick()" autofocus>关闭</button> -->

    <!-- 如果是通过 showModal() 方法打开的弹窗,则会自动聚焦到第一个可交互的元素上 -->
    <!-- 当有多个可交互元素的时候,可在对应的输入框上增加 autofocus 来手动指定需要聚焦到哪个 -->
    <form method="dialog">
      <h1>测试</h1>
      <input type="text" name="input" value="123">
      <input type="text" name="input" value="123" autofocus>
      <button value="close">关闭</button>
    </form>
  </dialog>

  <script>
    const dialog = document.getElementById('dialog')

    function onOpenClick() {
      // dialog.show()
      dialog.showModal()
    }

    function onCloseClick() {
      dialog.close();
      // 当使用的是 form{method="dialog"} 的时候, 可以通过 dialog.returnValue 来获取表单的值(input/button上设置的 value 属性)
      // 如果不是,则会回去一个空字符串
      console.log(dialog.returnValue)
    }

    dialog.addEventListener('close', (ev) => {
      console.log('close', ev)
    })

    // 当键盘按下 esc 的时候, 会触发 cancel 事件, 也会同时触发 close 事件, 但是 cancel 事件可以阻止 close 事件的触发
    // 而且 cancel 事件的触发是在 close 事件之前, cancel 只会由 esc 触发, close 可以由 esc 和 dialog.close() 触发
    dialog.addEventListener('cancel', (ev) => {
      ev.preventDefault() // 阻止 close 事件的触发

      console.log('cancel', ev)
    })


    dialog.addEventListener('open', (ev) => {
      console.log('open', ev)
    })
  </script>
</body>

</html>

以上。

cd ../