やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

ダイアログの最適解を探る〜WEBアプリ編〜

 HTML,CSS,JSでアプリ作成するときダイアログが欲しいので実装方法を探ってみました。

成果物

ダイアログの必要性

 要約のためにダイアログが必要です。メイン画面には要約したシンプルな情報だけを表示し、ダイアログでその詳細を表示・編集します。これにより見通しのよいアプリになります。

 もしダイアログを使わず、詳細もすべてメイン画面に含めたら、概要を理解するのに時間がかかってしまうでしょう。画面スクロールに手間がかかります。今、何について話しているのか、わかりにくくなるでしょう。

 アプリが一定の規模を超えたら、ダイアログによる要約は是非とも欲しい機能です。

実装方法

 ダイアログの実装方法は次の3通りあります。

§ 標準API

 JavaScriptの標準APIでダイアログ表示します。

alert('OK押下で閉じます。');

confirm('OKならtrue、キャンセルならfalseを返します。');

prompt('入力したテキストを返します。', '初期値');

 非常にシンプルなダイアログです。残念ながらデザイン設計できません。ブラウザ毎にデザインが異なります。タイトル、ボタン位置、ラベルの変更ができません。コンボボックスなど他のUIを含めることもできません。あまりにシンプルすぎてダサいし使いにくいです。

  • タイトルが指定できない(このページの内容になる)
  • ボタン位置を変更できない(デフォルトでフォーカスをキャンセルに当てたいけど不可)
  • ラベルの変更ができない(OKYES, キャンセルNOにしたいけど不可)
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.2.8.nomodule.min.js"></script>
<script>
window.addEventListener('DOMContentLoaded', async(event) => {
van.add(document.body, 
  van.tags.button({onclick:e=>alert('OK押下で閉じます。')}, 'alert'),
  van.tags.button({onclick:e=>confirm('OKならtrue、キャンセルならfalseを返します。')}, 'confirm'),
  van.tags.button({onclick:e=>prompt('入力したテキストを返します。', '初期値')}, 'prompt'),
)
})
</script>

 CodePenだとalert()などのダイアログが機能しませんでした。ローカルでindex.htmlを作って動作させてください。

See the Pen dialog-standard-api by ytyaru (@ytyaru) on CodePen.

§ <dialog>

 <dialog>はHTML要素なのでCSSでデザインを変更できます。異なるブラウザでも同じ見た目にできます。また、<select>など任意のUI要素を含めることも可能です。

 一見、最高に思えますが、問題もあります。

 実装状況が微妙です。特にFirefoxが98以降でのみ使えます。割と最近に実装されたので、使えない環境の人もまだいるでしょう。

 しかもダイアログとしての機能を自分で実装する必要があります。閉じるボタンや、ダイアログの外側をクリックしたときに閉じる機能など、最初から欲しい機能がありません。

 以下コードはダイアログを開く/閉じるボタンの機能を実装したものです。冗長ですね。

<script>
window.addEventListener('DOMContentLoaded', async(event) => {
    for (const dialog of document.querySelectorAll(`dialog`)) {
        const closeButton = dialog.querySelector(`header button[data-close]`)
        if (!closeButton) { continue }
        closeButton.addEventListener('click', async(event) => { dialog.close() })
    }
    for (const button of document.querySelectorAll(`button[data-open]`)) {
        const id = button.getAttribute('data-open')
        button.addEventListener('click', async(event) => { document.querySelector(`#${id}`).showModal() })
    }
})
</script>

<button data-open="dialog-1">ダイアログを開く</button>
<dialog id="dialog-1">
  <header><h1 style="display:inline;">ダイアログの表題</h1><button data-close></button></header>
  <main><p>ダイアログの本文</p></main>
</dialog>

See the Pen dialog-element by ytyaru (@ytyaru) on CodePen.

 ここからさらに、外側をクリックしたら閉じる機能や、CSSでデザイン設計せねばなりません。

 外側をクリックしたら閉じる機能は、モーダルの実装をmicromodalからdialog要素に置き換えることを考えるによると、ポリフィルのデモを参考にすればできそうらしいです。

§ micromodal.js

 micromodal.jsサードパーティ製ライブラリです。ダイアログとしての機能を最低限だけ実装します。

<style>
.modal {display:none;}
.modal.is-open {display:block;}
</style>
<script src="https://unpkg.com/micromodal/dist/micromodal.min.js"></script>
<script>
window.addEventListener('DOMContentLoaded', async(event) => {
    MicroModal.init();
})
</script>

<button data-micromodal-trigger="modal-1" role="button">ダイアログを開く</button>

<div id="modal-1" class="modal" aria-hidden="true">
  <div tabindex="-1" data-micromodal-close>
    <div role="dialog" aria-modal="true" aria-labelledby="modal-1-title" >
      <header>
        <h2 id="modal-1-title">ダイアログの見出し</h2>
        <button aria-label="Close modal" data-micromodal-close></button>
      </header>
      <div id="modal-1-content">
        <p>ダイアログの本文。</p>
      </div>
    </div>
  </div>
</div>

 <dialog>を使わず<div>で実装します。なのでFirefox 98以前でも動作します。

 以下のようなAPIになります。

MicroModal.show('modal-id');
MicroModal.close('modal-id');
MicroModal.init({
  onShow: modal => console.info(`${modal.id} is shown`),
  onClose: modal => console.info(`${modal.id} is hidden`),
  openTrigger: 'data-custom-open',
  closeTrigger: 'data-custom-close',
  openClass: 'is-open',
  disableScroll: true,
  disableFocus: false,
  awaitOpenAnimation: false,
  awaitCloseAnimation: false,
  debugMode: true
});

 特にdisableScrollは設定したほうが良いでしょう。ダイアログを開いた時にマウスホイールでスクロールしても、背景の画面はスクロールされなくなります。普通はそれを期待するはずです。

