fix(mobile): add functional mobile hamburger menu
- Add mobile menu button with hamburger/close icons - Add full-screen mobile navigation overlay - Add JavaScript for menu toggle functionality - Prevent body scroll when menu is open - Close menu on link click, escape key, or outside click - Add proper ARIA attributes for accessibility
This commit is contained in:
@@ -7,10 +7,25 @@
|
||||
</svg>
|
||||
<span data-testid="logo-text">Blog</span>
|
||||
</a>
|
||||
|
||||
|
||||
{% include "partials/nav.html" %}
|
||||
|
||||
|
||||
<div class="header-actions" data-testid="header-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="mobile-menu-btn"
|
||||
data-testid="mobile-menu-toggle"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-nav"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="menu-icon-open">
|
||||
<path d="M3 12h18M3 6h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="menu-icon-close" style="display: none;">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
@@ -170,5 +185,153 @@
|
||||
.user-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-btn:hover {
|
||||
background-color: var(--color-hover);
|
||||
}
|
||||
|
||||
.mobile-menu-btn[aria-expanded="true"] {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary-contrast);
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 4rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-body);
|
||||
z-index: 99;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mobile-nav.is-open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-nav .nav-link {
|
||||
display: block;
|
||||
padding: 1rem 0;
|
||||
font-size: 1.25rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.mobile-nav .nav-link:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Mobile Navigation Menu -->
|
||||
<nav class="mobile-nav" id="mobile-nav" data-testid="mobile-nav" aria-label="Mobile navigation">
|
||||
<a href="/" class="nav-link {% if active_page == 'home' %}active{% endif %}" data-testid="mobile-nav-link-home">
|
||||
Home
|
||||
</a>
|
||||
<a href="/posts" class="nav-link {% if active_page == 'posts' %}active{% endif %}" data-testid="mobile-nav-link-posts">
|
||||
Posts
|
||||
</a>
|
||||
<a href="/about" class="nav-link {% if active_page == 'about' %}active{% endif %}" data-testid="mobile-nav-link-about">
|
||||
About
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const menuBtn = document.querySelector('[data-testid="mobile-menu-toggle"]');
|
||||
const mobileNav = document.getElementById('mobile-nav');
|
||||
const menuIconOpen = menuBtn?.querySelector('.menu-icon-open');
|
||||
const menuIconClose = menuBtn?.querySelector('.menu-icon-close');
|
||||
|
||||
function toggleMenu() {
|
||||
if (!mobileNav || !menuBtn) return;
|
||||
|
||||
const isOpen = mobileNav.classList.toggle('is-open');
|
||||
menuBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
|
||||
if (menuIconOpen && menuIconClose) {
|
||||
menuIconOpen.style.display = isOpen ? 'none' : 'block';
|
||||
menuIconClose.style.display = isOpen ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Prevent body scroll when menu is open
|
||||
document.body.style.overflow = isOpen ? 'hidden' : '';
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
if (!mobileNav || !menuBtn) return;
|
||||
|
||||
mobileNav.classList.remove('is-open');
|
||||
menuBtn.setAttribute('aria-expanded', 'false');
|
||||
|
||||
if (menuIconOpen && menuIconClose) {
|
||||
menuIconOpen.style.display = 'block';
|
||||
menuIconClose.style.display = 'none';
|
||||
}
|
||||
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
if (menuBtn) {
|
||||
menuBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Close menu when clicking on a link
|
||||
if (mobileNav) {
|
||||
mobileNav.querySelectorAll('a').forEach(function(link) {
|
||||
link.addEventListener('click', closeMenu);
|
||||
});
|
||||
}
|
||||
|
||||
// Close menu on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && mobileNav?.classList.contains('is-open')) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (mobileNav?.classList.contains('is-open') &&
|
||||
!mobileNav.contains(e.target) &&
|
||||
!menuBtn?.contains(e.target)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user