feature: Enhance navigation components with scroll control and styling updates
Deploy to Dreamhost / build (push) Successful in 36s
Sync TODOs with Issues / sync_todos (push) Successful in 6s

This commit is contained in:
Keith Solomon
2026-08-29 10:46:03 -05:00
parent b9bbf615c9
commit 9ad4763f8b
4 changed files with 102 additions and 34 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ $showHero = in_array(
<?php get_template_part( 'views/partials/aura-bg' ); ?> <?php get_template_part( 'views/partials/aura-bg' ); ?>
<header role="banner" class="site-header bg-secondary flex flex-col items-center justify-start"> <header role="banner" class="site-header bg-secondary flex flex-col items-center justify-start">
<div class="header__nav-main container py-4 items-center grid gap-x-8 gap-y-0 grid-cols-[auto_1fr_auto] grid-rows-[1fr] justify-between"> <div class="header__nav-main lg:container py-4 items-center flex gap-x-8 gap-y-0 justify-between">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-header__logo m-0 p-0 flex gap-x-4 items-center justify-start text-white! text-45px font-bold leading-none hover:underline"> <a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-header__logo m-0 p-0 flex gap-x-4 items-center justify-start text-white! text-45px font-bold leading-none hover:underline">
<img class="site-header__logo-img aspect-square h-full w-full max-h-12 max-w-12" src="<?php echo esc_url( $headerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?> logo" /> Keith Solomon <img class="site-header__logo-img aspect-square h-full w-full max-h-12 max-w-12" src="<?php echo esc_url( $headerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?> logo" /> Keith Solomon
</a> </a>
+93 -27
View File
@@ -54,6 +54,12 @@ class Navigation {
*/ */
#slidingViewportEnabled = false; #slidingViewportEnabled = false;
/**
* Function reference for preventing scroll (needed to remove listener)
* @type {Function}
*/
#preventScrollHandler = (e) => e.preventDefault();
constructor(mobileMenuButtonId, dropDownClass, subMenuLinkClass = "sub-menu-item") { constructor(mobileMenuButtonId, dropDownClass, subMenuLinkClass = "sub-menu-item") {
@@ -62,7 +68,7 @@ class Navigation {
this.#subMenuElementIdentifier = subMenuLinkClass; this.#subMenuElementIdentifier = subMenuLinkClass;
this.handleEscapeKey(); this.handleEscapeKey();
// Initialize sliding viewport immediately if styles are detected // Initialize sliding viewport immediately if styles are detected
this.initializeSlidingViewport(); this.initializeSlidingViewport();
} }
@@ -115,10 +121,17 @@ class Navigation {
*/ */
toggleMobileMenu(event, button) { toggleMobileMenu(event, button) {
this.toggleAriaExpanded(event, button); this.toggleAriaExpanded(event, button);
const isExpanded = button.getAttribute("aria-expanded") === "true"; const isExpanded = button.getAttribute("aria-expanded") === "true";
if (!isExpanded) { if (isExpanded) {
// Prevent scroll when menu is open
document.documentElement.style.overflow = "hidden";
document.body.style.overflow = "hidden";
} else {
// Re-enable scroll when menu is closed
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
// Reset sliding navigation when menu is closed // Reset sliding navigation when menu is closed
if (this.#slidingViewportEnabled) { if (this.#slidingViewportEnabled) {
this.resetSlidingNavigation(); this.resetSlidingNavigation();
@@ -168,16 +181,27 @@ class Navigation {
// Note: cannot group negation -1*-1*-1 != -(1*1*1) // Note: cannot group negation -1*-1*-1 != -(1*1*1)
const isNull = event.relatedTarget == null; const isNull = event.relatedTarget == null;
const isNotMenuItem = !isNull && !event.relatedTarget.classList.contains("menu-vdi__toggle") const isNotMenuItem = !isNull && !event.relatedTarget.classList.contains("menu-vdi__toggle")
&& !event.relatedTarget.classList.contains("sub-menu-item") && !event.relatedTarget.classList.contains("sub-menu-item")
&& !event.relatedTarget.classList.contains("menu-vdi__link") && !event.relatedTarget.classList.contains("menu-vdi__link")
&& !event.relatedTarget.classList.contains("menu-vdi__back"); // Don't close on back button click && !event.relatedTarget.classList.contains("menu-vdi__back"); // Don't close on back button click
if (isNull || isNotMenuItem) if (isNull || isNotMenuItem) {
button.setAttribute("aria-expanded", false); button.setAttribute("aria-expanded", false);
// Re-enable scroll when menu is closed
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
}
}, true) }, true)
} }
} }
/**
* Handles escape behaviour
* Closes all dropdown when user hits
* escape key
*
* @link {https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event}
*/
/** /**
* Handles escape behaviour * Handles escape behaviour
* Closes all dropdown when user hits * Closes all dropdown when user hits
@@ -190,7 +214,11 @@ class Navigation {
if (event.key === "Escape") { if (event.key === "Escape") {
this.#mobileMenuButton.setAttribute("aria-expanded", false); this.#mobileMenuButton.setAttribute("aria-expanded", false);
this.closeAllDropDowns(); this.closeAllDropDowns();
// Re-enable scroll when menu is closed
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
// Reset sliding navigation on escape // Reset sliding navigation on escape
if (this.#slidingViewportEnabled) { if (this.#slidingViewportEnabled) {
this.resetSlidingNavigation(); this.resetSlidingNavigation();
@@ -265,7 +293,7 @@ class Navigation {
initializeSlidingViewport() { initializeSlidingViewport() {
// Check if we should enable sliding viewport (could be based on screen size, user preference, etc.) // Check if we should enable sliding viewport (could be based on screen size, user preference, etc.)
this.#slidingViewportEnabled = this.shouldEnableSlidingViewport(); this.#slidingViewportEnabled = this.shouldEnableSlidingViewport();
if (this.#slidingViewportEnabled) { if (this.#slidingViewportEnabled) {
console.log('Sliding viewport enabled, setting up structure'); console.log('Sliding viewport enabled, setting up structure');
this.setupSlidingViewportStructure(); this.setupSlidingViewportStructure();
@@ -282,30 +310,30 @@ class Navigation {
// Check if sliding viewport styles are loaded by testing if the CSS rule exists // Check if sliding viewport styles are loaded by testing if the CSS rule exists
// This allows CSS-based switching between navigation styles // This allows CSS-based switching between navigation styles
const isMobile = window.innerWidth <= 1000; // 62.5rem converted to px const isMobile = window.innerWidth <= 1000; // 62.5rem converted to px
if (!isMobile) return false; if (!isMobile) return false;
// Check if sliding viewport CSS is loaded by testing a specific rule // Check if sliding viewport CSS is loaded by testing a specific rule
// We need to test the element within the proper context (.nav-main) // We need to test the element within the proper context (.nav-main)
try { try {
const navMain = document.querySelector('.nav-main'); const navMain = document.querySelector('.nav-main');
if (!navMain) return false; if (!navMain) return false;
const testElement = document.createElement('div'); const testElement = document.createElement('div');
testElement.className = 'menu-vdi--sliding'; testElement.className = 'menu-vdi--sliding';
testElement.style.position = 'absolute'; testElement.style.position = 'absolute';
testElement.style.visibility = 'hidden'; testElement.style.visibility = 'hidden';
testElement.style.height = '1px'; testElement.style.height = '1px';
testElement.style.width = '1px'; testElement.style.width = '1px';
navMain.appendChild(testElement); navMain.appendChild(testElement);
// Get computed styles to check if sliding viewport CSS is active // Get computed styles to check if sliding viewport CSS is active
const computedStyle = window.getComputedStyle(testElement); const computedStyle = window.getComputedStyle(testElement);
const hasOverflowHidden = computedStyle.overflow === 'hidden'; const hasOverflowHidden = computedStyle.overflow === 'hidden';
navMain.removeChild(testElement); navMain.removeChild(testElement);
return hasOverflowHidden; return hasOverflowHidden;
} catch (error) { } catch (error) {
console.warn('Error detecting sliding viewport styles:', error); console.warn('Error detecting sliding viewport styles:', error);
@@ -347,7 +375,7 @@ class Navigation {
// Move existing menu items to main level // Move existing menu items to main level
const existingItems = Array.from(menuContainer.children); const existingItems = Array.from(menuContainer.children);
console.log('Moving', existingItems.length, 'existing items to main level'); console.log('Moving', existingItems.length, 'existing items to main level');
existingItems.forEach(item => { existingItems.forEach(item => {
mainLevel.appendChild(item); mainLevel.appendChild(item);
}); });
@@ -357,7 +385,7 @@ class Navigation {
viewport.appendChild(mainLevel); viewport.appendChild(mainLevel);
menuContainer.appendChild(viewport); menuContainer.appendChild(viewport);
console.log('Sliding viewport structure complete'); console.log('Sliding viewport structure complete');
} }
@@ -367,16 +395,16 @@ class Navigation {
*/ */
setupSlidingClickHandlers(level) { setupSlidingClickHandlers(level) {
const parentButtons = level.querySelectorAll('.menu-vdi__toggle'); const parentButtons = level.querySelectorAll('.menu-vdi__toggle');
parentButtons.forEach(button => { parentButtons.forEach(button => {
button.addEventListener('click', (event) => { button.addEventListener('click', (event) => {
if (this.#slidingViewportEnabled) { if (this.#slidingViewportEnabled) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const parentItem = button.closest('.menu-vdi__item--parent'); const parentItem = button.closest('.menu-vdi__item--parent');
const submenu = parentItem.querySelector('.menu-vdi__submenu'); const submenu = parentItem.querySelector('.menu-vdi__submenu');
if (submenu) { if (submenu) {
this.navigateToLevel(button.textContent.trim(), submenu); this.navigateToLevel(button.textContent.trim(), submenu);
} }
@@ -415,13 +443,13 @@ class Navigation {
// Clone and add submenu items // Clone and add submenu items
const submenuItems = submenuElement.cloneNode(true); const submenuItems = submenuElement.cloneNode(true);
submenuItems.className = 'menu-vdi__level-items'; submenuItems.className = 'menu-vdi__level-items';
// Convert submenu items to level items and ensure proper visibility // Convert submenu items to level items and ensure proper visibility
const items = submenuItems.querySelectorAll('.menu-vdi__item'); const items = submenuItems.querySelectorAll('.menu-vdi__item');
items.forEach(item => { items.forEach(item => {
// Remove any nested classes that don't apply to sliding mode // Remove any nested classes that don't apply to sliding mode
item.classList.remove('menu-vdi__item--child', 'menu-vdi__item--grandchild'); item.classList.remove('menu-vdi__item--child', 'menu-vdi__item--grandchild');
// Handle nested items if they exist // Handle nested items if they exist
const nestedToggle = item.querySelector('.menu-vdi__toggle'); const nestedToggle = item.querySelector('.menu-vdi__toggle');
if (nestedToggle) { if (nestedToggle) {
@@ -494,7 +522,7 @@ class Navigation {
// Animate back to previous level // Animate back to previous level
this.animateToLevel(this.#currentLevel); this.animateToLevel(this.#currentLevel);
// Ensure menu stays open after back navigation // Ensure menu stays open after back navigation
setTimeout(() => { setTimeout(() => {
if (this.#mobileMenuButton.getAttribute("aria-expanded") === "false") { if (this.#mobileMenuButton.getAttribute("aria-expanded") === "false") {
@@ -523,17 +551,17 @@ class Navigation {
resetSlidingNavigation() { resetSlidingNavigation() {
const menuContainer = document.getElementById('menu-container'); const menuContainer = document.getElementById('menu-container');
if (!menuContainer) return; if (!menuContainer) return;
if (this.#slidingViewportEnabled) { if (this.#slidingViewportEnabled) {
this.#currentLevel = 0; this.#currentLevel = 0;
this.#navigationStack = []; this.#navigationStack = [];
const viewport = document.querySelector('.menu-vdi__viewport'); const viewport = document.querySelector('.menu-vdi__viewport');
if (viewport) { if (viewport) {
// Remove all levels except main // Remove all levels except main
const levels = viewport.querySelectorAll('.menu-vdi__level:not(.menu-vdi__level--main)'); const levels = viewport.querySelectorAll('.menu-vdi__level:not(.menu-vdi__level--main)');
levels.forEach(level => level.remove()); levels.forEach(level => level.remove());
// Reset position // Reset position
this.animateToLevel(0); this.animateToLevel(0);
} }
@@ -564,13 +592,51 @@ class Navigation {
menuContainer.appendChild(item); menuContainer.appendChild(item);
}); });
} }
// Remove viewport structure // Remove viewport structure
viewport.remove(); viewport.remove();
} }
console.log('Sliding structure cleaned up'); console.log('Sliding structure cleaned up');
} }
/**
* Disable scroll on the page
* Prevents scrolling in all directions
*/
disableScroll() {
try {
// Apply overflow hidden to both html and body for better compatibility
document.documentElement.style.overflow = "hidden";
document.body.style.overflow = "hidden";
// Prevent touch scroll on mobile devices
if (this.#preventScrollHandler) {
document.addEventListener("touchmove", this.#preventScrollHandler, { passive: false });
}
} catch (error) {
console.warn("Error disabling scroll:", error);
}
}
/**
* Enable scroll on the page
* Restores normal scrolling behavior
*/
enableScroll() {
try {
// Restore overflow behavior
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
// Re-enable touch scroll on mobile devices
if (this.#preventScrollHandler) {
document.removeEventListener("touchmove", this.#preventScrollHandler);
}
} catch (error) {
console.warn("Error enabling scroll:", error);
}
}
} }
export default Navigation; export default Navigation;
+1 -1
View File
@@ -16,7 +16,7 @@ $GLOBALS['ks_portfolio_link_resume'] = isset( $GLOBALS['ks_portfolio_link_resume
?> ?>
<nav class="nav-main" role="navigation" aria-label="Main"> <nav class="nav-main" role="navigation" aria-label="Main">
<?php require_once __DIR__ . '/nav-main__toggle.php'; ?> <?php require_once __DIR__ . '/nav-main__toggle.php'; ?>
<?php <?php
if ( has_nav_menu( 'main_navigation' ) ) { if ( has_nav_menu( 'main_navigation' ) ) {
+7 -5
View File
@@ -13,6 +13,7 @@ $navIcon = isset( get_field( 'header', 'option' )['nav_icon'] ) ? get_field( 'he
// Generate toggle classes // Generate toggle classes
$toggleClasses = array( 'nav-main__toggle' ); $toggleClasses = array( 'nav-main__toggle' );
if ( $navIcon ) { if ( $navIcon ) {
$toggleClasses[] = 'custom-icon'; $toggleClasses[] = 'custom-icon';
} }
@@ -29,17 +30,18 @@ if ( $navIcon ) {
<?php else : ?> <?php else : ?>
<!-- Hamburger Icon --> <!-- Hamburger Icon -->
<span class="nav-toggle-icon nav-toggle-hamburger"> <span class="nav-toggle-icon nav-toggle-hamburger">
<svg class="text-current fill-current" viewBox="0 0 100 80" width="40" height="40"> <svg class="text-current fill-[#1A2A45]" viewBox="0 0 100 60" width="40" height="40">
<rect y="0" width="100" height="15"></rect> <rect y="0" width="100" height="15"></rect>
<rect y="30" width="100" height="15"></rect> <rect y="30" width="100" height="15"></rect>
<rect y="60" width="100" height="15"></rect> <rect y="60" width="100" height="15"></rect>
</svg> </svg>
</span> </span>
<!-- X Icon -->
<!-- X Icon -->
<span class="nav-toggle-icon nav-toggle-x"> <span class="nav-toggle-icon nav-toggle-x">
<svg class="text-current stroke-current" viewBox="0 0 100 80" width="40" height="40"> <svg class="text-current stroke-[#1A2A45]" viewBox="0 0 100 100" width="40" height="40">
<line x1="20" y1="20" x2="80" y2="60" stroke-width="15" stroke-linecap="round" /> <line x1="15" y1="15" x2="85" y2="85" stroke-width="15" stroke-linecap="round" />
<line x1="80" y1="20" x2="20" y2="60" stroke-width="15" stroke-linecap="round" /> <line x1="85" y1="15" x2="15" y2="85" stroke-width="15" stroke-linecap="round" />
</svg> </svg>
</span> </span>
<?php endif; ?> <?php endif; ?>