ksk-core
Version:
Core design system components and styles for Kickstart projects
367 lines (308 loc) • 10.7 kB
JavaScript
/**
* Navigation Component JavaScript
* Handles mobile menu, dropdown interactions, and keyboard accessibility
* For standard dropdown navigation (not mega menus)
*/
class Navigation {
constructor(element) {
// Prevent duplicate initialization
if (element.dataset.navigationInitialized === 'true') {
return;
}
// Skip initialization if this is a mega menu (handled by MegaNavigation)
if (element.classList.contains('navigation--mega')) {
return;
}
this.nav = element;
this.navigationId = this.nav.dataset.navigationId;
this.mobileAnimation = this.nav.dataset.mobileAnimation || 'left';
// Mark as initialized
this.nav.dataset.navigationInitialized = 'true';
// Elements
this.mobileToggle = this.nav.querySelector('[data-nav-toggle]');
this.mobileClose = this.nav.querySelector('[data-nav-close]');
this.menu = this.nav.querySelector('[data-nav-menu]');
this.overlay = this.nav.querySelector('[data-nav-overlay]');
this.dropdowns = this.nav.querySelectorAll('[data-dropdown]');
this.submenus = this.nav.querySelectorAll('[data-submenu]');
// State
this.isOpen = false;
this.activeDropdown = null;
this.focusedElement = null;
this.init();
}
init() {
this.bindEvents();
this.setupKeyboardNavigation();
}
bindEvents() {
// Mobile toggle
if (this.mobileToggle) {
this.mobileToggle.addEventListener('click', () => this.toggleMobile());
}
// Mobile close
if (this.mobileClose) {
this.mobileClose.addEventListener('click', () => this.closeMobile());
}
// Overlay click
if (this.overlay) {
this.overlay.addEventListener('click', () => this.closeMobile());
}
// Dropdown triggers
this.dropdowns.forEach((dropdown, index) => {
const trigger = dropdown.querySelector('[data-dropdown-trigger]');
const content = dropdown.querySelector('[data-dropdown-content]');
if (trigger && content) {
// Desktop hover
dropdown.addEventListener('mouseenter', () => {
if (window.innerWidth >= 768) {
this.openDropdown(dropdown);
}
});
dropdown.addEventListener('mouseleave', () => {
if (window.innerWidth >= 768) {
this.closeDropdown(dropdown);
}
});
// Click toggle (mobile and desktop)
trigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleDropdown(dropdown);
});
// Focus management for accessibility
trigger.addEventListener('focusout', (e) => {
setTimeout(() => {
if (!dropdown.contains(document.activeElement)) {
this.closeDropdown(dropdown);
}
}, 0);
});
content.addEventListener('focusout', (e) => {
setTimeout(() => {
if (!dropdown.contains(document.activeElement)) {
this.closeDropdown(dropdown);
}
}, 0);
});
}
});
// Submenu triggers
this.submenus.forEach(submenu => {
const trigger = submenu.querySelector('[data-submenu-trigger]');
if (trigger) {
trigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleSubmenu(submenu);
});
}
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.nav.contains(e.target)) {
this.closeAllDropdowns();
}
});
// Handle window resize
window.addEventListener('resize', () => {
if (window.innerWidth >= 768 && this.isOpen) {
this.closeMobile();
}
});
// Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (this.isOpen) {
this.closeMobile();
} else {
this.closeAllDropdowns();
}
}
});
}
setupKeyboardNavigation() {
const menuItems = this.nav.querySelectorAll('[role="menuitem"]');
menuItems.forEach((item, index) => {
item.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.focusNextItem(menuItems, index);
break;
case 'ArrowUp':
e.preventDefault();
this.focusPreviousItem(menuItems, index);
break;
case 'ArrowRight':
e.preventDefault();
this.handleRightArrow(item);
break;
case 'ArrowLeft':
e.preventDefault();
this.handleLeftArrow(item);
break;
case 'Enter':
case ' ':
e.preventDefault();
this.handleEnterSpace(item);
break;
case 'Home':
e.preventDefault();
menuItems[0].focus();
break;
case 'End':
e.preventDefault();
menuItems[menuItems.length - 1].focus();
break;
}
});
});
}
// Mobile menu methods
toggleMobile() {
if (this.isOpen) {
this.closeMobile();
} else {
this.openMobile();
}
}
openMobile() {
this.isOpen = true;
this.nav.classList.add('navigation--open');
this.menu.classList.add('navigation__menu--open');
this.overlay.classList.add('navigation__overlay--visible');
// Update ARIA
this.mobileToggle.setAttribute('aria-expanded', 'true');
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Focus first menu item
const firstMenuItem = this.menu.querySelector('[role="menuitem"]');
if (firstMenuItem) {
firstMenuItem.focus();
}
}
closeMobile() {
this.isOpen = false;
this.nav.classList.remove('navigation--open');
this.menu.classList.remove('navigation__menu--open');
this.overlay.classList.remove('navigation__overlay--visible');
// Update ARIA
this.mobileToggle.setAttribute('aria-expanded', 'false');
// Restore body scroll
document.body.style.overflow = '';
// Close all dropdowns
this.closeAllDropdowns();
// Return focus to toggle
this.mobileToggle.focus();
}
// Dropdown methods
toggleDropdown(dropdown) {
const isOpen = dropdown.classList.contains('navigation__dropdown--open');
if (isOpen) {
this.closeDropdown(dropdown);
} else {
this.closeAllDropdowns();
this.openDropdown(dropdown);
}
}
openDropdown(dropdown) {
const trigger = dropdown.querySelector('[data-dropdown-trigger]');
const content = dropdown.querySelector('[data-dropdown-content]');
dropdown.classList.add('navigation__dropdown--open');
content.classList.add('navigation__dropdown-content--open');
trigger.setAttribute('aria-expanded', 'true');
this.activeDropdown = dropdown;
}
closeDropdown(dropdown) {
const trigger = dropdown.querySelector('[data-dropdown-trigger]');
const content = dropdown.querySelector('[data-dropdown-content]');
dropdown.classList.remove('navigation__dropdown--open');
content.classList.remove('navigation__dropdown-content--open');
trigger.setAttribute('aria-expanded', 'false');
// Close all submenus within this dropdown
const submenus = dropdown.querySelectorAll('[data-submenu]');
submenus.forEach(submenu => this.closeSubmenu(submenu));
if (this.activeDropdown === dropdown) {
this.activeDropdown = null;
}
}
closeAllDropdowns() {
this.dropdowns.forEach(dropdown => this.closeDropdown(dropdown));
}
// Submenu methods
toggleSubmenu(submenu) {
const isOpen = submenu.classList.contains('navigation__submenu--open');
if (isOpen) {
this.closeSubmenu(submenu);
} else {
this.openSubmenu(submenu);
}
}
openSubmenu(submenu) {
const trigger = submenu.querySelector('[data-submenu-trigger]');
const content = submenu.querySelector('[data-submenu-content]');
submenu.classList.add('navigation__submenu--open');
content.classList.add('navigation__submenu-list--open');
trigger.setAttribute('aria-expanded', 'true');
}
closeSubmenu(submenu) {
const trigger = submenu.querySelector('[data-submenu-trigger]');
const content = submenu.querySelector('[data-submenu-content]');
submenu.classList.remove('navigation__submenu--open');
content.classList.remove('navigation__submenu-list--open');
trigger.setAttribute('aria-expanded', 'false');
}
// Keyboard navigation helpers
focusNextItem(items, currentIndex) {
const nextIndex = (currentIndex + 1) % items.length;
items[nextIndex].focus();
}
focusPreviousItem(items, currentIndex) {
const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
items[prevIndex].focus();
}
handleRightArrow(item) {
// Open submenu or dropdown if available
const dropdown = item.closest('[data-dropdown]');
const submenu = item.closest('[data-submenu]');
if (dropdown && !dropdown.classList.contains('navigation__dropdown--open')) {
this.openDropdown(dropdown);
} else if (submenu && !submenu.classList.contains('navigation__submenu--open')) {
this.openSubmenu(submenu);
}
}
handleLeftArrow(item) {
// Close current submenu or return to parent
const submenu = item.closest('[data-submenu]');
const dropdown = item.closest('[data-dropdown]');
if (submenu && submenu.classList.contains('navigation__submenu--open')) {
this.closeSubmenu(submenu);
} else if (dropdown && dropdown.classList.contains('navigation__dropdown--open')) {
this.closeDropdown(dropdown);
}
}
handleEnterSpace(item) {
// Trigger click for buttons, follow links
if (item.tagName === 'BUTTON') {
item.click();
} else if (item.tagName === 'A') {
window.location.href = item.href;
}
}
}
// Initialize all navigation components (excluding mega menus)
export function initNavigation() {
const navigationElements = document.querySelectorAll('.navigation:not(.navigation--mega)');
navigationElements.forEach(nav => {
new Navigation(nav);
});
}
// Auto-initialize when DOM is ready (for standalone usage)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNavigation);
} else {
initNavigation();
}
// Export for module usage
export default Navigation;