feat: modify the menu behavior

This commit is contained in:
Abdel LAMRANI 2026-10-13 11:34:55 +02:00
parent 12f22bf593
commit 91f875474b
2 changed files with 138 additions and 28 deletions

View file

@ -91,6 +91,8 @@ body {
left: 50%;
width: 80%;
transform: translateX(-50%);
max-height: calc(100dvh - 8rem);
overflow-y: auto;
.list-badge {
display: flex;
@ -130,6 +132,11 @@ body {
}
}
#main-nav:has(.nav-link[aria-expanded="true"])
.nav-link-active:not([aria-expanded="true"])::before {
display: none;
}
// * Mobile Offcanvas Nav
#offcanvas-main-menu {
.btn-club {
@ -137,11 +144,23 @@ body {
margin-top: -2rem;
margin-bottom: 2rem;
}
.navbar-nav {
align-items: center;
text-align: center;
}
.nav-item {
width: 100%;
flex-wrap: wrap;
justify-content: center;
}
.nav-link {
position: relative;
font-size: 1.375rem;
&-arrow {
padding-inline-end: 2rem;
&::after {
transition: .3s;
@ -174,6 +193,11 @@ body {
.nav-collapse {
width: 100%;
text-align: center;
.list-badge {
justify-content: center;
}
}
}
@ -201,7 +225,8 @@ body {
}
}
.nav-link:hover {
.nav-link:hover,
.nav-link[aria-expanded="true"] {
overflow: visible;
&::before {
@ -220,6 +245,21 @@ body {
}
}
//underline and semi-bold for submenu
.nav-collapse a:not(.btn, .badge) {
text-decoration: underline !important;
text-decoration-color: transparent !important;
text-decoration-thickness: 1px;
text-underline-offset: 0.25em;
transition: text-decoration-color 0.2s ease;
&:hover,
&:focus-visible {
font-weight: 600;
text-decoration-color: currentColor !important;
}
}
// * Adapt header in export pages because all links are hidden
.header-export {
min-height: 62px;

View file

@ -1,33 +1,103 @@
document.addEventListener("DOMContentLoaded", function () {
// * COLLAPSE INIT
const navBarCollapseDesktop = document.querySelectorAll(
"#main-nav .nav-collapse"
) as NodeListOf<HTMLDivElement>;
navBarCollapseDesktop.forEach(collapse => {
const bsCollapse = new window.bootstrap.Collapse(collapse, { toggle: false });
bsCollapse.hide();
const navLinkCollapse = document.querySelector("#main-nav .nav-link[data-bs-target='#" + collapse.id + "']") as HTMLLinkElement;
navLinkCollapse.addEventListener("mouseover", () => {
bsCollapse.show();
});
navLinkCollapse.parentElement?.addEventListener("mouseleave", () => {
setTimeout(() => {
bsCollapse.hide();
}, 350);
type CollapseEntry = {
collapse: HTMLDivElement;
trigger: HTMLAnchorElement;
instance: bootstrap.Collapse;
};
/**
* Sets up the submenus for desktop and mobile.
*/
function getCollapseEntries(root: HTMLElement): CollapseEntry[] {
return Array.from(root.querySelectorAll<HTMLDivElement>(".nav-collapse"))
.map(collapse => {
const trigger = root.querySelector<HTMLAnchorElement>(
`.nav-link[data-bs-target="#${collapse.id}"]`
);
if (!trigger) return null;
trigger.removeAttribute("data-bs-toggle");
collapse.classList.remove("show");
const instance = window.bootstrap.Collapse.getOrCreateInstance(collapse, { toggle: false });
trigger.classList.add("collapsed");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-controls", collapse.id);
// mark the link as open when submenu is open
collapse.addEventListener("show.bs.collapse", e => {
if (e.target !== collapse) return;
trigger.classList.remove("collapsed");
trigger.setAttribute("aria-expanded", "true");
});
// When is closed
collapse.addEventListener("hide.bs.collapse", e => {
if (e.target !== collapse) return;
trigger.classList.add("collapsed");
trigger.setAttribute("aria-expanded", "false");
});
return { collapse, trigger, instance };
})
.filter((entry): entry is CollapseEntry => entry !== null);
}
function initDesktopMenu(): void {
const nav = document.getElementById("main-nav");
if (!nav) return;
const entries = getCollapseEntries(nav);
const closeAll = (except?: CollapseEntry) =>
entries.forEach(entry => entry !== except && entry.instance.hide());
// open on click, only one submenu at a time
entries.forEach(entry => {
entry.trigger.addEventListener("click", event => {
event.preventDefault();
closeAll(entry);
entry.instance.toggle();
});
});
// * COLLAPSE MOBILE
const navBarLinkMobile = document.querySelectorAll(
"#offcanvas-main-menu .nav-link"
) as NodeListOf<HTMLAnchorElement>;
navBarLinkMobile.forEach(anchor => {
anchor.classList.remove("collapsed");
// close when click outside
document.addEventListener("click", event => {
if (!event.isTrusted) return;
if (!nav.contains(event.target as Node)) closeAll();
});
const navBarCollapseMobile = document.querySelectorAll(
"#offcanvas-main-menu .nav-collapse"
) as NodeListOf<HTMLDivElement>;
navBarCollapseMobile.forEach(collapse => {
(new window.bootstrap.Collapse(collapse, { toggle: true })).show();
// click to ESC
document.addEventListener("keydown", event => {
if (event.key !== "Escape") return;
const open = entries.find(e => e.trigger.getAttribute("aria-expanded") === "true");
closeAll();
open?.trigger.focus();
});
}
function initMobileMenu(): void {
const offcanvas = document.getElementById("offcanvas-main-menu");
if (!offcanvas) return;
const entries = getCollapseEntries(offcanvas);
const closeOthers = (except: CollapseEntry) =>
entries.forEach(entry => entry !== except && entry.instance.hide());
// one submenu at a time
entries.forEach(entry => {
entry.trigger.addEventListener("click", event => {
event.preventDefault();
closeOthers(entry);
entry.instance.toggle();
});
});
// close all submenus when menu is closed
offcanvas.addEventListener("hidden.bs.offcanvas", () => {
entries.forEach(entry => entry.instance.hide());
});
}
document.addEventListener("DOMContentLoaded", () => {
initDesktopMenu();
initMobileMenu();
});