【HTML】便利そうな dialog 使ってみた感想

## 背景

**モーダルを作るってダルくないですか？**

- モーダル自身を作成して・・・
- モーダルの背景を作成して・・・
- モーダル内のボタンを押すとモーダルと背景が非表示になって・・・ 
- モーダルの位置を固定して・・・
- 背景とz-index調整して・・・
- etc・・・ 

実装しないといけない要素が複数あるので面倒という話です。
そこで、この **`dialogタグ`** の登場です。

簡単にいうと**上記で列挙した機能を簡単に作ってくれるすごいやつ**です。

実は結構前（2014年）から存在していたのですが、対応しているブラウザが`Chrome`や`Opera` のみでした。それが、2022年に[ 標準ブラウザで対応できる](https://developer.mozilla.org/ja/docs/Web/HTML/Element/dialog#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7)ようになり、実用性が出てきたので、これを機に触って理解しようと思います。

[WEB+DB vol.135](https://www.amazon.co.jp/WEB-DB-PRESS-Vol-135-%E9%B6%B4%E9%95%B7/dp/429713571X) でも紹介されていますが、
実際に使ってみて「便利だな！」と思ったので、備忘録として残しました。



## 使い方

### dialogが提供するメソッドと属性
- ダイアログを表示するための `show()`メソッド
- モーダルダイアログを表示するための`showModal()` メソッド
- ダイアログを閉じるための`close()`メソッド
- ダイアログの状態を表す`open`属性

### dialog/dialogModalを表示する
```js
// dialog を取得
const dialog = document.getElementById('dialog');
// ダイアログを表示
dialog.show()
// モーダルダイアログを表示
dialog.showModal()
```
正直`showDialog()`メソッドが一番期待できるメソッドでした。
モーダルの背景とその擬似要素をカスタムできるので、とても便利


### dialog/dialogModalを閉じる
```js
// dialog を取得
const dialog = document.getElementById('dialog');
dialog.show()
// モーダルダイアログを閉じる
dialog.close()
```
### ダイアログが開いている状態
ダイアログが表示されていると、`open`属性がダイアログタグに付与されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/1460cbe1-e959-ec27-8c66-a27c2e863b63.png)

検証ツールで`open`を消すとモーダルを非表示にすることはできますが、その後`showModal()`メソッドが効かず、モーダル再表示ができないなどのバグの種になるので直接操作するのは避けたほうがよさそうですね。

用意されているメソッドを素直に使っていきましょう。


では実際に書いてみます。

### コード内容

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <style>
    .dialog::backdrop {
        backdrop-filter: blur(8px);
    }

    .dialog {
        box-shadow: 0px 20px 36px 0px rgba(0, 0, 0, 0.6);
    }
  </style>
</head>
<body>
  <button id="openButton">モーダルを開く</button>
  <dialog id="modalDialog" class="dialog">
    <div id="dialog-container">
        <header>
            <span>Header</span>
            <button id="closeButton" type="button">
                <p>閉じる</p>
            </button>
        </header>
        <div>Message</div>
        <form method="dialog">
            <button type="submit" value="OK">Ok</button>
            <button type="submit" value="CANCEL">Cancel</button>
        </form>
    </div>
  </dialog>


<script>
  const openButton = document.getElementById('openButton');
  const modalDialog = document.getElementById('modalDialog');

 // モーダルを開く
  openButton?.addEventListener('click', async () => {
    modalDialog.showModal();
    // モーダルダイアログを表示する際に背景部分がスクロールしないようにする
    document.documentElement.style.overflow = "hidden";
  });

  const closeButton = document.getElementById('closeButton');

  // モーダルを閉じる
  closeButton?.addEventListener('click', () => {
    modalDialog.close();
    // モーダルを解除すると、スクロール可能になる
    document.documentElement.removeAttribute("style");
  });
</script>

</body>
</html>
```


### デモ

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="vYQJWBa" data-user="knicisow-the-vuer" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/knicisow-the-vuer/pen/vYQJWBa">
  Untitled</a> by 小林誠 (<a href="https://codepen.io/knicisow-the-vuer">@knicisow-the-vuer</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


これでモーダルを表示/非表示を簡単に実装できました。

### 補足
**スクロールの固定**
```js
document.documentElement.style.overflow = "hidden";
```
モーダルが表示されていても、背景部分がスクロールして鬱陶しいので、
モーダル表示時にスクロールを固定するギミックを追加しました。


**::backdropによる擬似要素**
ダイアログモーダル表示時に、擬似要素として表示される`::backdrop`に対し、
ぼかし要素（blau）を追加することで背景も簡単にカスタムできます。



## モーダルが閉じる際に引数に値を渡す
モーダルが閉じた時(closeイベントが発火した時)に、引数に値を渡すことができます。

検証のために以下を追記
```JS
  // モーダルイベントが閉じる時の処理
  modalDialog.addEventListener("close",() => {
    alert(modalDialog.returnValue);
  });
```
### method="dialog"

`submit`として`Ok`と`Cancel`を用意しました。
`form` 要素で`method="dialog"`と指定すると`submit`でリクエストを送信せずに
`dialog`を閉じることができます。

また、その`value`が`dialog`の`returnValue`プロパティに格納されます。
よって、上記のコードを足した場合、モーダル上で
「Ok」をクリックすると「OK」とアラートが、
「Cancel」をクリックすると「CANCEL」とアラートが表示されます。

これによって、どのボタンをクリックしたのかの判別も容易です。
　
### close()メソッド
モーダルを閉じるメソッドである`close()`には引数に値を渡すことで、
その値を`dialog`の`returnValue`プロパティに渡すことができます。

```JS
  closeButton?.addEventListener('click', () => {
    // 引数を渡す
    modalDialog.close('閉じたで');
  });
```
上記をさらに追記することで
「閉じる」をクリックすると「閉じたで」とアラートが表示されます。

## モーダルを開く時にアニメーション
こっちはイージィ
以下のようにアニメーションをCSSで追加するだけ
```css
    @keyframes fadeIn {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }

    .dialog[open]{
        animation-name: fadeIn;
        animation-fill-mode: forwards;
        animation-duration: 200ms;
        animation-timing-function: ease-out;
    }
```

## モーダルを閉じる時にアニメーション
こっちが少々面倒くさい

というのも、`close()`メソッドが発火すると`open`属性解除に伴い 
`diaplay:none;` スタイルが適用され、モーダルが非表示になるからです。
つまり、アニメーションが見える前に非表示になってしまう。

そのため、モーダル非表示時にアニメーションをつけるには
`JS`で `diaplay:none;` を制御してモーダルを出しわけする必要があります。

まず、
**・open属性適用時のデフォルトスタイルを初期に適用**
**・非表示時のアニメーションを追加**
```css
    @keyframes fadeOut {
        from {
            opacity: 1;
        }
        to {
            opacity: 0;
        }
    }

    .dialog {
        /* デフォルトスタイル */
        display: block;
        position: fixed;
        inset-inline: 0;
        inset-block: 0;
        
        /* 非表示のアニメーション登録 */
        animation-name: fadeOut;
        animation-fill-mode: forwards;
        animation-duration: 200ms;
        animation-timing-function: ease-out;
    }
```

**モーダルに非表示スタイルを適用**

```html
<dialog id="modalDialog" class="dialog" style="display: none;">
```
**モーダルを開く際に非表示スタイルを解除する**
```js
   openButton?.addEventListener('click', async () => {
        modalDialog.removeAttribute("style")
```

**モーダルを閉じる（closeイベント発火）時に非表示スタイルを適用する**
この時、アニメーションが見えるように、アニメーションを待機してから、
非表示スタイルを適用する。
```js
    modalDialog.addEventListener("close", async(e) => {
        // アニメーション終了後に、スタイルを適用する
        await waitDialogAnimation(e.target)
        modalDialog.style.display = "none"
    })

    // アニメーションが完了するまで待機する
    const waitDialogAnimation = (dialog) => Promise.allSettled(
        Array.from(dialog.getAnimations()).map(animation => animation.finished)
    );
```

### デモ
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="yLQPLyB" data-user="knicisow-the-vuer" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/knicisow-the-vuer/pen/yLQPLyB">
  Untitled</a> by 小林誠 (<a href="https://codepen.io/knicisow-the-vuer">@knicisow-the-vuer</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

モーダルを閉じる時にアニメーションをつけるUIはあまり見ないので、
もしかしたら需要はそんなにないかもしれない。


## モーダルの背景をクリックして、モーダルを閉じたい
デフォルトではモーダルの背景をクリックしても、イベントは何も発火しません。
「背景クリック」でモーダルを閉じるようにしたければ、修正が必要です。

**下記のように`dialog`の内部にもう１階層が必要**
```html
  <dialog id="modalDialog" class="dialog">
    <div id="dialogInputArea"> // ここが必要
        // 略
    </div>
  </dialog>
```

**背景にクリックイベントを追加**
ダイアログモーダルが表示されている時、
ダイアログ要素（背景）をクリックするとモーダルを閉じるようにします。
```js

//ダイアログのクリックイベント
modalDialog.addEventListener('click', (event) => {
  if(event.target.closest('#dialogInputArea') === null) {
    modalDialog.close();
  }
});
```
背景クリックでモーダル非表示はUIとしても結構お目にかかることが多いので、
これはデフォルトで用意してほしかったなぁ。


## 感想

`dialog` を使うことで、自作するよりも簡単に実装できますね。
というか「モーダルを実装するなら全部、`dialog`でいいんじゃね？」
と思うくらいに便利だと思います。

`safari`とか`firefox`は2022年3月と比較的最近対応したので
場合によっては動かない可能性があるので注意。

## 最後に
ここまで読んでくださりありがとうございます。

皆さんもぜひ dialog 使ってみてください！