🐄
モーダルクラス
画像クリックで画像URLを取得してモーダル内でその画像を表示させるみたいなことはブログ(https://code-widgit.com/post-787/ )に書いていたけど、単純なモーダルのテンプレートがないなと思ってとりあえず作ってみました
>コードは書いたそのままここに投げてるので、まだ途中かつ整理してません
getTwoDigitsNumber
はこちらを参照
assignConsecutiveNumber
はこちらを参照
参考:ICSさんの記事
参考:YENDさんの記事
Special Thanks for ikeryo
VanillaJS(クラス)
main.js
import { Modal } from './_utils/_Modal';
//モーダル
new Modal({
modals:".js-modal-sample",
triggers:".js-modal-trigger",
wrapper: ".test-wrapper",
// closeAction: "modal",
});
new Modal({
modals: ".js-modal-sample-02",
triggers: ".js-modal-trigger-02",
// wrapper: null,
closeAction: "modal",
});
_Modal.js
'use strict'
import { BgFix } from './_BgFix';
import { assignConsecutiveNumber } from '../scripts/assignConsecutiveNumber';
const { log } = console;
export class Modal {
constructor(obj) {
//required modals and triggers
if (!obj.modals | !obj.triggers) return;
this.DOM = {};
this.DOM.modals = document.querySelectorAll(obj.modals);
this.DOM.triggers = document.querySelectorAll(obj.triggers);
if (!this.DOM.modals | !this.DOM.triggers) return;
//モーダル内フォーカス関係
this.focusableElements; //モーダル内のフォーカス可能な要素
this.FOCUSABLE_ELEMENTS = this._focusableElements(); //focus可能要素配列
//接頭辞
this.prefix = obj.modals.split(".")[1];
this.ariaPrefix = obj.modals.includes(".js-") ? obj.modals.split(".js-")[1] : obj.modals;
//オーバーレイ
this.DOM.overlays = this._overlayStoreInDOM();
//WAI-ARIAを変更するDOM
this.DOM.header = obj.header ? document.querySelector(obj.header) : document.querySelector(".js-header");
this.DOM.main = [...document.getElementsByTagName("main")][0];
this.DOM.wrapper = obj.wrapper ? document.querySelector(obj.wrapper) : null;
this.DOM.footer = obj.footer ? document.querySelector(obj.footer) : document.querySelector(".js-footer");
this.DOM.hiddenElements = [this.DOM.header, this.DOM.main, this.DOM.wrapper, this.DOM.footer].filter(Boolean);
this.DOM.modalDialogs = this._storeAgainInNodeList("dialog");
this.DOM.modalTitles = this._storeAgainInNodeList("title");
this.DOM.modalDescs = this._storeAgainInNodeList("desc");
//ID, aria系の自動修正
this._idAriaAutoFix();
this.index; //モーダルのインデックス
this.HIDDEN_CLASS = "--hidden"; //hidden class
this.lastFocus; //前回のフォーカス位置
//何をトリガーにして閉じるか modal or button and overlay
//modal全体をトリガーにする時のみ {closeAction: "modal",} を指定する
this.closeAction = obj.closeAction ? obj.closeAction : null;
this.BgFix = new BgFix(); //背景固定クラス
this._openModal(); //open modal act
this._closeModal(); //close modal act
}
_storeAgainInNodeList(type) {
let items = [];
this.DOM.modals.forEach((modal, i) => {
items[i] = modal.querySelectorAll(`*[data-modal-type=${type}]`)[0];
});
items = Object.setPrototypeOf(items, NodeList.prototype);
return items;
}
//focus可能要素を代入
_focusableElements() {
return [
"a[href]",
"area[href]",
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
"select:not([disabled]):not([aria-hidden])",
"textarea:not([disabled]):not([aria-hidden])",
"button:not([disabled]):not([aria-hidden])",
"iframe",
"object",
"embed",
"[contenteditable]",
'[tabindex]:not([tabindex^="-"])',
];
}
//Focus Trap
_focusHandle(e) {
const firstFocusableElement = this.focusableElements[0];
const lastFocusableElement = this.focusableElements[this.focusableElements.length - 1];
if (e.code === "Tab") {
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstFocusableElement) {
e.preventDefault();
// ダイアログ内で最初のtabableの要素の時、最後のtabableの要素にフォーカスを移す
lastFocusableElement.focus();
}
} else {
// Tab
if (document.activeElement === lastFocusableElement) {
e.preventDefault();
// ダイアログ内で最後のtabableの要素の時、最初のtabableの要素にフォーカスを移す
firstFocusableElement.focus();
}
}
}
};
//when "Escape" is pushed
_escapeAction(e) {
if (e.code === "Escape") {
this._closeAction(this.DOM.modals[this.index], this.index);
}
}
//this.DOM.overlaysにオーバーレイ要素を格納
_overlayStoreInDOM() {
this.DOM.modals.forEach(modal => {
const modalChildren = [...modal.children];
modalChildren.forEach(child => {
if (child.dataset.modalType == "overlay") {
child.classList.add(`${this.prefix}-overlay`);
}
});
});
return document.querySelectorAll(`.${this.prefix}-overlay`);
}
//idとariaの自動修正
_idAriaAutoFix() {
assignConsecutiveNumber(this.DOM.modals, "id", this.prefix);
assignConsecutiveNumber(this.DOM.triggers, "aria-controls", this.prefix);
assignConsecutiveNumber(this.DOM.modalDialogs, "aria-labelledby", `${this.ariaPrefix}-title`);
assignConsecutiveNumber(this.DOM.modalDialogs, "aria-describeby", `${this.ariaPrefix}-desc`);
assignConsecutiveNumber(this.DOM.modalTitles, "id", `${this.ariaPrefix}-title`);
assignConsecutiveNumber(this.DOM.modalDescs, "id", `${this.ariaPrefix}-desc`);
}
//user-select変更
_userSelect(boolean) {
if (!this.DOM.main) return;
if (boolean) {
this.DOM.main.setAttribute("style", "user-select: none");
} else {
this.DOM.main.removeAttribute("style", "user-select: none");
}
}
//モーダルオープン
_openModal() {
this.DOM.triggers.forEach((trigger, i) => {
trigger.addEventListener("click", (event) => {
this.index = i; //インデックスを記録
this.BgFix.on(); //背景固定
this._userSelect(true); //add user-select
this.DOM.modals[i].classList.remove(this.HIDDEN_CLASS);
//モーダル内のフォーカス可能な要素
this.focusableElements = [
...this.DOM.modals[i].querySelectorAll(this.FOCUSABLE_ELEMENTS.join(","))
];
//現在のfocusを記録
this.lastFocus = document.activeElement;
//モーダル内の最初の要素にフォーカス
this.focusableElements[0].focus();
//Focus Trap
this.DOM.modals[i].addEventListener("keydown", this._focusHandle.bind(this));
//when push "Escape"
this.DOM.modals[i].addEventListener("keydown", this._escapeAction.bind(this));
//WAI-ARIAの設定を追加
this.DOM.hiddenElements.forEach((element) => element.setAttribute('aria-hidden', 'true'));
this.DOM.modals[i].setAttribute('aria-hidden', 'false')
trigger.setAttribute('aria-expanded', 'true');
});
});
}
//モーダルクローズ
_closeModal() {
this.DOM.modals.forEach((modal, i) => {
//モーダル全体クリックで閉じるが有効の場合
if (this.closeAction == "modal") {
modal.addEventListener("click", () => {
this._closeAction(modal, i);
});
} //モーダル全体クリックで閉じるが無効の場合
else {
//ボタンなどをフォーカス可能要素クリックで閉じる
const focusableCloseTrigger = [ //フォーカス可能な要素
...modal.querySelectorAll(this.FOCUSABLE_ELEMENTS.join(","))
]
focusableCloseTrigger.forEach(t => {
t.addEventListener("click", () => {
this._closeAction(modal, i);
});
});
//オーバーレイクリックで閉じる
this.DOM.overlays[i].addEventListener("click", () => {
this._closeAction(modal, i);
});
}
});
}
//モーダルクローズ動作
_closeAction(modal, i) {
this._userSelect(false); //remove user-select
this.BgFix.off(); //背景固定解除
modal.classList.add(this.HIDDEN_CLASS);
//前回のフォーカス位置に戻す
this.lastFocus.focus();
//WAI-ARIAの設定を戻す
this.DOM.hiddenElements.forEach((element) => element.removeAttribute('aria-hidden'))
modal.setAttribute('aria-hidden', 'true');
this.DOM.triggers[i].setAttribute('aria-expanded', 'false');
}
}
HTML
index.html
<div class="test-wrapper">wrapperです</div>
<!-- modalオリジナル -->
<!-- modal1つ目 -->
<div id="modal_01" class="p-modal --hidden js-modal-sample" aria-hidden="true">
<!-- <button type="button" id="testbtn" class="p-modal__btn jstest" aria-label="close this modal01">
<span class="p-modal__close p-modal__close--01"></span>
<span class="p-modal__close p-modal__close--02"></span>
</button> -->
<div class="p-modal__overlay" data-modal-type="overlay"></div>
<div role="dialog" class="p-modal__content" aria-modal="true" aria-labelledby="dialog-title" aria-describeby="dialog-desc" data-modal-type="dialog">
<h3 id="dialog-title" class="p-modal__title" data-modal-type="title">モーダルです!!その1</p>
<p id="dialog-desc" class="p-modal__desc" data-modal-type="desc">これはダイアログのサンプルです。</p>
<button type="button">→モーダルを閉じる←</button>
</div>
</div>
<div class="p-hoge">
<button class="p-hoge__btn js-modal-trigger" aria-controls="modal_01" aria-expanded="false">ここをクリックでモーダルを開く</button>
</div>
<!-- modal2つ目 -->
<div id="modal_02" class="p-modal --hidden js-modal-sample" aria-hidden="true">
<button type="button" class="p-modal__btn" aria-label="close this modal">
<span class="p-modal__close p-modal__close--01"></span>
<span class="p-modal__close p-modal__close--02"></span>
</button>
<div class="p-modal__overlay" data-modal-type="overlay"></div>
<div role="dialog" class="p-modal__content" aria-modal="true" aria-labelledby="dialog-title" aria-describeby="dialog-desc" data-modal-type="dialog">
<h3 id="dialog-title" class="p-modal__title" data-modal-type="title">モーダルです!!222222</p>
<p id="dialog-desc" class="p-modal__desc" data-modal-type="desc">これはダイアログのサンプルです。</p>
</div>
</div>
<div class="p-hoge">
<button class="p-hoge__btn js-modal-trigger" aria-controls="modal_02" aria-expanded="false">ここをクリックでモーダルを開く2222</button>
</div>
<div class="space"></div>
<!-- modalオリジナル複数設置テスト -->
<!-- modal1つ目 -->
<div id="modal_01" class="p-modal --hidden js-modal-sample-02" aria-hidden="true">
<button type="button" id="testbtn" class="p-modal__btn jstest02" aria-label="close this modal01">
<span class="p-modal__close p-modal__close--01"></span>
<span class="p-modal__close p-modal__close--02"></span>
</button>
<div class="p-modal__overlay" data-modal-type="overlay"></div>
<div role="dialog" class="p-modal__content" aria-modal="true" aria-labelledby="dialog-title" aria-describeby="dialog-desc" data-modal-type="dialog">
<h3 id="dialog-title" class="p-modal__title" data-modal-type="title">複数モーダルのテストです!!その1</h3>
<p id="dialog-desc" class="p-modal__desc" data-modal-type="desc">これはダイアログのサンプルです。</p>
<button type="button">モーダルを閉じる</button>
</div>
</div>
<div class="p-hoge">
<button class="p-hoge__btn js-modal-trigger-02" aria-controls="modal_01" aria-expanded="false">(複数テスト)ここをクリックでモーダルを開く</button>
</div>
<!-- modal2つ目 -->
<div id="modal_02" class="p-modal --hidden js-modal-sample-02" aria-hidden="true">
<button type="button" class="p-modal__btn" aria-label="close this modal">
<span class="p-modal__close p-modal__close--01"></span>
<span class="p-modal__close p-modal__close--02"></span>
</button>
<div class="p-modal__overlay" data-modal-type="overlay"></div>
<div role="dialog" class="p-modal__content" aria-modal="true" aria-labelledby="dialog-title" aria-describeby="dialog-desc" data-modal-type="dialog">
<h3 id="dialog-title" class="p-modal__title" data-modal-type="title">複数モーダルテストです!!222222</h3>
<p id="dialog-desc" class="p-modal__desc" data-modal-type="desc">これはダイアログのサンプルです。</p>
<!-- <button type="button">モーダルを閉じるのですうう</button> -->
</div>
</div>
<div class="p-hoge ">
<button class="p-hoge__btn js-modal-trigger-02" aria-controls="modal_02" aria-expanded="false">複数テスト)ここをクリックでモーダルを開く2222</button>
</div>
SCSS
.p-modal {
width: 100%;
height: 100vh;
height: calc(var(--vh, 1vh) * 100);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: $header_z-index + 1;
visibility: visible;
opacity: 1;
transition: opacity .4s ease-out;
}
.p-modal.--hidden {
opacity: 0;
visibility: hidden;
transition: all .4s ease-out;
}
.p-modal__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: inherit;
height: calc(var(--vh, 1vh) * 100);
background-color: rgba(0, 0, 0, .6);
}
.p-modal__content {
background-color: #fff;
padding: 30px 50px;
border-radius: 10px;
border: 4px solid rgb(89, 165, 246);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
z-index: $header_z-index + 1;
}
.p-modal__btn {
top: 10px;
left: 10px;
width: 30px;
height: 30px;
border: none;
display: block;
position: relative;
}
.p-modal__close {
position: absolute;
top: 0;
left: 45%;
width: 4px;
height: inherit;
background-color: #fff;
border-radius: 2px;
}
.p-modal__close--01 {
display: inline-block;
transform: rotate(45deg);
}
.p-modal__close--02 {
display: inline-block;
transform: rotate(-45deg);
}
Discussion