🎛️

dialog要素をhtml/javascriptで実装してみる

2024/06/12に公開

ダイアログ・モーダル・モードレスの定義

ダイアログ

ダイアログとは、一般的に「対話」や「会話」を意味し、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