UNPKG

stylescape

Version:

Stylescape is a visual identity framework developed by Scape Agency.

226 lines 8.06 kB
export class ResponsiveMenuManager { constructor(menuSelector, toggleSelector, options = {}) { this.isExpanded = false; this.isMobile = false; this.focusableElements = []; this.handleToggleClick = (event) => { event.preventDefault(); this.toggleMenu(); }; this.handleOutsideClick = (event) => { if (!this.isExpanded || !this.isMobile) return; const target = event.target; const nav = this.menu?.closest("nav") || this.menu?.parentElement; if (nav && !nav.contains(target)) { this.close(); } }; this.handleKeydown = (event) => { if (!this.isExpanded) return; if (event.key === "Escape") { event.preventDefault(); this.close(); return; } if (event.key === "Tab" && this.options.trapFocus && this.isMobile) { this.handleTabKey(event); } }; this.handleLinkClick = () => { if (this.isMobile && this.isExpanded) { this.close(); } }; this.handleResize = () => { this.checkWindowSize(); }; this.menu = typeof menuSelector === "string" ? document.querySelector(menuSelector) : menuSelector; this.toggle = typeof toggleSelector === "string" ? document.querySelector(toggleSelector) : toggleSelector; this.options = { breakpoint: options.breakpoint ?? 768, expandedClass: options.expandedClass ?? "nav--expanded", mobileClass: options.mobileClass ?? "nav--mobile", closeOnLinkClick: options.closeOnLinkClick ?? true, closeOnOutsideClick: options.closeOnOutsideClick ?? true, closeOnEscape: options.closeOnEscape ?? true, animationDuration: options.animationDuration ?? 300, trapFocus: options.trapFocus ?? true, onOpen: options.onOpen ?? (() => { }), onClose: options.onClose ?? (() => { }), linkSelector: options.linkSelector ?? "a", }; if (!this.menu) { console.warn("[Stylescape] ResponsiveMenuManager menu element not found"); return; } if (!this.toggle) { console.warn("[Stylescape] ResponsiveMenuManager toggle element not found"); return; } this.init(); } get expanded() { return this.isExpanded; } get mobile() { return this.isMobile; } open() { if (!this.menu || this.isExpanded) return; this.isExpanded = true; this.menu.hidden = false; this.menu.classList.add(this.options.expandedClass); this.toggle?.setAttribute("aria-expanded", "true"); if (this.isMobile) { document.body.style.overflow = "hidden"; } if (this.options.trapFocus) { this.updateFocusableElements(); requestAnimationFrame(() => { this.focusableElements[0]?.focus(); }); } this.options.onOpen(); } close() { if (!this.menu || !this.isExpanded) return; this.isExpanded = false; this.menu.classList.remove(this.options.expandedClass); this.toggle?.setAttribute("aria-expanded", "false"); document.body.style.overflow = ""; if (this.isMobile) { setTimeout(() => { if (!this.isExpanded && this.menu) { this.menu.hidden = true; } }, this.options.animationDuration); } this.toggle?.focus(); this.options.onClose(); } toggleMenu() { if (this.isExpanded) { this.close(); } else { this.open(); } } checkWindowSize() { const wasMobile = this.isMobile; this.isMobile = window.innerWidth < this.options.breakpoint; const nav = this.menu?.closest("nav") || this.menu?.parentElement; if (this.isMobile) { nav?.classList.add(this.options.mobileClass); if (!this.isExpanded && this.menu) { this.menu.hidden = true; } } else { nav?.classList.remove(this.options.mobileClass); if (this.menu) { this.menu.hidden = false; } if (wasMobile && this.isExpanded) { this.close(); } } } destroy() { this.toggle?.removeEventListener("click", this.handleToggleClick); document.removeEventListener("click", this.handleOutsideClick); document.removeEventListener("keydown", this.handleKeydown); window.removeEventListener("resize", this.handleResize); this.menu ?.querySelectorAll(this.options.linkSelector) .forEach((link) => { link.removeEventListener("click", this.handleLinkClick); }); this.menu = null; this.toggle = null; } static initMenus() { const managers = []; document .querySelectorAll('[data-ss="responsive-menu"]') .forEach((nav) => { const toggle = nav.querySelector("[data-ss-menu-toggle]"); const menu = nav.querySelector("[data-ss-menu-content]"); if (toggle && menu) { const breakpoint = nav.dataset.ssMenuBreakpoint; managers.push(new ResponsiveMenuManager(menu, toggle, { breakpoint: breakpoint ? parseInt(breakpoint, 10) : undefined, })); } }); return managers; } init() { if (!this.menu || !this.toggle) return; const menuId = this.menu.id || `nav-menu-${Date.now()}`; this.menu.id = menuId; this.toggle.setAttribute("aria-controls", menuId); this.toggle.setAttribute("aria-expanded", "false"); this.menu.setAttribute("role", "navigation"); this.checkWindowSize(); this.toggle.addEventListener("click", this.handleToggleClick); window.addEventListener("resize", this.handleResize); if (this.options.closeOnOutsideClick) { document.addEventListener("click", this.handleOutsideClick); } if (this.options.closeOnEscape) { document.addEventListener("keydown", this.handleKeydown); } if (this.options.closeOnLinkClick) { this.menu .querySelectorAll(this.options.linkSelector) .forEach((link) => { link.addEventListener("click", this.handleLinkClick); }); } } updateFocusableElements() { if (!this.menu) return; const focusableSelectors = [ "button:not([disabled])", "a[href]", "input:not([disabled])", '[tabindex]:not([tabindex="-1"])', ].join(","); this.focusableElements = Array.from(this.menu.querySelectorAll(focusableSelectors)); } 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 ResponsiveMenuManager; //# sourceMappingURL=ResponsiveMenuManager.js.map