11"use client" ;
22
3- import { useState } from "react" ;
3+ import { useState , useEffect , useCallback } from "react" ;
44import Link from "next/link" ;
55import { NavItem , isDropdown } from "@/types/navigation" ;
66import { trackEvent } from "@/lib/tracking" ;
@@ -13,10 +13,26 @@ export default function MobileMenu({ items }: MobileMenuProps) {
1313 const [ open , setOpen ] = useState ( false ) ;
1414 const [ expandedIndex , setExpandedIndex ] = useState < number | null > ( null ) ;
1515
16+ const closeMenu = useCallback ( ( ) => {
17+ setOpen ( false ) ;
18+ setExpandedIndex ( null ) ;
19+ } , [ ] ) ;
20+
1621 const toggleDropdown = ( index : number ) => {
1722 setExpandedIndex ( expandedIndex === index ? null : index ) ;
1823 } ;
1924
25+ useEffect ( ( ) => {
26+ if ( ! open ) return ;
27+ const handleKeyDown = ( e : KeyboardEvent ) => {
28+ if ( e . key === "Escape" ) {
29+ closeMenu ( ) ;
30+ }
31+ } ;
32+ document . addEventListener ( "keydown" , handleKeyDown ) ;
33+ return ( ) => document . removeEventListener ( "keydown" , handleKeyDown ) ;
34+ } , [ open , closeMenu ] ) ;
35+
2036 return (
2137 < div className = "lg:hidden" >
2238 < button
@@ -38,73 +54,94 @@ export default function MobileMenu({ items }: MobileMenuProps) {
3854 </ button >
3955
4056 { open && (
41- < nav className = "absolute left-0 right-0 top-full z-50 border-t border-border bg-white shadow-lg" >
42- < ul className = "divide-y divide-border" >
43- { items . map ( ( item , index ) =>
44- isDropdown ( item ) ? (
45- < li key = { item . label } >
46- < button
47- type = "button"
48- className = "flex w-full items-center justify-between px-4 py-3 font-medium text-dark"
49- onClick = { ( ) => toggleDropdown ( index ) }
50- >
51- { item . label }
52- < span
53- className = { `text-xs transition-transform duration-200 ${
54- expandedIndex === index ? "rotate-180" : ""
55- } `}
57+ < >
58+ < div
59+ className = "fixed inset-0 z-40"
60+ aria-hidden = "true"
61+ onClick = { closeMenu }
62+ />
63+ < nav
64+ aria-label = "Menu de navigation"
65+ className = "absolute left-0 right-0 top-full z-50 border-t border-border bg-white shadow-lg"
66+ >
67+ < ul className = "divide-y divide-border" >
68+ { items . map ( ( item , index ) =>
69+ isDropdown ( item ) ? (
70+ < li key = { item . label } >
71+ < button
72+ type = "button"
73+ className = "flex w-full items-center justify-between px-4 py-3 font-medium text-dark"
74+ onClick = { ( ) => toggleDropdown ( index ) }
75+ >
76+ { item . label }
77+ < span
78+ className = { `text-xs transition-transform duration-200 ${
79+ expandedIndex === index ? "rotate-180" : ""
80+ } `}
81+ >
82+ ▼
83+ </ span >
84+ </ button >
85+ { expandedIndex === index && (
86+ < ul className = "bg-light-gray pb-2" >
87+ { item . items . map ( ( link ) => (
88+ < li key = { link . href } >
89+ < Link
90+ href = { link . href }
91+ className = "block px-8 py-3 text-sm text-dark hover:text-primary"
92+ onClick = { closeMenu }
93+ >
94+ { link . label }
95+ </ Link >
96+ </ li >
97+ ) ) }
98+ { item . highlight && (
99+ < li key = { item . highlight . href } >
100+ < Link
101+ href = { item . highlight . href }
102+ className = "block px-8 py-3 text-sm font-semibold text-primary hover:text-primary-dark"
103+ onClick = { closeMenu }
104+ >
105+ { item . highlight . label }
106+ </ Link >
107+ </ li >
108+ ) }
109+ </ ul >
110+ ) }
111+ </ li >
112+ ) : (
113+ < li key = { item . href } >
114+ < Link
115+ href = { item . href }
116+ className = "block px-4 py-3 font-medium text-dark hover:text-primary"
117+ onClick = { closeMenu }
56118 >
57- ▼
58- </ span >
59- </ button >
60- { expandedIndex === index && (
61- < ul className = "bg-light-gray pb-2" >
62- { item . items . map ( ( link ) => (
63- < li key = { link . href } >
64- < Link
65- href = { link . href }
66- className = "block px-8 py-3 text-sm text-dark hover:text-primary"
67- onClick = { ( ) => setOpen ( false ) }
68- >
69- { link . label }
70- </ Link >
71- </ li >
72- ) ) }
73- </ ul >
74- ) }
75- </ li >
76- ) : (
77- < li key = { item . href } >
78- < Link
79- href = { item . href }
80- className = "block px-4 py-3 font-medium text-dark hover:text-primary"
81- onClick = { ( ) => setOpen ( false ) }
82- >
83- { item . label }
84- </ Link >
85- </ li >
86- ) ,
87- ) }
88- < li >
89- < Link
90- href = "/audit-symfony-gratuit"
91- className = "block px-4 py-3 font-semibold text-primary"
92- onClick = { ( ) => { trackEvent ( "cta_click" , { cta_location : "header_mobile" , cta_text : "Audit Symfony gratuit" } ) ; setOpen ( false ) ; } }
93- >
94- Audit Symfony gratuit
95- </ Link >
96- </ li >
97- < li >
98- < Link
99- href = "/contact"
100- className = "block px-4 py-3 font-semibold text-primary"
101- onClick = { ( ) => { trackEvent ( "cta_click" , { cta_location : "header_mobile" , cta_text : "Contact" } ) ; setOpen ( false ) ; } }
102- >
103- Contact
104- </ Link >
105- </ li >
106- </ ul >
107- </ nav >
119+ { item . label }
120+ </ Link >
121+ </ li >
122+ ) ,
123+ ) }
124+ < li >
125+ < Link
126+ href = "/audit-symfony-gratuit"
127+ className = "block px-4 py-3 font-semibold text-primary"
128+ onClick = { ( ) => { trackEvent ( "cta_click" , { cta_location : "header_mobile" , cta_text : "Audit Symfony gratuit" } ) ; closeMenu ( ) ; } }
129+ >
130+ Audit Symfony gratuit
131+ </ Link >
132+ </ li >
133+ < li >
134+ < Link
135+ href = "/contact"
136+ className = "block px-4 py-3 font-semibold text-primary"
137+ onClick = { ( ) => { trackEvent ( "cta_click" , { cta_location : "header_mobile" , cta_text : "Contact" } ) ; closeMenu ( ) ; } }
138+ >
139+ Contact
140+ </ Link >
141+ </ li >
142+ </ ul >
143+ </ nav >
144+ </ >
108145 ) }
109146 </ div >
110147 ) ;
0 commit comments