<dialog>で遊ぶ
公開日:
カテゴリ: JavaScript
ちょっと見ないうちに<dialog>
要素に色々な属性やAPIが生えてきたので、確認がてら遊んでみます。
この記事は主にChromeで動作を確認しています。他のブラウザでは未実装の機能もありますので、実際に使用する場合は対応状況を確認しましょう。
基本
<dialog>
要素はHTMLDialogElement
クラスのHTML要素です。HTMLがバージョンで呼ばれていた時代の末期に追加された要素で、長らく音沙汰のない状態が続いていましたが、ここ最近じわりと機能が追加されています。
初期状態ではdisplay: none;
が指定されており、非表示要素になっています。また、UAのスタイルとしてposition: absolute;
が指定されています。
open
属性の値によってダイアログ要素の表示・非表示の状態を切り替えることができます。
<dialog open>dialog</dialog>
ダイアログのオープン
ダイアログはopen
属性で初期から表示させる他に、JavaScriptで表示状態を切り替えることができます。JavaScriptでの開き方は2種類あります。
モードレス表示
ダイアログを開いた状態でも他の操作をすることができる、いわゆる「モードレス」ダイアログとして開く方法です。HTMLDialogElement
のshow()
を呼び出すことで開くことができます。
開くと要素にopen
属性が追加され、open
を指定したときと同じ挙動になります。
<button id="button">モードレス表示</button>
<dialog id="dialog">dialog</dialog>
const dialog = document.getElementById("dialog");
document.getElementById("button").addEventListener("click", () => dialog.show());
モーダル表示
ダイアログを開いた状態では他の操作をすることができない、いわゆる「モーダル」ダイアログとして開く方法です。HTMLDialogElement
のshowModal()
を呼び出すことで開くことができます。
開くとモードレス表示と同様にopen
属性が追加されますが、要素は最上位レイヤー(#top-layer)に配置されます。またモーダル表示中は、開いたダイアログとその内部以外の要素にフォーカスできなくなります。
<button id="button">モーダル表示</button>
<dialog id="dialog">dialog</dialog>
const dialog = document.getElementById("dialog");
document.getElementById("button").addEventListener("click", () => dialog.showModal());
モーダル表示時の背景は::backdrop
疑似要素でスタイルすることができます。
<button id="button">モーダル表示</button>
<dialog id="dialog">dialog</dialog>
#dialog::backdrop {
background-color: #0008;
backdrop-filter: blur(5px);
}
const dialog = document.getElementById("dialog");
document.getElementById("button").addEventListener("click", () => dialog.showModal());
ちなみに::backdrop
疑似要素は最上位レイヤー(#top-layer)に配置されている要素にのみ適用されます。モーダルダイアログ以外ではPopover
で使えます(そちらでは使う機会は滅多に無さそうですが)。
ダイアログを閉じる
ダイアログを閉じる方法は3パターンあります。JavaScriptで閉じる方法、<form>
要素で閉じる方法、ダイアログ要素の属性で指定する方法があります。
JavaScriptで閉じる
HTMLDialogElement
にclose()
が用意されており、実行するとダイアログを閉じることができます。
<button id="button">ダイアログを開く</button>
<dialog id="dialog"><button id="close">閉じる</button></dialog>
const dialog = document.getElementById("dialog");
document.getElementById("button").addEventListener("click", () => dialog.showModal());
document.getElementById("close").addEventListener("click", () => dialog.close());
フォームの送信として閉じる方法
ダイアログ要素内に配置された<form>
要素のメソッドにdialog
を指定することで、submit
時にダイアログを閉じることができます。ダイアログを閉じるだけで、実際のフォーム送信リクエスト(GETやPOST)は実行されません。
<button id="button">ダイアログを開く</button>
<dialog open>
<form method="dialog">
<input type="submit" value="submit">
</form>
</dialog>
formmethod
属性でも同じ結果になります。
<button id="button">ダイアログを開く</button>
<dialog open>
<form>
<input type="submit" value="submit" formmethod="dialog">
</form>
</dialog>
HTMLのclosedby属性による指定
ダイアログ要素にclosedby
属性を追加することで、ダイアログの閉じ方を指定することができます。属性値によって挙動が異なります。指定しなかった場合、モードレスとして開くとnone
、モーダルとして開くとcloserequest
が自動で設定されます。
none
標準でダイアログを閉じる方法はありません。配置された閉じるボタン等のみによって閉じることができます。
<dialog id="dialog" open closedby="none">
<button id="close">閉じる</button>
</dialog>
const dialog = document.getElementById("dialog");
document.getElementById("close").addEventListener("click", () => dialog.close());
closerequest
ブラウザが標準で提供している閉じる手段を使用することができます。多くのブラウザではEsc
キー押下でダイアログを閉じる機能を、手動でイベントを追加しなくても実現できます。
<dialog open closedby="closerequest">dialog</dialog>
any
closerequest
に加えて、Light Dismissと呼ばれるダイアログの外側をクリックした際にも閉じる指定方法です。
<dialog open closedby="any">dialog</dialog>
JavaScriptでイベントを捕捉する
ダイアログ要素では、通常のイベントに加えてclose
とcancel
の2種類のイベントが存在します。
closeイベント
ダイアログを閉じたタイミングで発火されます。
<div id="output"></div>
<dialog id="dialog" open>
<button id="close">閉じる</button>
</dialog>
const dialog = document.getElementById("dialog");
const output = document.getElementById("output");
dialog.addEventListener("close", () => output.textContent = "close");
document.getElementById("close").addEventListener("click", () => dialog.close());
cancelイベント
ダイアログ要素のrequestClose()
を実行した場合、もしくはブラウザが標準で提供している閉じる手段を用いた場合に発火されます。closedby
属性に指定する値とは単語の順番が逆です。
cancel
イベント発火後にclose()
が実行されます。cancelable
なイベントであるため、e.preventDefault()
を実行することでダイアログを閉じる動作を抑制することができます。
<button id="button">モーダル表示</button>
<div id="output"></div>
<dialog id="dialog">
<button id="close">close()</button>
<button id="requestclose">requestClose()</button>
</dialog>
const dialog = document.getElementById("dialog");
document.getElementById("button").addEventListener("click", () => dialog.showModal());
document.getElementById("close").addEventListener("click", () => dialog.close());
document.getElementById("requestclose").addEventListener("click", () => dialog.requestClose());
const output = document.getElementById("output");
dialog.addEventListener("close", () => output.textContent += " close");
dialog.addEventListener("cancel", () => output.textContent += " cancel");
なお、ダイアログ表示後にユーザが何も操作をせずにEscキーで閉じた場合は発火されません。例えば、画面表示時にモーダルダイアログを表示させ、画面内のどこかをクリックしたりキーボードを押下したりせずにEscキーを押下した場合、cancel
イベントが発火することなくダイアログが閉じます。
Chromium系の挙動
Chromium系ではcancel
イベントの挙動が一部異なります。
- モーダル表示状態でEscキーを2回連続で押下した際の2回目の
cancel
イベントはキャンセル不可(cancelable=false
)
仕様なのかバグなのかは不明です。issueを見る限りでは意図的のように見えますが、実態はよくわかりません。
ダイアログから値を渡す
close()
あるいはrequestClose()
の引数に値をセットした場合、ダイアログのreturnValue
プロパティに値が格納されます。渡せるものはstring
のみです。
<button id="button">モーダル表示</button>
<div id="output"></div>
<dialog id="dialog">
<button id="close">close("1")</button>
<button id="requestclose">requestClose("2")</button>
</dialog>
const dialog = document.getElementById("dialog");
document.getElementById("button").addEventListener("click", () => dialog.showModal());
document.getElementById("close").addEventListener("click", () => dialog.close("1"));
document.getElementById("requestclose").addEventListener("click", () => dialog.requestClose("2"));
const output = document.getElementById("output");
dialog.addEventListener("close", () => output.textContent = dialog.returnValue);
実用?サンプル
とりあえず、すぐに思いついたサンプルを1つ。実用性があるかどうかは知りません。
確認ダイアログ
requestClose()
のおかげで、だいぶスッキリ書けています。Promise.withResolvers()
を使っているからスッキリ書けているとか言わない。
<button id="button">確認ダイアログを表示</button>
<div id="output"></div>
const confirmDialog = async ({ msg }) => {
const dialog = document.createElement("dialog");
const message = document.createElement("p");
message.textContent = msg;
dialog.appendChild(message);
const accept = document.createElement("button");
accept.textContent = "はい";
accept.addEventListener("click", () => dialog.close("1"));
dialog.appendChild(accept);
const decline = document.createElement("button");
decline.textContent = "いいえ";
decline.addEventListener("click", () => dialog.close("2"));
dialog.appendChild(decline);
const cancel = document.createElement("button");
cancel.textContent = "キャンセル";
cancel.addEventListener("click", () => dialog.requestClose());
dialog.appendChild(cancel);
const { promise, resolve, reject } = Promise.withResolvers();
dialog.addEventListener("close", () => resolve(dialog.returnValue));
dialog.addEventListener("cancel", reject);
document.body.appendChild(dialog);
dialog.showModal();
return promise;
};
const output = document.getElementById("output");
document.getElementById("button").addEventListener("click", async () => {
try {
const result = await confirmDialog({ msg: "よろしいですか?" });
output.textContent = `${result}→処理を続行します...`;
} catch {
output.textContent = "キャンセルされました";
}
});
その他
記事内のWeb Featuresのステータスは、以下のスクリプトを用いて表示しています。
カテゴリ: JavaScript