dialog要素をhtml/javascriptで実装してみる
ダイアログ・モーダル・モードレスの定義
ダイアログ
ダイアログとは、一般的に「対話」や「会話」を意味し、2人以上の人々が意見を交換する行為を指します。ユーザーのアクションに対するレスポンスとして表示されるコンポーネントです。
ダイアログには、ユーザーが表示中のダイアログを閉じるまで他の操作を行えない「モーダルダイアログ」と、ダイアログ表示中も他の操作を続けられる「モードレスダイアログ」があります。
dialogタグの基本的な仕様
open属性
dialogタグにopen属性を設定すると、ブラウザ上に表示されます。
ダイアログはボタン操作などにより表示・非表示を切り替える必要があります。JavaScriptを使用して、動的な機能を追加します。
showメソッド
定義: ダイアログをモードレス(modeless)として表示します。
特徴: ダイアログを開いている間も、ユーザーは他の操作を続けられます。
showModalメソッド
定義: ダイアログをモーダル(modal)として表示します。
特徴: ダイアログが表示されている間、ユーザーは背後のページにアクセスできません。ダイアログを閉じるまで他の操作はブロックされます。背景をクリック操作させないために、dialogタグに::backdropの疑似要素が追加されます。
例
showボタンをクリックすると、タブ操作で背景のitemリンクにフォーカスが移動します。
showModalボタンをクリックすると、モーダル状態のため、タブ操作で背景のitemリンクにフォーカスが移動しません。
dialogタグに考慮されているウェブアクセシビリティの要件
フォーカス
- モーダルダイアログを開いた時、フォーカスがモーダルダイアログのフォーカス可能な要素に移動します。
- モーダルダイアログを閉じた時、フォーカスがモーダルダイアログを開く要素に移動します。
キーボード操作
- モーダルダイアログを開いた状態で、タブキーで操作した時、フォーカスが背後のページのフォーカス可能な要素に移動しません。
- Escキーで、モーダルダイアログを閉じることができます。
マウス操作
- モーダルダイアログを開いた状態で、背後のページがクリック操作できません。
追加で実装する機能
背後のページがスクロールに反応しないようにする
背後のページをクリックすることはできませんが、スクロールは可能です。モーダルダイアログが開いているときは背後のページがスクロールしないようにします。
開閉ボタンの状態の管理
モーダルダイアログの状態が表示・非表示かをユーザーに伝えるために、aria-expanded属性を使用します。
backdrop疑似要素をクリックした時、モーダルダイアログを閉じる
マウス操作で、コンテンツ以外の部分(backdrop疑似要素部分)をクリックした時、モーダルダイアログを閉じるようにします。
実装
基本のHTML構造とJavaScriptです。これに機能を追加していきます。
<button type="button" class="dialog__open-button" aria-expanded="false" aria-controls="modal">
モーダルダイアログを開く
</button>
<dialog id="modal" class="dialog">
<div class="dialog__inner">
<h2 class="dialog__heading">モーダルダイアログ</h2>
<ul class="dialog__list">
<li class="dialog__list-item">アイテム</li>
</ul>
<button type="button" aria-controls="modal" aria-expanded="false" class="dialog__close-button">
モーダルダイアログを閉じる
</button>
</div>
</dialog>
const dialog: HTMLDialogElement | null = document.querySelector('.dialog');
const openDialogButton: HTMLButtonElement | null = document.querySelector('.dialog__open-button');
const closeDialogButton: HTMLButtonElement | null = document.querySelector('.dialog__close-button');
const openDialog = () => {
dialog.showModal();
};
const closeDialog = () => {
dialog.close();
};
openDialogButton.addEventListener('click', () => {
openDialog();
});
closeDialogButton.addEventListener('click', () => {
closeDialog();
});
背後のページがスクロールに反応しないようにする。
やり方は、モーダルダイアログが開いている時にposition:fixed;を使用して配置を固定します。モーダルダイアログが閉じたら、position:fixed;を解除します。
const scrollLock = () => {
const scrollY = window.scrollY;
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollY}px`;
document.body.style.left = '0';
document.body.style.width = '100%';
document.body.style.overflowY = 'scroll';
};
const scrollUnlock = () => {
const scrollY = document.body.style.top;
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.width = '';
document.body.style.overflowY = '';
window.scrollTo(0, parseInt(scrollY || '0', 10) * -1);
};
const openDialog = () => {
dialog.showModal();
scrollLock();
};
const closeDialog = () => {
dialog.close();
scrollUnlock();
};
開閉ボタンのaria-expanded属性の切り替え
const openDialog = () => {
dialog.showModal();
openDialogButton.setAttribute('aria-expanded', 'true');
closeDialogButton.setAttribute('aria-expanded', 'true');
scrollLock();
};
const closeDialog = () => {
dialog.close();
openDialogButton.setAttribute('aria-expanded', 'false');
closeDialogButton.setAttribute('aria-expanded', 'false');
scrollUnlock();
};
backdrop疑似要素をクリックした時、モーダルダイアログを閉じる
backdrop疑似要素をクリックした時に、閉じるボタンを押したときと同じ関数を実行させる。
dialog.addEventListener('click', (event) => {
if (event.target === dialog) {
closeDialog();
}
});
if (event.target === dialog) {
closeDialog();
}
if (event.target === dialog)は、クリックされた要素(event.target)が、ダイアログ内の他の要素ではなく、ダイアログの背景部分がクリックされたときに発火します。
JavaScriptのコード
const dialog: HTMLDialogElement | null = document.querySelector('.dialog');
const openDialogButton: HTMLButtonElement | null = document.querySelector('.dialog__open-button');
const closeDialogButton: HTMLButtonElement | null =
document.querySelector('.dialog__close-button');
const scrollLock = () => {
const scrollY = window.scrollY;
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollY}px`;
document.body.style.left = '0';
document.body.style.width = '100%';
document.body.style.overflowY = 'scroll';
};
const scrollUnlock = () => {
const scrollY = document.body.style.top;
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.width = '';
document.body.style.overflowY = '';
window.scrollTo(0, parseInt(scrollY || '0', 10) * -1);
};
const openDialog = () => {
dialog.showModal();
openDialogButton.setAttribute('aria-expanded', 'true');
closeDialogButton?.setAttribute('aria-expanded', 'true');
scrollLock();
};
const closeDialog = () => {
openDialogButton.setAttribute('aria-expanded', 'false');
closeDialogButton.setAttribute('aria-expanded', 'false');
if (dialog.open) {
dialog.close();
}
scrollUnlock();
};
openDialogButton.addEventListener('click', () => {
openDialog();
});
closeDialogButton.addEventListener('click', () => {
closeDialog();
});
dialog.addEventListener('click', (event) => {
if (event.target === dialog) {
closeDialog();
}
});
Discussion