ダイアログ コンポーネントを作成する

<dialog> 要素を使用して、色を適応させ、レスポンシブで、ユーザー補助に対応したミニモーダルとメガモーダルを構築する方法の基本的な概要。

この記事では、<dialog> 要素を使用して、色に適応し、レスポンシブで、ユーザー補助に対応したミニモーダルとメガモーダルを構築する方法について説明します。デモを試すソースを表示する

ライトモードとダークモードのメガダイアログとミニダイアログのデモ。

動画で視聴したい場合は、この投稿の YouTube バージョンをご利用ください。

概要

<dialog> 要素は、ページ内のコンテキスト情報やアクションに適しています。フォームが小さい場合や、ユーザーに必要なアクションが確認またはキャンセルのみの場合など、マルチページ アクションではなく、同じページのアクションがユーザー エクスペリエンスにメリットをもたらす場合を検討してください。

<dialog> 要素は最近、すべてのブラウザで安定して動作するようになりました。

対応ブラウザ

  • Chrome: 37.
  • Edge: 79.
  • Firefox: 98.
  • Safari: 15.4。

ソース

要素に欠けている点がいくつかあるため、この GUI チャレンジでは、デベロッパー エクスペリエンスとして期待する項目(追加のイベント、ライトを閉じる、カスタム アニメーション、ミニタイプとメガタイプ)を追加します。

マークアップ

<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> の 3 つの要素があります。これらは、セマンティック コンテナとして機能し、ダイアログの表示のスタイル ターゲットとしても機能します。ヘッダーにはモーダルのタイトルと閉じるボタンが表示されます。この記事では、フォームの入力と情報について説明します。フッターには操作ボタンの <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>

ダイアログ要素は、データとユーザー操作を収集できる完全なビューポート要素のための強固な基盤を提供します。これらの基本原則により、サイトやアプリで非常に興味深く効果的なインタラクションを行うことができます。

ユーザー補助

ダイアログ要素には、優れたユーザー補助機能が組み込まれています。通常は追加する機能ですが、多くの機能はすでに用意されています。

フォーカスを復元しています

サイドナビ コンポーネントの作成で手動で行ったように、開閉を適切に行うには、関連する開閉ボタンにフォーカスを合わせることが重要です。サイドナビを開くと、閉じるボタンにフォーカスが移動します。閉じるボタンを押すと、開いたボタンにフォーカスが復元されます。

ダイアログ要素では、これがデフォルトの動作として組み込まれています。

残念ながら、ダイアログのアニメーション化を行うと、この機能は失われます。JavaScript セクションで、その機能を復元します。

フォーカスのトラップ

ダイアログ要素は、ドキュメントで inert を管理します。inert より前は、JavaScript を使用して要素からフォーカスが外れたことを監視し、その時点でインターセプトして元に戻していました。

対応ブラウザ

  • Chrome: 102。
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5。

ソース

inert の後、ドキュメントの任意の部分を「凍結」して、フォーカス ターゲットやマウス操作の対象にならないようにすることができます。フォーカスをトラップするのではなく、ドキュメント内の唯一のインタラクティブな部分にフォーカスを誘導します。

要素を開いて自動フォーカスする

デフォルトでは、ダイアログ要素はダイアログ マークアップ内の最初のフォーカス可能な要素にフォーカスを割り当てます。これがユーザーのデフォルトとして最適な要素でない場合は、autofocus 属性を使用します。前述のように、このメッセージを確認ボタンではなくキャンセルボタンに配置することをおすすめします。これにより、確認が意図的なものであり、偶発的なものではないことを確認できます。

Esc キーで閉じる

中断の原因となる可能性のある要素は簡単に閉じられるようにすることが重要です。幸い、ダイアログ要素はエスケープキーを処理するため、オーケストレーションの負担を軽減できます。

スタイル

ダイアログ要素のスタイル設定には簡単な方法とハードパスがあります。簡単な方法では、ダイアログの表示プロパティを変更せず、その制限を操作します。ダイアログの開閉や display プロパティの取得など、カスタム アニメーションを提供するハードな方法を採用します。

開いた小道具を使用したスタイル設定

アダプティブ カラーと全体的なデザインの一貫性を高めるために、私は CSS 変数ライブラリの Open Props を導入しました。提供されている無料の変数に加えて、正規化ファイルといくつかのボタンもインポートします。どちらも、Open Props がオプションのインポートとして提供しています。これらのインポートにより、ダイアログとデモのカスタマイズに集中できます。また、サポートと見栄えを良くするために多くのスタイルを必要としません。

<dialog> 要素のスタイル設定

表示プロパティの所有

ダイアログ要素のデフォルトの表示と非表示の動作では、表示プロパティが block から none に切り替わります。そのため 内外にアニメーション化することは できませんインとアウトの両方をアニメーション化するには、まず独自の display プロパティを設定します。

dialog {
  display: grid;
}

