雑記帳

整理しない情報集

更新情報

<dialog>で遊ぶ

公開日:

カテゴリ: JavaScript

ちょっと見ないうちに<dialog>要素に色々な属性やAPIが生えてきたので、確認がてら遊んでみます。

この記事は主にChromeで動作を確認しています。他のブラウザでは未実装の機能もありますので、実際に使用する場合は対応状況を確認しましょう。

基本

<dialog>要素はHTMLDialogElementクラスのHTML要素です。HTMLがバージョンで呼ばれていた時代の末期に追加された要素で、長らく音沙汰のない状態が続いていましたが、ここ最近じわりと機能が追加されています。

初期状態ではdisplay: none;が指定されており、非表示要素になっています。また、UAのスタイルとしてposition: absolute;が指定されています。

open属性の値によってダイアログ要素の表示・非表示の状態を切り替えることができます。

<dialog open>dialog</dialog>

ダイアログのオープン

ダイアログはopen属性で初期から表示させる他に、JavaScriptで表示状態を切り替えることができます。JavaScriptでの開き方は2種類あります。

モードレス表示

ダイアログを開いた状態でも他の操作をすることができる、いわゆる「モードレス」ダイアログとして開く方法です。HTMLDialogElementshow()を呼び出すことで開くことができます。

開くと要素にopen属性が追加され、openを指定したときと同じ挙動になります。

<button id="button">モードレス表示</button>
<dialog id="dialog">dialog</dialog>
const dialog = document.getElementById("dialog");
document.getElementById("button").addEventListener("click", () => dialog.show());

モーダル表示

ダイアログを開いた状態では他の操作をすることができない、いわゆる「モーダル」ダイアログとして開く方法です。HTMLDialogElementshowModal()を呼び出すことで開くことができます。

開くとモードレス表示と同様に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で閉じる

HTMLDialogElementclose()が用意されており、実行するとダイアログを閉じることができます。

<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でイベントを捕捉する

ダイアログ要素では、通常のイベントに加えてclosecancelの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