UNPKG

stylescape

Version:

Stylescape is a visual identity framework developed by Scape Agency.

221 lines 7.84 kB
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