上記の CSS スニペットに示すように、ディスプレイ プロパティの値を変更して所有するには、適切なユーザー エクスペリエンスを実現するために、多くのスタイルを管理する必要があります。まず、ダイアログのデフォルトの状態が閉じます。この状態を視覚的に表し、次のスタイルでダイアログがインタラクションを受けないようにすることができます。

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

ダイアログは非表示になり、開いていないときに操作できなくなります。後で、ダイアログの inert 属性を管理する JavaScript を追加して、キーボードとスクリーン リーダーのユーザーが隠しダイアログにアクセスできないようにします。

ダイアログにアダプティブ カラーテーマを設定する

ライトモードとダークモードを示すメガ ダイアログで、サーフェスの色が示されています。

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 が 2 回使用されていることに注目してください。最初の方法では、物理ビューポート ユニットである 80vh を使用します。実際には、国際ユーザー向けにダイアログを相対フロー内に維持したいため、2 番目の宣言で論理的な新しい dvb ユニットを使用しています。このユニットは、安定性が向上したときに使用します。

メガダイアログの配置

ダイアログ要素の配置を支援するには、全画面バックドロップとダイアログ コンテナの 2 つの部分に分割することをおすすめします。背景はすべてを覆い、このダイアログが前面にあり、背後のコンテンツにアクセスできないことを示すシェード効果を提供する必要があります。ダイアログ コンテナは、この背景の上に自由に配置でき、コンテンツに必要な形状にすることができます。

次のスタイルでは、ダイアログ要素をウィンドウに固定し、各隅に伸ばし、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;
  }
}

開いた状態で、パソコンとモバイルの両方のメガ ダイアログに余白の余白が重なっている DevTools のスクリーンショット。

ミニ ダイアログの配置

デスクトップ パソコンなど、大きなビューポートを使用している場合は、ミニダイアログを呼び出した要素の上に配置しました。そのためには、JavaScript が必要です。私が使用している手法は、こちらで確認できますが、この記事の範囲外です。JavaScript がないと、メガダイアログと同様に、ミニダイアログが画面の中央に表示されます。

目立たせる

最後に、ダイアログに少し華やかさを加えて、ページの上に浮かぶ柔らかいサーフェスのように見せます。柔らかさを実現するには、ダイアログの角を丸くします。奥行きは、Open Props の丁寧に作られたシャドウ プロップのいずれかによって実現されています。

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

背景疑似要素をカスタマイズする

背景は控えめに扱い、backdrop-filter でメガ ダイアログにぼかし効果のみを追加しました。

対応ブラウザ

  • Chrome: 76。
  • Edge: 79。
  • Firefox: 103.
  • Safari: 18。

ソース

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

また、将来的にブラウザで背景要素の遷移が許可されることを期待して、backdrop-filter に遷移を設定しました。

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

カラフルなアバターのぼかし背景に重ねて表示されるメガダイアログのスクリーンショット。

スタイル設定の追加機能

このセクションは、一般的なダイアログ要素というより、ダイアログ要素のデモに関連するものが多いため、「追加機能」と呼んでいます。

スクロール コンテインメント

ダイアログが表示されても、ユーザーはダイアログの背後にあるページをスクロールできます。これは望ましくありません。

通常は overscroll-behavior を使うのが普通の解決策ですが、仕様によると、これはスクロール ポート、つまりスクローラーではないため、ダイアログには影響しません。したがって、防止するものはありません。JavaScript を使用して、このガイドの新しいイベント(「閉じた」や「開いた」など)を監視し、ドキュメントで overflow: hidden を切り替えることもできます。または、すべてのブラウザで :has() が安定するまで待つこともできます。

対応ブラウザ

  • Chrome: 105。
  • Edge: 105。
  • Firefox: 121。
  • Safari: 15.4。

ソース

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

メガダイアログが開いているときに、html ドキュメントに overflow: hidden が追加されました。

<form> レイアウト

ユーザーからインタラクション情報を収集するうえで非常に重要な要素であるだけでなく、ここではヘッダー、フッター、記事要素のレイアウトにも使用しています。このレイアウトでは、記事の子をスクロール可能な領域として明示します。これは grid-template-rows で実現します。article 要素には 1fr が指定され、フォーム自体の最大高さはダイアログ要素と同じです。この固定の高さと固定の行サイズを設定することで、記事要素を制限し、オーバーフローしたときにスクロールできるようにします。

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

グリッド レイアウト情報が行の上にオーバーレイ表示されている devtools のスクリーンショット。

ダイアログ <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);
  }
}

ダイアログのヘッダーに Flexbox のレイアウト情報を重ねて表示している Chrome DevTools のスクリーンショット。

ヘッダーの閉じるボタンのスタイル設定

このデモでは 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 DevTools のスクリーンショット。

ダイアログ <article> のスタイル設定

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

Flexbox のレイアウト情報がフッター要素に重なっている Chrome DevTools のスクリーンショット。

