From 91f875474b5e3c62bbd7fa7dc1b327d84df024c2 Mon Sep 17 00:00:00 2001 From: Abdel LAMRANI Date: Tue, 13 Oct 2026 11:34:55 +0200 Subject: [PATCH] feat: modify the menu behavior --- resources/sass/fo/base/_layout.scss | 42 +++++++++- resources/ts/fo/partials/layout.ts | 124 ++++++++++++++++++++++------ 2 files changed, 138 insertions(+), 28 deletions(-) diff --git a/resources/sass/fo/base/_layout.scss b/resources/sass/fo/base/_layout.scss index 1bd667b9..5450c41d 100644 --- a/resources/sass/fo/base/_layout.scss +++ b/resources/sass/fo/base/_layout.scss @@ -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; diff --git a/resources/ts/fo/partials/layout.ts b/resources/ts/fo/partials/layout.ts index 11c5d142..e4e4981e 100644 --- a/resources/ts/fo/partials/layout.ts +++ b/resources/ts/fo/partials/layout.ts @@ -1,33 +1,103 @@ -document.addEventListener("DOMContentLoaded", function () { - // * COLLAPSE INIT - const navBarCollapseDesktop = document.querySelectorAll( - "#main-nav .nav-collapse" - ) as NodeListOf; - 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(".nav-collapse")) + .map(collapse => { + const trigger = root.querySelector( + `.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; - 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; - 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(); });