diff --git a/static/js/modules/Navigation.js b/static/js/modules/Navigation.js index 004bf52..ce92855 100644 --- a/static/js/modules/Navigation.js +++ b/static/js/modules/Navigation.js @@ -36,6 +36,24 @@ class Navigation { */ #subMenuElementIdentifier; + /** + * Current navigation level in sliding viewport + * @type {number} + */ + #currentLevel = 0; + + /** + * Navigation stack for breadcrumb functionality + * @type {Array} + */ + #navigationStack = []; + + /** + * Whether sliding viewport mode is enabled + * @type {boolean} + */ + #slidingViewportEnabled = false; + constructor(mobileMenuButtonId, dropDownClass, subMenuLinkClass = "sub-menu-item") { @@ -44,6 +62,9 @@ class Navigation { this.#subMenuElementIdentifier = subMenuLinkClass; this.handleEscapeKey(); + + // Initialize sliding viewport immediately if styles are detected + this.initializeSlidingViewport(); } /** @@ -93,7 +114,16 @@ class Navigation { * @param {HTMLButtonElement} button */ toggleMobileMenu(event, button) { - this.toggleAriaExpanded(event, button) + this.toggleAriaExpanded(event, button); + + const isExpanded = button.getAttribute("aria-expanded") === "true"; + + if (!isExpanded) { + // Reset sliding navigation when menu is closed + if (this.#slidingViewportEnabled) { + this.resetSlidingNavigation(); + } + } } /** @@ -138,7 +168,9 @@ class Navigation { // Note: cannot group negation -1*-1*-1 != -(1*1*1) const isNull = event.relatedTarget == null; const isNotMenuItem = !isNull && !event.relatedTarget.classList.contains("menu-vdi__toggle") - && !event.relatedTarget.classList.contains("sub-menu-item") && !event.relatedTarget.classList.contains("menu-vdi__link"); + && !event.relatedTarget.classList.contains("sub-menu-item") + && !event.relatedTarget.classList.contains("menu-vdi__link") + && !event.relatedTarget.classList.contains("menu-vdi__back"); // Don't close on back button click if (isNull || isNotMenuItem) button.setAttribute("aria-expanded", false); @@ -158,6 +190,11 @@ class Navigation { if (event.key === "Escape") { this.#mobileMenuButton.setAttribute("aria-expanded", false); this.closeAllDropDowns(); + + // Reset sliding navigation on escape + if (this.#slidingViewportEnabled) { + this.resetSlidingNavigation(); + } } }, { passive: true }); } @@ -220,6 +257,320 @@ class Navigation { } }); } + + /** + * Initialize sliding viewport navigation + * Detects if sliding viewport should be enabled and sets up the structure + */ + initializeSlidingViewport() { + // Check if we should enable sliding viewport (could be based on screen size, user preference, etc.) + this.#slidingViewportEnabled = this.shouldEnableSlidingViewport(); + + if (this.#slidingViewportEnabled) { + console.log('Sliding viewport enabled, setting up structure'); + this.setupSlidingViewportStructure(); + } else { + console.log('Sliding viewport not enabled'); + } + } + + /** + * Determine if sliding viewport should be enabled + * @returns {boolean} + */ + shouldEnableSlidingViewport() { + // Check if sliding viewport styles are loaded by testing if the CSS rule exists + // This allows CSS-based switching between navigation styles + const isMobile = window.innerWidth <= 1000; // 62.5rem converted to px + + if (!isMobile) return false; + + // Check if sliding viewport CSS is loaded by testing a specific rule + // We need to test the element within the proper context (.nav-main) + try { + const navMain = document.querySelector('.nav-main'); + if (!navMain) return false; + + const testElement = document.createElement('div'); + testElement.className = 'menu-vdi--sliding'; + testElement.style.position = 'absolute'; + testElement.style.visibility = 'hidden'; + testElement.style.height = '1px'; + testElement.style.width = '1px'; + + navMain.appendChild(testElement); + + // Get computed styles to check if sliding viewport CSS is active + const computedStyle = window.getComputedStyle(testElement); + const hasOverflowHidden = computedStyle.overflow === 'hidden'; + + navMain.removeChild(testElement); + + return hasOverflowHidden; + } catch (error) { + console.warn('Error detecting sliding viewport styles:', error); + return false; + } + } + + /** + * Setup the HTML structure for sliding viewport navigation + */ + setupSlidingViewportStructure() { + const menuContainer = document.getElementById('menu-container'); + if (!menuContainer) { + console.warn('Menu container not found'); + return; + } + + // Don't set up if already configured + if (menuContainer.classList.contains('menu-vdi--sliding')) { + console.log('Sliding viewport already configured'); + return; + } + + console.log('Setting up sliding viewport structure'); + + // Add sliding class to enable sliding styles + menuContainer.classList.add('menu-vdi--sliding'); + + // Create viewport container + const viewport = document.createElement('div'); + viewport.className = 'menu-vdi__viewport'; + viewport.setAttribute('data-current-level', '0'); + + // Create main level + const mainLevel = document.createElement('div'); + mainLevel.className = 'menu-vdi__level menu-vdi__level--main'; + mainLevel.setAttribute('data-level', '0'); + + // Move existing menu items to main level + const existingItems = Array.from(menuContainer.children); + console.log('Moving', existingItems.length, 'existing items to main level'); + + existingItems.forEach(item => { + mainLevel.appendChild(item); + }); + + // Setup click handlers for parent items in sliding mode + this.setupSlidingClickHandlers(mainLevel); + + viewport.appendChild(mainLevel); + menuContainer.appendChild(viewport); + + console.log('Sliding viewport structure complete'); + } + + /** + * Setup click handlers for parent menu items in sliding mode + * @param {HTMLElement} level + */ + setupSlidingClickHandlers(level) { + const parentButtons = level.querySelectorAll('.menu-vdi__toggle'); + + parentButtons.forEach(button => { + button.addEventListener('click', (event) => { + if (this.#slidingViewportEnabled) { + event.preventDefault(); + event.stopPropagation(); + + const parentItem = button.closest('.menu-vdi__item--parent'); + const submenu = parentItem.querySelector('.menu-vdi__submenu'); + + if (submenu) { + this.navigateToLevel(button.textContent.trim(), submenu); + } + } + }, true); // Use capture phase to intercept before other handlers + }); + } + + /** + * Navigate to a specific navigation level + * @param {string} levelTitle + * @param {HTMLElement} submenuElement + */ + navigateToLevel(levelTitle, submenuElement) { + const viewport = document.querySelector('.menu-vdi__viewport'); + if (!viewport) return; + + this.#currentLevel++; + this.#navigationStack.push({ title: levelTitle, level: this.#currentLevel }); + + // Create new level + const newLevel = document.createElement('div'); + newLevel.className = 'menu-vdi__level'; + newLevel.setAttribute('data-level', this.#currentLevel); + + // Add back button + const backButton = this.createBackButton(); + newLevel.appendChild(backButton); + + // Add level title + const levelTitleEl = document.createElement('div'); + levelTitleEl.className = 'menu-vdi__level-title'; + levelTitleEl.textContent = levelTitle; + newLevel.appendChild(levelTitleEl); + + // Clone and add submenu items + const submenuItems = submenuElement.cloneNode(true); + submenuItems.className = 'menu-vdi__level-items'; + + // Convert submenu items to level items and ensure proper visibility + const items = submenuItems.querySelectorAll('.menu-vdi__item'); + items.forEach(item => { + // Remove any nested classes that don't apply to sliding mode + item.classList.remove('menu-vdi__item--child', 'menu-vdi__item--grandchild'); + + // Handle nested items if they exist + const nestedToggle = item.querySelector('.menu-vdi__toggle'); + if (nestedToggle) { + nestedToggle.addEventListener('click', (event) => { + event.preventDefault(); + // Could implement deeper nesting here if needed + }); + } + }); + + // Remove any hidden submenu classes from nested elements to ensure visibility + const nestedSubmenus = submenuItems.querySelectorAll('.menu-vdi__submenu'); + nestedSubmenus.forEach(nestedSubmenu => { + nestedSubmenu.classList.remove('menu-vdi__submenu'); + nestedSubmenu.classList.add('menu-vdi__nested-items'); + }); + + // Ensure all list items are visible in sliding mode + const allListItems = submenuItems.querySelectorAll('li'); + allListItems.forEach(li => { + if (!li.classList.contains('menu-vdi__item')) { + li.classList.add('menu-vdi__item'); + } + }); + + newLevel.appendChild(submenuItems); + viewport.appendChild(newLevel); + + // Animate to new level + this.animateToLevel(this.#currentLevel); + } + + /** + * Create a back button for navigation levels + * @returns {HTMLElement} + */ + createBackButton() { + const backButton = document.createElement('button'); + backButton.className = 'menu-vdi__back'; + backButton.innerHTML = ` + + + + Back + `; + + backButton.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + this.navigateBack(); + }); + + return backButton; + } + + /** + * Navigate back to previous level + */ + navigateBack() { + if (this.#currentLevel > 0) { + this.#currentLevel--; + this.#navigationStack.pop(); + + // Remove the current level element + const viewport = document.querySelector('.menu-vdi__viewport'); + const currentLevelEl = viewport.querySelector(`[data-level="${this.#currentLevel + 1}"]`); + if (currentLevelEl) { + currentLevelEl.remove(); + } + + // Animate back to previous level + this.animateToLevel(this.#currentLevel); + + // Ensure menu stays open after back navigation + setTimeout(() => { + if (this.#mobileMenuButton.getAttribute("aria-expanded") === "false") { + this.#mobileMenuButton.setAttribute("aria-expanded", "true"); + } + }, 50); + } + } + + /** + * Animate viewport to specific level + * @param {number} level + */ + animateToLevel(level) { + const viewport = document.querySelector('.menu-vdi__viewport'); + if (!viewport) return; + + const translateX = -level * 100; + viewport.style.transform = `translateX(${translateX}%)`; + viewport.setAttribute('data-current-level', level); + } + + /** + * Reset sliding navigation to main level + */ + resetSlidingNavigation() { + const menuContainer = document.getElementById('menu-container'); + if (!menuContainer) return; + + if (this.#slidingViewportEnabled) { + this.#currentLevel = 0; + this.#navigationStack = []; + + const viewport = document.querySelector('.menu-vdi__viewport'); + if (viewport) { + // Remove all levels except main + const levels = viewport.querySelectorAll('.menu-vdi__level:not(.menu-vdi__level--main)'); + levels.forEach(level => level.remove()); + + // Reset position + this.animateToLevel(0); + } + } else { + // Clean up sliding structure and restore normal menu + this.cleanupSlidingStructure(); + } + } + + /** + * Clean up sliding viewport structure and restore normal menu + */ + cleanupSlidingStructure() { + const menuContainer = document.getElementById('menu-container'); + if (!menuContainer) return; + + // Remove sliding class + menuContainer.classList.remove('menu-vdi--sliding'); + + // Find viewport and move items back to container + const viewport = menuContainer.querySelector('.menu-vdi__viewport'); + if (viewport) { + const mainLevel = viewport.querySelector('.menu-vdi__level--main'); + if (mainLevel) { + // Move all items back to menu container + const items = Array.from(mainLevel.children); + items.forEach(item => { + menuContainer.appendChild(item); + }); + } + + // Remove viewport structure + viewport.remove(); + } + + console.log('Sliding structure cleaned up'); + } } export default Navigation; diff --git a/styles/navigation/index.css b/styles/navigation/index.css index bdc5f9e..cfaabe4 100644 --- a/styles/navigation/index.css +++ b/styles/navigation/index.css @@ -7,13 +7,30 @@ * - index.php * - nav-functional.php * +* Desktop Navigation Styles: * Choose one or the other for the main navigation, based on theme needs. +* @import 'nav-main-default'; (standard dropdown) +* @import 'nav-main-mega'; (mega menu style) * -* @import 'nav-main-default'; -* @import 'nav-main-mega'; +* Mobile Navigation Styles: +* Choose one for mobile navigation behavior: +* @import 'nav-mobile-accordion'; (traditional dropdown/accordion style) +* @import 'nav-mobile-sliding'; (sliding viewport style) */ @import "./nav-functional.css"; @import "./nav-aux.css"; + +/* Mobile Navigation Style - Choose one of the following: */ +/* Traditional dropdown style */ @import "./nav-main-default.css"; +/* Mega menu style */ +/* @import "./nav-main-mega.css"; */ + +/* Mobile Navigation Style - Choose one of the following: */ +/* Accordion/dropdown style */ +/* @import "./nav-mobile-accordion.css"; */ +/* Sliding viewport style */ +@import "./nav-mobile-sliding.css"; + @import "./nav-footer.css"; diff --git a/styles/navigation/nav-main-default.css b/styles/navigation/nav-main-default.css index 9aa2c77..7742244 100644 --- a/styles/navigation/nav-main-default.css +++ b/styles/navigation/nav-main-default.css @@ -1,5 +1,10 @@ /** - * VDI Navs & Menu - Main Navigation + Default Dropdown Menu Styles + * VDI Navs & Menu - Main Navigation + Default Dropdown Menu Styles (Desktop Only) + * + * This file contains only desktop navigation styles. + * For mobile navigation, choose one of: + * - nav-mobile-accordion.css (traditional dropdown/accordion style) + * - nav-mobile-sliding.css (sliding viewport style) * * Please review documentation upon first use, and, as-needed: * TODO: Add documenation link here @@ -22,46 +27,6 @@ } } -/* mobile */ -@media screen and (max-width: 62.5rem) { - .nav-main { - .nav-main__toggle { - /* display */ - @apply text-white p-3; - } - - .menu-vdi { - @apply flex-col bg-white w-[95%] right-0 z-10 py-6; - top: var(--hgtHeader); - min-height: calc(100vh - var(--hgtHeader)); - - .menu-vdi__submenu { - @apply py-2 px-7 flex-col; - } - - .menu-vdi__item { - a, - button { - /* text */ - @apply font-bold text-20px text-black hover:text-light no-underline leading-snug; - /* spacing & display */ - @apply block w-full p-4; - /* interaction */ - @apply focus-visible:bg-secondary-200 hover:bg-secondary-200; - } - - a { - @apply block w-full; - } - - button { - @apply flex w-full justify-between; - } - } - } - } -} - @media screen and (min-width: 62.5rem) { .menu-vdi { .menu-vdi__toggle { diff --git a/styles/navigation/nav-mobile-accordion.css b/styles/navigation/nav-mobile-accordion.css new file mode 100644 index 0000000..9481544 --- /dev/null +++ b/styles/navigation/nav-mobile-accordion.css @@ -0,0 +1,52 @@ +/** + * VDI Navs & Menu - Mobile Accordion Navigation + * + * Traditional mobile accordion/dropdown style navigation + * Include this file for accordion-style mobile navigation + */ + +/* Mobile accordion navigation */ +@media screen and (max-width: 62.5rem) { + .nav-main { + .nav-main__toggle { + /* display */ + @apply text-white p-3; + } + + .menu-vdi { + @apply flex-col bg-white w-[95%] right-0 z-10 py-6; + @apply absolute hidden; /* Hidden by default */ + top: var(--hgtHeader); + min-height: calc(100vh - var(--hgtHeader)); + + .menu-vdi__submenu { + @apply py-2 px-7 flex-col; + } + + .menu-vdi__item { + a, + button { + /* text */ + @apply font-bold text-20px text-black hover:text-light no-underline leading-snug; + /* spacing & display */ + @apply block w-full p-4; + /* interaction */ + @apply focus-visible:bg-secondary-200 hover:bg-secondary-200; + } + + a { + @apply block w-full; + } + + button { + @apply flex w-full justify-between; + } + } + } + + /* Show menu when toggle button is expanded */ + .nav-main__toggle[aria-expanded="true"] ~ .menu-vdi:not(.menu-vdi--sliding) { + @apply !flex; /* Use !important to override hidden */ + } + } +} \ No newline at end of file diff --git a/styles/navigation/nav-mobile-sliding.css b/styles/navigation/nav-mobile-sliding.css new file mode 100644 index 0000000..d3df530 --- /dev/null +++ b/styles/navigation/nav-mobile-sliding.css @@ -0,0 +1,130 @@ +/** + * VDI Navs & Menu - Mobile Sliding Viewport Navigation + * + * Sliding viewport style for mobile navigation where + * users can navigate through menu levels by sliding between views + * Include this file for sliding-style mobile navigation + */ + +/* Mobile sliding viewport navigation */ +@media screen and (max-width: 62.5rem) { + .nav-main { + .nav-main__toggle { + /* display */ + @apply text-white p-3; + } + + .menu-vdi--sliding { + @apply relative overflow-hidden bg-white w-[95%] right-0 z-10 py-6; + @apply absolute hidden; /* Hidden by default */ + top: var(--hgtHeader); + min-height: calc(100vh - var(--hgtHeader)); + + /* Container for all navigation levels */ + .menu-vdi__viewport { + @apply flex transition-transform duration-300 ease-in-out; + width: 100%; + } + + /* Each navigation level */ + .menu-vdi__level { + @apply w-full flex-shrink-0 flex-col; + min-width: 100%; + } + + /* Back button for secondary levels */ + .menu-vdi__back { + @apply flex items-center gap-2 p-4 border-b border-gray-200 font-bold text-black hover:bg-secondary-200 focus-visible:bg-secondary-200 cursor-pointer; + + svg { + @apply w-5 h-5; + } + } + + /* Level indicator for context */ + .menu-vdi__level-title { + @apply p-4 border-b border-gray-200 font-bold text-lg text-center bg-gray-50; + } + + /* Navigation items in sliding mode */ + .menu-vdi__item { + a, + button { + /* text */ + @apply font-bold text-20px text-black hover:text-light no-underline leading-snug; + /* spacing & display */ + @apply block w-full p-4; + /* interaction */ + @apply focus-visible:bg-secondary-200 hover:bg-secondary-200; + } + + a { + @apply block w-full; + } + + button { + @apply flex w-full justify-between items-center; + + /* Arrow indicator for items with children */ + svg { + @apply w-5 h-5; + } + } + } + + /* Hide submenu toggles in sliding mode as they become navigation buttons */ + .menu-vdi__toggle { + /* Override the accordion toggle behavior */ + &[aria-expanded="true"] { + svg { + @apply rotate-0; /* Don't rotate arrow */ + } + + +.menu-vdi__submenu { + @apply hidden; /* Don't show dropdown */ + } + } + } + + /* Hide traditional submenus in sliding mode */ + .menu-vdi__submenu { + @apply hidden; + } + + /* Ensure menu items in sliding viewport have proper styling */ + .menu-vdi__level-items { + @apply flex-col; + + .menu-vdi__item { + @apply w-full; + + a { + @apply p-4 block w-full no-underline; + } + + span { + @apply p-4 block w-full font-bold; + } + } + + /* Style nested items that were converted from submenus */ + .menu-vdi__nested-items { + @apply block; + + .menu-vdi__item { + @apply w-full; + + a { + @apply p-4 pl-8 block w-full no-underline; /* Add indent for nested items */ + } + } + } + } + } + + /* Show sliding menu when toggle button is expanded and menu has sliding class */ + .nav-main__toggle[aria-expanded="true"] + .menu-vdi--sliding { + @apply !block; /* Use !important to override hidden */ + } + } +} \ No newline at end of file