menu 要素は、ダイアログのアクション ボタンを格納するために使用します。gap を使用して、ボタンの間にスペースを確保するラップ フレックスボックス レイアウトを使用しています。メニュー要素には <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;
}

フッター メニュー要素に flexbox 情報がオーバーレイされている Chrome DevTools のスクリーンショット。

アニメーション

ダイアログ要素は多くの場合、ウィンドウに出入りするため、アニメーション化されます。この開始と終了をダイアログにサポート モーションを提供することで、ユーザーはフローにじっと目を通すことができます。

通常、ダイアログの要素はアニメーションインのみ可能で、アウトできません。これは、ブラウザが要素の display プロパティを切り替えるためです。これまで、ガイドは表示をグリッドに設定していましたが、なしには設定されませんでした。これにより、アニメーションイン/アウトが可能になります。

Open Props には、使用できる多くのキーフレーム アニメーションが付属しているため、オーケストレーションが簡単で読みやすくなっています。アニメーションの目標と、私が採用したレイヤ化アプローチは次のとおりです。

  1. モーションの低減はデフォルトの遷移で、単純な不透明度のフェードインとフェードアウトです。
  2. モーションに問題がなければ、スライドとスケールのアニメーションが追加されます。
  3. メガダイアログのレスポンシブ モバイル レイアウトが、スライドアウトするように調整されました。

安全で有意義なデフォルトの遷移

Open Props にはフェードインとフェードアウトのキーフレームが用意されていますが、デフォルトでは、このレイヤ化された遷移アプローチを使用し、キーフレーム アニメーションはアップグレードとして使用することをおすすめします。先ほど、ダイアログの表示スタイルを不透明度に設定し、[open] 属性に応じて 1 または 0 をオーケストレートしました。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;
  }
}

モバイル向けに終了アニメーションを適応させる

前述のスタイル設定のセクションでは、メガダイアログ スタイルをモバイル デバイス用に適合させ、アクションシートのようにしました。小さな紙が画面の下部からスライドアップし、下部に固定されているように見えます。スケールアウト終了のアニメーションは、この新しいデザインにうまく適合していないため、いくつかのメディアクエリといくつかの 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 という 2 つの新しいイベントを作成します。次に、ダイアログの組み込みの閉じるイベントをリッスンします。次に、ダイアログを 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 が非同期関数である理由は、返された Promise を await して、クローズされたイベントに確実に進むことができるためです。

開店イベントと開店済みイベントの追加

組み込みのダイアログ要素は、閉じるイベントと同様に開くイベントを提供しないため、これらのイベントは簡単に追加できません。MutationObserver を使用して、ダイアログの属性の変化に関する分析情報を提供します。このオブザーバーでは、open 属性の変更を監視し、それに応じてカスタム イベントを管理します。

終了イベントとクローズド イベントの開始方法と同様に、openingopened という 2 つの新しいイベントを作成します。前にダイアログのクローズ イベントをリッスンしていましたが、今回は作成されたミューテーション オブザーバーを使用してダイアログの属性を監視します。


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 要素の子要素を監視し、ダイアログ要素が削除されるのを監視します。


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

ミューテーション オブザーバー コールバックは、ドキュメントの本文に子が追加または削除されるたびに呼び出されます。モニタリングされる特定のミューテーションは、ダイアログの nodeName を持つ removedNodes です。ダイアログが削除された場合は、メモリを解放するためにクリック イベントと閉じるイベントが削除され、カスタム削除イベントがディスパッチされます。

loading 属性の削除

ページに追加されたときやページの読み込み時にダイアログ アニメーションで終了アニメーションが再生されないように、読み込み属性がダイアログに追加されました。次のスクリプトは、ダイアログ アニメーションの実行が完了するのを待ってから、属性を削除します。ダイアログのアニメーションの開始と終了が自由に設定できるようになり、邪魔になるアニメーションを効果的に非表示にできるようになりました。

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)

このように、2 つのダイアログは、軽い閉じ方、アニメーションの読み込みの修正、より多くのイベントに対応するようにアップグレードされています。

新しいカスタム イベントをリッスンする

アップグレードされた各ダイアログ要素は、次のように 5 つの新しいイベントをリッスンできるようになりました。

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

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

MegaDialog.addEventListener('removed', dialogRemoved)

これらのイベントを処理する例を 2 つ示します。

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 イベントでは、ダイアログが閉じられたか、キャンセルされたか、確認されたかを把握することが重要です。確認されると、スクリプトはフォームの値を取得してフォームをリセットします。このリセット機能は、ダイアログが再び表示されたときに空白になり、新しい送信の準備が整う場合に便利です。

まとめ

私の方法をご覧になったところで、あなたならどうしますか?

アプローチを多様化して、ウェブで構築するすべての方法を学びましょう。

デモを作成して、ツイートしてください。リンクを送信していただければ、下のコミュニティ リミックスのセクションに追加します。

コミュニティ リミックス

リソース