Merge pull request 'Navigation Dropdown' (#31) from nav-dropdown into main

Reviewed-on: Webmaster/dev-ieeeucsd-org#31
Reviewed-by: Shing Hung <shing.hung@ieeeucsd.org>
This commit is contained in:
Shing Hung 2025-02-10 00:34:53 +00:00
commit fe79dd6c9c
2 changed files with 418 additions and 155 deletions

View file

@ -24,7 +24,29 @@ import pages from "../../data/pages.json";
<!-- Desktop Navigation -->
<div class="hidden md:flex md:w-[55%] md:justify-between">
{
pages.map((page) => (
pages.map((page) =>
page.subpages ? (
<div class="relative group">
<a
href={page.path}
class="uppercase rounded-full duration-300 px-[1.5vw] py-[0.2vw] text-[1.2vw] text-nowrap text-white border-white hover:opacity-50 border-[0.1vw] font-light inline-block"
>
{page.name}
</a>
<div class="absolute left-1/2 transform -translate-x-1/2 top-full pt-2 w-64 z-50">
<div class="rounded-lg bg-ieee-black border border-white shadow-lg hidden group-hover:block animate-fade-down animate-duration-200 animate-ease-in-out">
{page.subpages.map((subpage) => (
<a
href={subpage.path}
class="block px-6 py-3 text-white hover:bg-gray-700 text-[1vw] first:rounded-t-lg last:rounded-b-lg text-center whitespace-nowrap transition-colors duration-200"
>
{subpage.name}
</a>
))}
</div>
</div>
</div>
) : (
<a
href={page.path}
class={`uppercase rounded-full duration-300 px-[1.5vw] py-[0.2vw] text-[1.2vw] text-nowrap
@ -36,7 +58,8 @@ import pages from "../../data/pages.json";
>
{page.name}
</a>
))
)
)
}
</div>
@ -78,13 +101,53 @@ import pages from "../../data/pages.json";
<!-- Mobile Menu -->
<div
id="mobile-menu"
class="fixed inset-0 z-[51] hidden xl:hidden motion-safe:transition-transform motion-safe:duration-300 translate-x-full"
class="fixed inset-0 z-[51] hidden xl:hidden motion-safe:transition-transform motion-safe:duration-300 translate-x-full bg-black"
>
<div
class="flex flex-col items-center min-h-screen justify-center bg-black py-20 px-4 space-y-8"
class="flex flex-col items-center min-h-screen w-full justify-center py-20 px-4 space-y-6 overflow-y-auto"
>
{
pages.map((page) => (
pages.map((page) =>
page.subpages ? (
<div class="w-full max-w-md space-y-4 relative dropdown-container">
<div class="flex gap-2">
<a
href={page.path}
class="flex-1 block py-4 px-12 text-center rounded-[3rem] motion-safe:transition-colors motion-safe:duration-200 uppercase font-bold text-2xl text-white hover:text-gray-300 border-white border-2"
>
{page.name}
</a>
<button
class="mobile-dropdown-toggle py-4 px-6 text-center rounded-[3rem] motion-safe:transition-all motion-safe:duration-200 uppercase font-bold text-2xl text-white hover:text-gray-300 border-white border-2 flex items-center justify-center"
aria-expanded="false"
>
<svg
class="w-6 h-6 transform transition-transform duration-200 dropdown-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div>
<div class="mobile-dropdown-content mt-2 space-y-2 pl-4 absolute w-full animate-duration-200 animate-ease-in-out">
{page.subpages.map((subpage) => (
<a
href={subpage.path}
class="block py-3 px-8 text-center rounded-[2rem] motion-safe:transition-all motion-safe:duration-200 uppercase font-medium text-lg w-full text-white hover:text-gray-300 border-white border bg-[#111111] hover:bg-[#222222]"
>
{subpage.name}
</a>
))}
</div>
</div>
) : (
<a
href={page.path}
class={`block py-4 px-12 text-center rounded-[3rem] motion-safe:transition-colors motion-safe:duration-200 uppercase font-bold text-2xl w-full max-w-md
@ -96,7 +159,8 @@ import pages from "../../data/pages.json";
>
{page.name}
</a>
))
)
)
}
</div>
</div>
@ -106,6 +170,111 @@ import pages from "../../data/pages.json";
#mobile-menu.show {
@apply translate-x-0;
}
.mobile-dropdown-content {
opacity: 0;
pointer-events: none;
z-index: 30;
width: calc(100% - 1rem) !important;
animation: fadeOut 0.2s ease-in-out forwards;
}
.mobile-dropdown-content.show {
opacity: 1;
pointer-events: all;
backdrop-filter: none;
animation: fadeIn 0.2s ease-in-out forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-10px);
}
}
.mobile-dropdown-content a {
margin-inline: auto;
width: 100% !important;
max-width: calc(100% - 1rem);
}
.dropdown-icon.rotated {
transform: rotate(180deg);
}
/* Add styles for focus effect */
.dropdown-container {
transition: all 0.3s ease;
position: relative;
isolation: isolate;
}
.dropdown-container.active {
transform: scale(1.02);
z-index: 25;
backdrop-filter: none;
filter: none;
transition: all 0.2s ease;
}
.dropdown-container.active .mobile-dropdown-toggle,
.dropdown-container.active > div > a {
position: relative;
z-index: 25;
pointer-events: all;
backdrop-filter: none;
filter: none;
transition: all 0.2s ease;
}
.dropdown-container.active::after {
content: "";
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 15;
pointer-events: none;
transition: all 0.3s ease;
}
#mobile-menu.has-active-dropdown > div > *:not(.dropdown-container.active) {
opacity: 0.2;
transform: scale(0.98);
filter: blur(2px);
transition: all 0.2s ease;
pointer-events: none;
}
#mobile-menu.has-active-dropdown .dropdown-container.active {
pointer-events: all;
filter: none;
transition: all 0.2s ease;
}
#mobile-menu.has-active-dropdown
.dropdown-container.active
.mobile-dropdown-content.show {
filter: none;
backdrop-filter: none;
background: transparent;
}
</style>
<script>
@ -113,6 +282,9 @@ import pages from "../../data/pages.json";
const mobileMenu = document.getElementById("mobile-menu");
const menuIcon = document.querySelector(".menu-icon");
const closeIcon = document.querySelector(".close-icon");
const dropdownToggles = document.querySelectorAll(
".mobile-dropdown-toggle"
);
function toggleMenu(show: boolean) {
if (show) {
@ -130,9 +302,30 @@ import pages from "../../data/pages.json";
closeIcon?.classList.add("hidden");
document.body.style.overflow = "";
// First wait for the navbar to slide out
setTimeout(() => {
mobileMenu?.classList.add("hidden");
}, 100);
// Then reset all dropdowns and focus states
document
.querySelectorAll(".dropdown-container")
.forEach((el) => {
const dropdownContent = el.querySelector(
".mobile-dropdown-content"
);
const dropdownToggle = el.querySelector(
".mobile-dropdown-toggle"
);
const dropdownIcon =
dropdownToggle?.querySelector(".dropdown-icon");
el.classList.remove("active");
dropdownContent?.classList.remove("show");
dropdownIcon?.classList.remove("rotated");
dropdownToggle?.setAttribute("aria-expanded", "false");
});
mobileMenu?.classList.remove("has-active-dropdown");
}, 300);
}
}
@ -141,7 +334,63 @@ import pages from "../../data/pages.json";
toggleMenu(isMenuHidden);
});
// Close menu when clicking outside
// Handle dropdown toggles
dropdownToggles.forEach((toggle) => {
toggle.addEventListener("click", (e) => {
e.stopPropagation();
const container = toggle.closest(".dropdown-container");
const content = toggle.parentElement
?.nextElementSibling as HTMLElement;
const icon = toggle.querySelector(".dropdown-icon");
const isExpanded = toggle.getAttribute("aria-expanded") === "true";
// If clicking an already active dropdown, close it and restore interactions
if (isExpanded) {
// First close the dropdown
content?.classList.remove("show");
icon?.classList.remove("rotated");
// Then wait for animation to complete plus a delay before removing focus
setTimeout(() => {
container?.classList.remove("active");
mobileMenu?.classList.remove("has-active-dropdown");
toggle.setAttribute("aria-expanded", "false");
}, 300); // 200ms for animation + 100ms delay
return;
}
// Remove active state from all dropdowns first
document.querySelectorAll(".dropdown-container").forEach((el) => {
const dropdownContent = el.querySelector(
".mobile-dropdown-content"
);
const dropdownToggle = el.querySelector(
".mobile-dropdown-toggle"
);
const dropdownIcon =
dropdownToggle?.querySelector(".dropdown-icon");
dropdownContent?.classList.remove("show");
dropdownIcon?.classList.remove("rotated");
dropdownToggle?.setAttribute("aria-expanded", "false");
});
// First focus the new dropdown
container?.classList.add("active");
mobileMenu?.classList.add("has-active-dropdown");
toggle.setAttribute("aria-expanded", "true");
icon?.classList.add("rotated");
// Then show the content with animation after a short delay
setTimeout(() => {
requestAnimationFrame(() => {
content?.classList.add("show");
});
}, 100);
});
});
// Close menu when clicking outside ~ Doesnt really work at the moment
document.addEventListener("click", (e) => {
if (
!mobileMenu?.contains(e.target as Node) &&

View file

@ -9,7 +9,21 @@
},
{
"name": "Projects",
"path": "/projects"
"path": "/projects",
"subpages": [
{
"name": "Robocup",
"path": "/projects/robocup"
},
{
"name": "Signal Processing",
"path": "/projects/signal-processing"
},
{
"name": "Supercomputing",
"path": "/projects/supercomputing"
}
]
},
{
"name": "Board",