From d1610f4281f1525b840e90d18ad63aaeb3538eec Mon Sep 17 00:00:00 2001 From: Nonimart Date: Thu, 4 Sep 2025 16:11:08 +0200 Subject: [PATCH] FEATURE Improving the mobile menu behaviour --- resources/js/header.ts | 187 +++++++++++++++++++++++++++++------------ 1 file changed, 133 insertions(+), 54 deletions(-) diff --git a/resources/js/header.ts b/resources/js/header.ts index 9b5c7f3..8b36a89 100644 --- a/resources/js/header.ts +++ b/resources/js/header.ts @@ -1,61 +1,140 @@ -export default function menuInit() { - let main_navigation = - document.querySelector('#primary-menu'); - const header = document.querySelector('#primary-header'); - const primary_menu = - header.querySelector('#primary-menu'); - const burgerMenuToggle = header.querySelector( - '#burger-menu-toggle' - ); - const submenuToggles = primary_menu.querySelectorAll( - '.menu-item-submenu-toggle' - ); +// Déclaration pour GSAP si disponible +declare var gsap: any; - // #### Open/close burger nav - burgerMenuToggle.addEventListener('click', function (e) { +class MobileMenu { + private mobileMenu: HTMLElement | null; + private mobileMenuToggle: HTMLElement | null; + private header: HTMLElement | null; + private primaryMenu: HTMLElement | null; + private submenuToggles: NodeListOf; + + constructor() { + this.mobileMenu = document.querySelector('#menu-mobile-brand'); + this.header = document.querySelector('#primary-header'); + this.primaryMenu = this.header?.querySelector('#primary-menu-nav') || null; + this.mobileMenuToggle = this.mobileMenu?.querySelector('#mobile-menu-toggle') || null; + this.submenuToggles = + this.primaryMenu?.querySelectorAll('.menu-item-submenu-toggle') || ([] as any); + + this.init(); + } + + /** + * Initialise le menu mobile et ses événements + */ + private init(): void { + if (!this.mobileMenu || !this.header || !this.mobileMenuToggle || !this.primaryMenu) { + console.warn('Éléments du menu mobile introuvables'); + return; + } + + this.bindEvents(); + } + + /** + * Lie tous les événements du menu mobile + */ + private bindEvents(): void { + // Événement pour le toggle du menu mobile + this.mobileMenuToggle?.addEventListener('click', (e) => this.handleToggleClick(e)); + + // Événement pour fermer le menu avec Tab + document.addEventListener('focusin', (e) => this.handleFocusOut(e), true); + + // Événements pour les sous-menus + this.submenuToggles.forEach((button) => { + button.addEventListener('click', (e) => this.handleSubmenuToggle(e, button)); + }); + } + + /** + * Ouvre le menu mobile + */ + public openMobileMenu(): void { + if (!this.mobileMenu || !this.mobileMenuToggle || !this.header) return; + + this.mobileMenu.setAttribute('nav-open', 'true'); + this.header.setAttribute('nav-open', 'true'); + this.mobileMenuToggle.setAttribute('aria-expanded', 'true'); + + // // Animation GSAP si disponible + // if (typeof gsap !== 'undefined' && this.primaryMenu) { + // gsap.from(this.primaryMenu, { + // opacity: 0, + // y: '-100vh', + // duration: 0.5, + // ease: 'power4.out', + // }); + // } + } + + /** + * Ferme le menu mobile + */ + public closeMobileMenu(): void { + if (!this.mobileMenu || !this.mobileMenuToggle || !this.header) return; + + this.mobileMenu.setAttribute('nav-open', 'false'); + this.header.setAttribute('nav-open', 'false'); + this.mobileMenuToggle.setAttribute('aria-expanded', 'false'); + } + + /** + * Toggle l'état du menu mobile + */ + public toggleMobileMenu(): void { + if (!this.mobileMenu) return; + + if (this.mobileMenu.getAttribute('nav-open') === 'true') { + this.closeMobileMenu(); + this.mobileMenuToggle.textContent = 'Menu'; + } else { + this.openMobileMenu(); + this.mobileMenuToggle.textContent = 'Fermer'; + } + } + + /** + * Vérifie si le menu mobile est ouvert + */ + public isMenuOpen(): boolean { + return this.mobileMenu?.getAttribute('nav-open') === 'true' || false; + } + + /** + * Gère le clic sur le bouton toggle + */ + private handleToggleClick(e: Event): void { e.preventDefault(); - header.classList.toggle('nav-open'); - burgerMenuToggle.toggleAttribute('aria-expanded'); - gsap.from(primary_menu, { - opacity: 20, - y: '-100vh', - duration: 0.5, - ease: Power4.easeOut, - }); - }); + this.toggleMobileMenu(); + } - // #### Close nav when reaching the end of the menu with tab - document.addEventListener( - 'focusin', - (e) => { - const header = document.querySelector( - '#primary-header' - ); - if ( - header.classList.contains('nav-open') && - !header.contains(document.activeElement) - ) { - header.classList.remove('nav-open'); - burgerMenuToggle.setAttribute( - 'aria-expanded', - false - ); + /** + * Gère la fermeture du menu lors du focus en dehors + */ + private handleFocusOut(e: Event): void { + if (!this.header || !this.isMenuOpen()) return; - burgerMenuToggle.focus(); - } - }, - true - ); + const target = e.target as Element; + if (!this.header.contains(target)) { + this.closeMobileMenu(); + this.mobileMenuToggle?.focus(); + } + } - submenuToggles.forEach((button) => { - button.addEventListener('click', function (e) { - let isExpanded = - button.getAttribute('aria-expanded') === 'true'; - button.setAttribute('aria-expanded', !isExpanded); + /** + * Gère le toggle des sous-menus + */ + private handleSubmenuToggle(e: Event, button: Element): void { + e.preventDefault(); + const isExpanded = button.getAttribute('aria-expanded') === 'true'; + button.setAttribute('aria-expanded', String(!isExpanded)); - button.parentElement - .querySelector('.sub-menu') - .classList.toggle('sub-menu-open'); - }); - }); + const submenu = button.parentElement?.querySelector('.sub-menu'); + submenu?.classList.toggle('sub-menu-open'); + } +} + +export default function menuInit(): MobileMenu { + return new MobileMenu(); }