stylescape
Version:
Stylescape is a visual identity framework developed by Scape Agency.
226 lines • 8.06 kB
JavaScript
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