<main class="main">
<h1 class="title">ダイアログの実装例</h1>
<p class="desc">おまけ:コードをまとめる</p>
<div class="container">
<button data-open-trigger="dialog">ダイアログを開く</button>
</div>
</main>
<!-- //.main -->
<div id="dialog" class="dialog __hidden">
<div class="dialog__bglayer" data-close-trigger="dialog"></div>
<div class="dialog__container" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describeby="dialog-desc">
<h2 id="dialog-title" class="dialog__title">ダイアログです</h2>
<p id="dialog-desc" class="dialog__description">これはダイアログのサンプルです。</p>
<div class="dialog__action">
<button data-close-trigger="dialog">ダイアログを閉じる</button>
</div>
</div>
</div>
<!-- //.dialog -->
:root {
--scroll-y: 0;
}
* {
box-sizing: border-box;
}
.main {
padding: 2rem;
min-height: 100vh;
}
.container {
display: flex;
align-items: center;
margin-top: 2rem;
}
.title {
font-size: 1.25rem;
font-weight: 600;
}
.desc {
font-size: 1rem;
margin-top: 1rem;
}
.dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
transition: opacity 0.2s ease-out;
opacity: 1;
visibility: visible;
}
.dialog.__hidden {
opacity: 0;
visibility: hidden;
transition: all 0.2s ease-out;
}
.dialog__bglayer {
position: absolute;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.75);
z-index: -1;
}
.dialog__container {
background-color: #fff;
max-width: min(500px, 100%);
max-height: 100vh;
padding: 2rem;
border-radius: 5px;
overflow-y: scroll;
}
.dialog__title {
font-size: 1rem;
font-weight: 600;
}
.dialog__description {
margin-top: 1rem;
font-size: 1rem;
}
.dialog__action {
margin-top: 2rem;
}
[data-open-trigger]:focus,
[data-close-trigger]:focus {
outline: 2px solid #2962ff;
}
.fixed {
position: fixed;
top: var(--scroll-y);
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.user-select-none {
user-select: none;
user-select: none;
}
const dialogControl = () => {
const FOCUSABLE_ELEMENTS = [
"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^="-"])'
];
class Dialog {
constructor({ dialogId, openTrigger, closeTrigger, mainContents }) {
this.dialog = document.getElementById(dialogId);
this.openTriggers = [document.querySelectorAll(openTrigger)];
this.closeTriggers = [document.querySelectorAll(closeTrigger)];
this.mainContents = [document.querySelectorAll(mainContents)];
this.focusableElements = [
this.dialog.querySelectorAll(FOCUSABLE_ELEMENTS.join(","))
];
this.focusBeforeElement = null;
// bind
this.open = this.open.bind(this);
this.close = this.close.bind(this);
this.registerEvents = this.registerEvents.bind(this);
this.keydownDiaogContainer = this.keydownDiaogContainer.bind(this);
// addEventListeners
this.registerEvents();
}
registerEvents() {
this.openTriggers.forEach((trigger) => {
trigger.addEventListener("click", this.open);
});
this.closeTriggers.forEach((trigger) => {
trigger.addEventListener("click", this.close);
});
this.dialog.addEventListener("keydown", this.keydownDiaogContainer);
}
open() {
if (!this.dialog.classList.contains("__hidden")) return;
this.dialog.classList.remove("__hidden");
this.focusBeforeElement = document.activeElement;
this.focusableElements[0].focus();
this.bgScrollBehavior("fix");
this.noSelectContents(true);
this.mainContents.forEach((contet) => {
contet.setAttribute("aria-hidden", "true");
});
}
close() {
if (this.dialog.classList.contains("__hidden")) return;
this.dialog.classList.add("__hidden");
this.focusBeforeElement.focus();
this.focusBeforeElement = null;
this.bgScrollBehavior("scroll");
this.noSelectContents(false);
this.mainContents.forEach((contet) => {
contet.setAttribute("aria-hidden", "false");
});
}
keydownDiaogContainer(e) {
const firstFocusableElement = this.focusableElements[0];
const lastFocusableElement = this.focusableElements[
this.focusableElements.length - 1
];
if (e.code === "Tab") {
if (e.shiftKey) {
if (document.activeElement === firstFocusableElement) {
e.preventDefault();
lastFocusableElement.focus();
}
} else {
if (document.activeElement === lastFocusableElement) {
e.preventDefault();
firstFocusableElement.focus();
}
}
}
if (e.code === "Escape") {
this.close();
}
}
bgScrollBehavior(state) {
const isFixed = state === "fix";
if (isFixed) {
const scrollY = document.documentElement.scrollTop;
document.body.classList.add("fixed");
document.documentElement.style.setProperty(
"--scroll-y",
`${scrollY * -1}px`
);
} else {
const scrollY = parseInt(
document.documentElement.style.getPropertyValue("--scroll-y") || "0"
);
document.body.classList.remove("fixed");
window.scrollTo(0, scrollY * -1);
}
}
noSelectContents(bool) {
if (bool) {
this.mainContents.forEach((contet) => {
contet.classList.add("user-select-none");
});
} else {
this.mainContents.forEach((contet) => {
contet.classList.remove("user-select-none");
});
}
}
destroy() {
this.openTriggers.forEach((trigger) => {
trigger.removeEventListener("click", this.open);
});
this.closeTriggers.forEach((trigger) => {
trigger.removeEventListener("click", this.close);
});
this.dialog.removeEventListener("keydown", this.keydownDiaogContainer);
}
}
let dialog = null;
const init = ({ dialogId, openTrigger, closeTrigger, mainContents }) => {
if (dialog) return console.error("Dialog was already instantiated.");
dialog = new Dialog({ dialogId, openTrigger, closeTrigger, mainContents });
};
const open = () => {
if (!dialog) return console.error("Dialog is not instantiated.");
dialog.open();
};
const close = () => {
if (!dialog) return console.error("Dialog is not instantiated.");
dialog.close();
};
const destroy = () => {
if (!dialog) return console.error("Dialog is not instantiated.");
dialog.destroy();
};
return { init, open, close, destroy };
};
const dialog = dialogControl();
dialog.init({
dialogId: "dialog",
openTrigger: `*[data-open-trigger="dialog"]`,
closeTrigger: `*[data-close-trigger="dialog"]`,
mainContents: ".main"
});
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.