204 lines
5.6 KiB
JavaScript
204 lines
5.6 KiB
JavaScript
|
|
/**
|
||
|
|
* Navigation Component
|
||
|
|
* Handles mobile menu, dropdowns, and accessibility
|
||
|
|
*/
|
||
|
|
|
||
|
|
(function () {
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
class Navigation {
|
||
|
|
constructor() {
|
||
|
|
this.mobileMenuToggle = document.getElementById("mobileMenuToggle");
|
||
|
|
this.mobileMenu = document.getElementById("mobileMenu");
|
||
|
|
this.mobileMenuClose = document.getElementById("mobileMenuClose");
|
||
|
|
this.overlay = document.getElementById("mobileMenuOverlay");
|
||
|
|
this.body = document.body;
|
||
|
|
|
||
|
|
this.init();
|
||
|
|
}
|
||
|
|
|
||
|
|
init() {
|
||
|
|
this.setupMobileMenu();
|
||
|
|
this.setupAccessibility();
|
||
|
|
this.highlightCurrentPage();
|
||
|
|
this.setupKeyboardNavigation();
|
||
|
|
}
|
||
|
|
|
||
|
|
setupMobileMenu() {
|
||
|
|
// Open mobile menu
|
||
|
|
if (this.mobileMenuToggle) {
|
||
|
|
this.mobileMenuToggle.addEventListener("click", () =>
|
||
|
|
this.openMobileMenu()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close mobile menu
|
||
|
|
if (this.mobileMenuClose) {
|
||
|
|
this.mobileMenuClose.addEventListener("click", () =>
|
||
|
|
this.closeMobileMenu()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.overlay) {
|
||
|
|
this.overlay.addEventListener("click", () => this.closeMobileMenu());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close on ESC key
|
||
|
|
document.addEventListener("keydown", (e) => {
|
||
|
|
if (
|
||
|
|
e.key === "Escape" &&
|
||
|
|
this.mobileMenu &&
|
||
|
|
this.mobileMenu.classList.contains("active")
|
||
|
|
) {
|
||
|
|
this.closeMobileMenu();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
openMobileMenu() {
|
||
|
|
if (this.mobileMenu) {
|
||
|
|
this.mobileMenu.classList.add("active");
|
||
|
|
this.mobileMenu.setAttribute("aria-hidden", "false");
|
||
|
|
this.body.style.overflow = "hidden";
|
||
|
|
|
||
|
|
if (this.overlay) {
|
||
|
|
this.overlay.classList.add("active");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Focus first link
|
||
|
|
const firstLink = this.mobileMenu.querySelector("a");
|
||
|
|
if (firstLink) {
|
||
|
|
setTimeout(() => firstLink.focus(), 100);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
closeMobileMenu() {
|
||
|
|
if (this.mobileMenu) {
|
||
|
|
this.mobileMenu.classList.remove("active");
|
||
|
|
this.mobileMenu.setAttribute("aria-hidden", "true");
|
||
|
|
this.body.style.overflow = "";
|
||
|
|
|
||
|
|
if (this.overlay) {
|
||
|
|
this.overlay.classList.remove("active");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return focus to toggle button
|
||
|
|
if (this.mobileMenuToggle) {
|
||
|
|
this.mobileMenuToggle.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
setupAccessibility() {
|
||
|
|
// Wait for body to exist
|
||
|
|
if (!document.body) return;
|
||
|
|
|
||
|
|
// Add ARIA labels to nav items
|
||
|
|
const navLinks = document.querySelectorAll(".nav-link");
|
||
|
|
navLinks.forEach((link) => {
|
||
|
|
if (!link.getAttribute("aria-label")) {
|
||
|
|
link.setAttribute(
|
||
|
|
"aria-label",
|
||
|
|
`Navigate to ${link.textContent.trim()}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add skip to main content link
|
||
|
|
if (!document.getElementById("skip-to-main")) {
|
||
|
|
const skipLink = document.createElement("a");
|
||
|
|
skipLink.id = "skip-to-main";
|
||
|
|
skipLink.href = "#main-content";
|
||
|
|
skipLink.textContent = "Skip to main content";
|
||
|
|
skipLink.className = "skip-link";
|
||
|
|
document.body.insertBefore(skipLink, document.body.firstChild);
|
||
|
|
|
||
|
|
// Add styles for skip link
|
||
|
|
if (!document.getElementById("skip-link-styles")) {
|
||
|
|
const style = document.createElement("style");
|
||
|
|
style.id = "skip-link-styles";
|
||
|
|
style.textContent = `
|
||
|
|
.skip-link {
|
||
|
|
position: fixed;
|
||
|
|
top: -100px;
|
||
|
|
left: 0;
|
||
|
|
padding: 10px 20px;
|
||
|
|
background: #000;
|
||
|
|
color: #fff;
|
||
|
|
z-index: 10001;
|
||
|
|
text-decoration: none;
|
||
|
|
border-radius: 0 0 8px 0;
|
||
|
|
}
|
||
|
|
.skip-link:focus {
|
||
|
|
top: 0;
|
||
|
|
}
|
||
|
|
`;
|
||
|
|
document.head.appendChild(style);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
highlightCurrentPage() {
|
||
|
|
const currentPath = window.location.pathname;
|
||
|
|
const navLinks = document.querySelectorAll(".nav-link, .mobile-link");
|
||
|
|
|
||
|
|
navLinks.forEach((link) => {
|
||
|
|
const href = link.getAttribute("href");
|
||
|
|
if (
|
||
|
|
href &&
|
||
|
|
(currentPath === href || currentPath.startsWith(href + "/"))
|
||
|
|
) {
|
||
|
|
link.classList.add("active");
|
||
|
|
link.setAttribute("aria-current", "page");
|
||
|
|
} else {
|
||
|
|
link.classList.remove("active");
|
||
|
|
link.removeAttribute("aria-current");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
setupKeyboardNavigation() {
|
||
|
|
// Tab trap in mobile menu when open
|
||
|
|
if (this.mobileMenu) {
|
||
|
|
const focusableElements = this.mobileMenu.querySelectorAll(
|
||
|
|
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||
|
|
);
|
||
|
|
|
||
|
|
if (focusableElements.length > 0) {
|
||
|
|
const firstElement = focusableElements[0];
|
||
|
|
const lastElement = focusableElements[focusableElements.length - 1];
|
||
|
|
|
||
|
|
this.mobileMenu.addEventListener("keydown", (e) => {
|
||
|
|
if (
|
||
|
|
e.key === "Tab" &&
|
||
|
|
this.mobileMenu.classList.contains("active")
|
||
|
|
) {
|
||
|
|
if (e.shiftKey) {
|
||
|
|
if (document.activeElement === firstElement) {
|
||
|
|
e.preventDefault();
|
||
|
|
lastElement.focus();
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if (document.activeElement === lastElement) {
|
||
|
|
e.preventDefault();
|
||
|
|
firstElement.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize navigation when DOM is ready
|
||
|
|
if (document.readyState === "loading") {
|
||
|
|
document.addEventListener("DOMContentLoaded", () => {
|
||
|
|
new Navigation();
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
new Navigation();
|
||
|
|
}
|
||
|
|
})();
|