stylescape
Version:
Stylescape is a visual identity framework developed by Scape Agency.
221 lines • 7.84 kB
JavaScript
export class Modal {
constructor(selectorOrElement, options = {}) {
this.triggerElement = null;
this.focusableElements = [];
this.isOpen = false;
this.backdropElement = null;
this.handleKeydown = (event) => {
if (event.key === "Escape" && this.options.closeOnEscape) {
event.preventDefault();
this.close();
return;
}
if (event.key === "Tab" && this.options.trapFocus) {
this.handleTabKey(event);
}
};
this.handleCloseClick = () => {
this.close();
};
this.element =
typeof selectorOrElement === "string"
? document.querySelector(selectorOrElement)
: selectorOrElement;
this.options = {
closeOnBackdrop: options.closeOnBackdrop ?? true,
closeOnEscape: options.closeOnEscape ?? true,
animationDuration: options.animationDuration ?? 300,
openClass: options.openClass ?? "modal--open",
backdropClass: options.backdropClass ?? "modal-backdrop",
trapFocus: options.trapFocus ?? true,
focusElement: options.focusElement ?? "",
returnFocus: options.returnFocus ?? true,
onOpen: options.onOpen ?? (() => { }),
onClose: options.onClose ?? (() => { }),
onBeforeClose: options.onBeforeClose ?? (() => true),
};
if (!this.element) {
console.warn("[Stylescape] Modal element not found");
return;
}
this.init();
}
get opened() {
return this.isOpen;
}
open(trigger) {
if (!this.element || this.isOpen)
return;
this.triggerElement =
trigger || document.activeElement;
this.createBackdrop();
this.element.hidden = false;
this.element.setAttribute("aria-hidden", "false");
document.body.classList.add("modal-open");
document.body.style.overflow = "hidden";
requestAnimationFrame(() => {
this.element?.classList.add(this.options.openClass);
this.backdropElement?.classList.add(`${this.options.backdropClass}--visible`);
});
this.updateFocusableElements();
this.setInitialFocus();
document.addEventListener("keydown", this.handleKeydown);
this.isOpen = true;
this.options.onOpen(this.element);
}
close() {
if (!this.element || !this.isOpen)
return;
if (this.options.onBeforeClose(this.element) === false) {
return;
}
this.element.classList.remove(this.options.openClass);
this.backdropElement?.classList.remove(`${this.options.backdropClass}--visible`);
setTimeout(() => {
if (!this.element)
return;
this.element.hidden = true;
this.element.setAttribute("aria-hidden", "true");
document.body.classList.remove("modal-open");
document.body.style.overflow = "";
this.removeBackdrop();
if (this.options.returnFocus && this.triggerElement) {
this.triggerElement.focus();
}
this.isOpen = false;
this.options.onClose(this.element);
}, this.options.animationDuration);
document.removeEventListener("keydown", this.handleKeydown);
}
toggle(trigger) {
if (this.isOpen) {
this.close();
}
else {
this.open(trigger);
}
}
setContent(html) {
const content = this.element?.querySelector("[data-ss-modal-content], .modal-content");
if (content) {
content.innerHTML = html;
this.updateFocusableElements();
}
}
destroy() {
this.close();
document.removeEventListener("keydown", this.handleKeydown);
this.element
?.querySelectorAll("[data-ss-modal-close]")
.forEach((button) => {
button.removeEventListener("click", this.handleCloseClick);
});
this.element = null;
}
static initModals() {
const modals = [];
const modalMap = new Map();
document
.querySelectorAll('[data-ss="modal"]')
.forEach((el) => {
const closeOnBackdrop = el.dataset.ssModalCloseBackdrop !== "false";
const closeOnEscape = el.dataset.ssModalCloseEscape !== "false";
const modal = new Modal(el, {
closeOnBackdrop,
closeOnEscape,
});
modals.push(modal);
if (el.id) {
modalMap.set(`#${el.id}`, modal);
}
});
document
.querySelectorAll("[data-ss-modal-trigger]")
.forEach((trigger) => {
const targetSelector = trigger.dataset.ssModalTrigger;
if (targetSelector) {
const modal = modalMap.get(targetSelector);
if (modal) {
trigger.addEventListener("click", () => modal.open(trigger));
}
}
});
return modals;
}
init() {
if (!this.element)
return;
this.element.setAttribute("role", "dialog");
this.element.setAttribute("aria-modal", "true");
this.element.setAttribute("aria-hidden", "true");
this.element.hidden = true;
this.element
.querySelectorAll("[data-ss-modal-close]")
.forEach((button) => {
button.addEventListener("click", this.handleCloseClick);
});
}
createBackdrop() {
this.backdropElement = document.createElement("div");
this.backdropElement.className = this.options.backdropClass;
if (this.options.closeOnBackdrop) {
this.backdropElement.addEventListener("click", () => this.close());
}
document.body.appendChild(this.backdropElement);
}
removeBackdrop() {
this.backdropElement?.remove();
this.backdropElement = null;
}
updateFocusableElements() {
if (!this.element)
return;
const focusableSelectors = [
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
"a[href]",
'[tabindex]:not([tabindex="-1"])',
].join(",");
this.focusableElements = Array.from(this.element.querySelectorAll(focusableSelectors));
}
setInitialFocus() {
if (!this.element)
return;
if (this.options.focusElement) {
const focusEl = this.element.querySelector(this.options.focusElement);
if (focusEl) {
focusEl.focus();
return;
}
}
if (this.focusableElements.length > 0) {
this.focusableElements[0].focus();
}
else {
this.element.setAttribute("tabindex", "-1");
this.element.focus();
}
}
handleTabKey(event) {
if (this.focusableElements.length === 0)
return;
const firstElement = this.focusableElements[0];
const lastElement = this.focusableElements[this.focusableElements.length - 1];
if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
}
else {
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
}
}
export default Modal;
//# sourceMappingURL=Modal.js.map