MicroModal.init({disableScroll:true});

 厄介なのはダイアログの開く/閉じる機能をCSSで表現している所です。

.modal {display:none;}
.modal.is-open {display:block;}

See the Pen dialog-micromodal-js-0 by ytyaru (@ytyaru) on CodePen.

 上記例でわかるように、ダイアログ感がありません。メイン画面の下にボロンと表示される形です。ちがう、そうじゃない。

 CSSで実装しないと、ダイアログっぽい表示すらできないんです。

 アニメーション用のオプションもありますが、やはりCSSで定義していないと動作しません。

 公式ドキュメントの末尾にコード例へのリンクがありました。

 そのままでは動作しなかったのでJavaScriptコードの追加をしてCodePenで動かしたのが以下です。

See the Pen dialog-micromodal-js-1 by ytyaru (@ytyaru) on CodePen.

 レスポンシブでない点が気になります。max-width: 500px;の所。他にもフォントサイズやパディングなど調整が必要そうです。アニメーションが実装されている所は嬉しいですね。

 結構大変そうですが、現状ベストなダイアログ実装方法です。

ソースコード

micromodal.css

/**************************\
  Basic Modal Styles
\**************************/

.modal {
  font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;
}

.modal__overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal__container {
  background-color: #fff;
  padding: 30px;
  max-width: 500px;
  max-height: 100vh;
  border-radius: 4px;
  overflow-y: auto;
  box-sizing: border-box;
}

.modal__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal__title {
  margin-top: 0;
  margin-bottom: 0;
  font-weight: 600;
  font-size: 1.25rem;
  line-height: 1.25;
  color: #00449e;
  box-sizing: border-box;
}

.modal__close {
  background: transparent;
  border: 0;
}

.modal__header .modal__close:before { content: "\2715"; }

.modal__content {
  margin-top: 2rem;
  margin-bottom: 2rem;
  line-height: 1.5;
  color: rgba(0,0,0,.8);
}

.modal__btn {
  font-size: .875rem;
  padding-left: 1rem;
  padding-right: 1rem;
  padding-top: .5rem;
  padding-bottom: .5rem;
  background-color: #e6e6e6;
  color: rgba(0,0,0,.8);
  border-radius: .25rem;
  border-style: none;
  border-width: 0;
  cursor: pointer;
  -webkit-appearance: button;
  text-transform: none;
  overflow: visible;
  line-height: 1.15;
  margin: 0;
  will-change: transform;
  -moz-osx-font-smoothing: grayscale;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  -webkit-transform: translateZ(0);
  transform: translateZ(0);
  transition: -webkit-transform .25s ease-out;
  transition: transform .25s ease-out;
  transition: transform .25s ease-out,-webkit-transform .25s ease-out;
}

.modal__btn:focus, .modal__btn:hover {
  -webkit-transform: scale(1.05);
  transform: scale(1.05);
}

.modal__btn-primary {
  background-color: #00449e;
  color: #fff;
}



/**************************\
  Demo Animation Style
\**************************/
@keyframes mmfadeIn {
    from { opacity: 0; }
      to { opacity: 1; }
}

@keyframes mmfadeOut {
    from { opacity: 1; }
      to { opacity: 0; }
}

@keyframes mmslideIn {
  from { transform: translateY(15%); }
    to { transform: translateY(0); }
}

@keyframes mmslideOut {
    from { transform: translateY(0); }
    to { transform: translateY(-10%); }
}

.micromodal-slide {
  display: none;
}

.micromodal-slide.is-open {
  display: block;
}

.micromodal-slide[aria-hidden="false"] .modal__overlay {
  animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}

.micromodal-slide[aria-hidden="false"] .modal__container {
  animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
}

.micromodal-slide[aria-hidden="true"] .modal__overlay {
  animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}

.micromodal-slide[aria-hidden="true"] .modal__container {
  animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
}

.micromodal-slide .modal__container,
.micromodal-slide .modal__overlay {
  will-change: transform;
}

index.html

<link rel="stylesheet" href="micromodal.css">
<script src="https://unpkg.com/micromodal/dist/micromodal.min.js"></script>
<script>
window.addEventListener('DOMContentLoaded', async(event) => {
    MicroModal.init({
      disableScroll: true,
      awaitOpenAnimation: true,
      awaitCloseAnimation: true
    });
})
</script>

<button data-micromodal-trigger="modal-1" role="button">ダイアログを開く</button>

  <div class="modal micromodal-slide" id="modal-1" aria-hidden="true">
    <div class="modal__overlay" tabindex="-1" data-micromodal-close>
      <div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
        <header class="modal__header">
          <h2 class="modal__title" id="modal-1-title">
            Micromodal
          </h2>
          <button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
        </header>
        <main class="modal__content" id="modal-1-content">
          <p>
            Try hitting the <code>tab</code> key and notice how the focus stays within the modal itself. Also, <code>esc</code> to close modal.
          </p>
        </main>
        <footer class="modal__footer">
          <button class="modal__btn modal__btn-primary">Continue</button>
          <button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Close</button>
        </footer>
      </div>
    </div>
  </div>

まとめ

 ダイアログの実装方法は次の3通りあります。

 micromodal.jsの方法が最も美しく高機能で楽に実装できるでしょう。

 (他にも候補はありますがフレームワークに依存しており手が出しにくいです)

参考