こんにちは!もりけん塾のJavaScript課題24、会員登録画面の作成にチャレンジしたので、学習記録を残します。
課題24から、プルリクエストを細かく出すことにしました。今までは、コミットは細かくしてましたが、仕様をすべて満たしてから、プルリクエストしてました。
粒度を細かくすることで、レビュー側の負担が減り、修正も容易になるので、良いそうです。
一回目のプルリクエストで実装した仕様
・バリデーションはここではなし
・ユーザー名、メールアドレス、パスワードの入力欄と利用規約に関するチェックボックス(画像参照)がある。
・送信ボタンがあるが振るまいの実装はしないで良い
・利用規約のテキストを押すと、モーダルが立ち上がり(前回作ったもので良い)、ダミーの利用規約がテキストとして読める。
・checkedがtrueの場合送信ボタンを押下すると別ページのregister-done.htmlに飛ぶ
・register-done.htmlは画面のようなテキストになっている(画面は適当で、遷移できていることが分かれば良い。CSSも書かないでも良い)
コードレビュー
モーダルの実装部分のコードです。CSSは、TailwindCSSを使って実装しています。
<div data-id="popup-modal" tabindex="-1" class="hidden modal">
<div class="relative p-4 w-full max-w-2xl h-auto" data-id="modal-inner">
<div class="relative bg-white rounded-lg shadow overflow-y-auto h-96">
<button type="button" class="close_btn" data-modal-toggle="popup-modal">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd">
</path>
</svg>
<span class="sr-only">Close modal</span>
</button>
<div class="p-6">
<div class="my-6">
<h2>利用規約</h2>
<p class="text-xs">この利用規約(以下,「本規約」といいます。)は,_____(以下,「当社」といいます。)
がこのウェブサイト上で提供するサービス(以下,「本サービス」といいます。)の利用条件を定めるものです。
登録ユーザーの皆さま(以下,「ユーザー」といいます。)には,本規約に従って,本サービスをご利用いただきます。
</p>
</div>
<div class="my-6">
<h2>第1条(適用)</h2>
<ol class="list-decimal list-inside pl-4 -indent-4">
<li class="text-xs mb-1">
本規約は,ユーザーと当社との間の本サービスの利用に関わる一切の関係に適用されるものとします。
</li>
<li class="text-xs mb-1">
当社は本サービスに関し,本規約のほか,ご利用にあたってのルール等,
各種の定め(以下,「個別規定」といいます。)をすることがあります。
これら個別規定はその名称のいかんに関わらず,本規約の一部を構成するものとします。
</li>
<li class="text-xs mb-1">
本規約の規定が前条の個別規定の規定と矛盾する場合には,
個別規定において特段の定めなき限り,個別規定の規定が優先されるものとします。
</li>
</ol>
</div>
<!--長いので、第2条から第14条までは、省略してのせてます -->
<div class="my-6" data-id="last_text">
<h2>第15条(準拠法・裁判管轄)</h2>
<ol class="list-decimal list-inside pl-4 -indent-4">
<li class="text-xs mb-1">本規約の解釈にあたっては,日本法を準拠法とします。</li>
<li class="text-xs mb-1">本サービスに関して紛争が生じた場合には,当社の本店所在地を管轄する裁判所を専属的合意管轄とします。</li>
</ol>
</div>
</div>
</div>
<div class="my-4 text-center">
<button data-modal-toggle="popup-modal" type="button" class="agree_btn" data-id="agree_button">
利用規約を最後まで読み、同意する
</button>
<p data-modal-toggle="popup-modal" class="cancel_text">
キャンセル
</p>
</div>
</div>
</div>
const showModal = (target) => {
target.classList.remove('hidden');
target.setAttribute('aria-modal', 'true');
target.setAttribute('role', 'dialog');
target.removeAttribute('aria-hidden');
}
const hideModal = (target) => {
target.classList.add('hidden');
target.setAttribute('aria-hidden', 'true');
target.removeAttribute('aria-modal');
target.removeAttribute('role');
}
const toggleModal = () => {
const modalTarget = document.querySelector('[data-id="popup-modal"]');
const modalTriggerElements = document.querySelectorAll("[data-modal-toggle]");
const modalIsHidden = modalTarget.classList.contains("hidden");
modalTarget.addEventListener('click',() => hideModal(modalTarget));
modalTriggerElements.forEach((trigger) => {
trigger.addEventListener('click', () => {
modalIsHidden ? showModal(modalTarget) : hideModal(modalTarget);
});
});
}
toggleModal();
TailwindCSSのコンポーネントプラグイン、flowbiteを参考にして実装して自分で実装してみましたが、モーダルのアクセシビリティの要件を満たしていないことをレビューで指摘されたので、修正しました。また、ダイアログ属性も、JavaScriptではなく、HTML側でつけるのが、一般的なことを教えていただきました。
モーダルのアクセシビリティについて、ほとんど知らなかったので、要件を調べるところから始めました。
まず、ダイアログにしたい要素を、role=”dialog”で囲みます。さらに、下記の要件が必要です。
https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Roles/dialog_role
- ダイアログには適切なラベルを付ける必要があります。
- キーボードのフォーカスを正しく管理する必要があります。
ラベルをつけるには、role="dialog"
要素に aria-labelledby
属性を使用します。また、aria-describedby 属性で、ダイアログのタイトル以外に説明テキストを含んでいる場合に、ID を指定して、ダイアログとの関連付けを行います。
キーボードのフォーカスを正しく管理するためには、モーダルが開いたときに、モーダルにフォーカスを自動で当て背景の要素にフォーカスが当たらないように制御したり、モーダルが開いている間は、モーダルの要素にしかフォーカスがいかないようにする、フォーカストラップを実装する必要があります。
フォーカス管理の基本的な要件は、ざっくりまとめると下記です。
- ダイアログには、 [閉じる]、[OK]、[キャンセル]など、1つ以上のフォーカス可能なコントロール要素を持たせる。
- ダイアログが表示されたら、キーボードのフォーカスを、ダイアログ内のフォームの最初の要素などフォーカス可能なコントロール要素に移動させる。
- ダイアログが閉じた後は、キーボードのフォーカスは、ダイアログに移動する前の位置に戻す。(ユーザーが自分がどの位置にいたのか混乱するのを防ぐため)
- タブ移動で、フォーカスが最後のものに達した後、最初のフォーカス可能な要素にフォーカスを移す。(フォーカストラップのこと)
- モーダルダイアログが表示されている限り、メインアプリケーションの UI やページコンテンツは一時的に無効になっているとみなす。
これらの要件を守って、修正したのが下記のコードです。ダイアログの属性は、HTML要素に追加しました。
const showModal = (target) => {
target.classList.remove('hidden');
target.setAttribute('aria-hidden', 'false');
document.querySelector('[data-id="close-button"]').focus(); //モーダルの最初の要素にフォーカス
document.body.setAttribute('aria-hidden', 'true');
}
const hideModal = (target) => {
target.classList.add('hidden');
target.setAttribute('aria-hidden', 'true');
document.querySelector('[data-id="terms-link"]').focus(); //モーダルを開くリンクにフォーカスを戻す
document.body.removeAttribute('aria-hidden');
}
//フォーカストラップを追加
const focusLock = (target) => {
const focusableElementsSelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, [tabindex="0"], [contenteditable]';
target.addEventListener("keydown",(event)=> {
if (event.key === "Tab") {
event.preventDefault();
const focusableElements = [...target.querySelectorAll(focusableElementsSelector)];
const focusedItemIndex = focusableElements.indexOf(document.activeElement);
if (focusedItemIndex === focusableElements.length - 1) {
focusableElements[0].focus();
} else {
focusableElements[focusedItemIndex + 1].focus();
}
}
if (event.key === "Escape") {
event.preventDefault();
hideModal(target);
}
});
}
const toggleModal = () => {
const modalTarget = document.querySelector('[data-id="popup-modal"]');
const modalTriggerElements = document.querySelectorAll("[data-modal-toggle]");
const isHiddenModalTarget = modalTarget.classList.contains("hidden");
focusLock(modalTarget);
modalTarget.addEventListener('click',() => hideModal(modalTarget));
modalTriggerElements.forEach((trigger) => {
trigger.addEventListener('click', () => {
isHiddenModalTarget ? showModal(modalTarget) : hideModal(modalTarget);
});
});
}
toggleModal();
role="dialog"
とaria-labelledby
属性、aria-describedby属性を追加しました。
<div data-id="popup-modal" tabindex="-1" class="hidden modal" aria-hidden="true">
<div class="relative bg-white rounded-lg shadow overflow-y-auto h-96 p-4 w-full max-w-2xl" role="dialog" aria-modal="true" aria-labelledby="modal_title" aria-describedby="modal_body">
<button type="button" class="close_btn" data-id="close-button" data-modal-toggle="popup-modal">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd">
</path>
</svg>
<span class="sr-only">Close modal</span>
</button>
<div class="p-6">
<div class="my-6">
<h2 id="modal_title">利用規約</h2>
<p id="modal_body" class="text-xs">この利用規約(以下,「本規約」といいます。)は,_____(以下,「当社」といいます。)
がこのウェブサイト上で提供するサービス(以下,「本サービス」といいます。)の利用条件を定めるものです。
登録ユーザーの皆さま(以下,「ユーザー」といいます。)には,本規約に従って,本サービスをご利用いただきます。
</p>
</div>
会員登録の入力フォームのコードは下記のように、なりました。
<div class="mx-auto my-20 max-w-xs w-full">
<form method="post" id="member_registration">
<h1>会員登録</h1>
<div class="mb-6">
<label for="user_name">お名前</label>
<input type="text" name="name" id="user_name" placeholder="山田 太郎" required>
</div>
<div class="mb-6">
<label for="email">メールアドレス</label>
<input type="email" name="email" id="email" placeholder="sample@sample.com" required>
</div>
<div class="mb-6">
<label for="password">パスワード</label>
<input type="password" name="password" id="password" placeholder="********" required>
</div>
<div class="flex items-center mb-6">
<input type="checkbox" name="agree_check" id="agree_check" data-id="agree_check" value="agreement" disabled>
<label for="agree_check"><a href="#" data-id="terms-link" data-modal-toggle="popup-modal">利用規約</a>を読み、同意しました。</label>
</div>
<div class="text-center">
<button type="submit" name="submit_button" class="submit_btn" data-id="submit_button" disabled>会員登録する</button>
</div>
</form>
</div>
const agreeCheckBox = document.querySelector('[data-id="agree_check"]');
const agreeButton = document.querySelector('[data-id="agree_button"]');
const submitButton = document.querySelector('[data-id="submit_button"]');
agreeCheckBox.addEventListener('change' , () => {
submitButton.disabled = !agreeCheckBox.checked;
});
agreeButton.addEventListener('click' , () => {
agreeCheckBox.removeAttribute('disabled');
agreeCheckBox.checked = true;
submitButton.disabled = false;
});
submitButton.addEventListener("click", (e) => {
e.preventDefault();
window.location.href = "register-done.html";
});
完成した画面
最終的に、下記のような画面ができあがりました。
残りの実装をして、課題24を進めたいと思います。
今回学んだこと
- ダイアログのアクセシビリティ要件
- フォーカストラップの実装方法
- モーダルの実装方法
次回は、スクロールイベントを手軽に扱うことができるAPI、Intersection Observer APIを使った実装をしていきます。
私が所属しているフロントエンドエンジニアを目指す方のための塾 「もりけん塾」の森田賢二先生のTwitterはこちら!
【募集】フロントエンド、とりわけJavaScriptを教えてもらいたい塾生を募集しています。条件はありますが、お金は頂きません。詳しくはDMください#JavaScript教えてもらいたい方#駆け出しのエンジニアと繋がりたい
— フロントエンドエンジニア (@terrace_tech) April 18, 2020
わたしについて👉https://t.co/FETVmMT5AY
先生のブログ「武骨日記」はこちら!