11use crate :: components:: themed_icon:: ThemedIcon ;
2+ use leptos:: ev;
23use leptos:: prelude:: RwSignal ;
34use leptos:: prelude:: * ;
45use leptos_router:: components:: A ;
56
67#[ component]
78pub fn Header ( #[ prop( optional, default = "header_background" ) ] background_class : & ' static str ) -> impl IntoView {
89 let menu_open = RwSignal :: new ( false ) ;
10+ let close_menu = move || menu_open. set ( false ) ;
11+
12+ // ESC closes the mobile menu when it is open.
13+ window_event_listener ( ev:: keydown, move |e| {
14+ if e. key ( ) == "Escape" && menu_open. get_untracked ( ) {
15+ menu_open. set ( false ) ;
16+ }
17+ } ) ;
18+
919 view ! {
1020 <header class=format!(
11- "w-full h-[80px] md :h-[160px] px-2 md:px-32 {} flex items-center justify-between z-50 m-0 p-0 relative" ,
21+ "w-full h-[80px] lg :h-[160px] px-4 sm:px-8 md:px-16 {} flex items-center justify-between z-50 m-0 p-0 relative" ,
1222 background_class,
1323 ) >
14- <div class="flex items-center space-x-6" >
24+ <div class="flex items-center space-x-6 flex-shrink-0 " >
1525 <ThemedIcon
1626 name="odplogo"
1727 alt="ODP Logo"
18- class="w-[100px ] h-[34.5px] md :w-[149px] md :h-[51.43px ] object-contain"
28+ class="w-[120px ] h-[41px] sm :w-[140px] sm :h-[48px] lg:w-[180px] lg:h-[62px ] object-contain"
1929 />
2030 </div>
2131
2232 <button
23- class="md:hidden flex flex-col justify-center items-center w-10 h-10 p-2 focus:outline-none"
24- aria-label="Open menu"
33+ class="lg:hidden flex flex-col justify-center items-center w-10 h-10 p-2 focus:outline-none"
34+ aria-label=move || if menu_open. get( ) { "Close menu" } else { "Open menu" }
35+ aria-expanded=move || if menu_open. get( ) { "true" } else { "false" }
36+ aria-controls="primary-mobile-nav"
2537 on: click=move |_| menu_open. update( |v| * v = !* v)
2638 >
2739 <span class="block w-6 h-0.5 bg-black dark:bg-white mb-1" ></span>
2840 <span class="block w-6 h-0.5 bg-black dark:bg-white mb-1" ></span>
2941 <span class="block w-6 h-0.5 bg-black dark:bg-white" ></span>
3042 </button>
3143
32- <nav class="hidden md :flex [column-gap:25px]" >
44+ <nav class="hidden lg :flex [column-gap:25px]" aria-label= "Primary " >
3345 <NavButton href="/getting-started" label="Getting Started" />
3446 <NavButton href="/projects" label="Projects" />
3547 <ExternalNavButton
@@ -40,26 +52,47 @@ pub fn Header(#[prop(optional, default = "header_background")] background_class:
4052 <NavButton href="/home" label="Home" />
4153 </nav>
4254
55+ // Backdrop: catches clicks outside the open mobile menu and dismisses it.
56+ <div
57+ class="fixed inset-0 z-40 lg:hidden"
58+ style=move || { if menu_open. get( ) { "display: block;" } else { "display: none;" } }
59+ on: click=move |_| close_menu( )
60+ aria-hidden="true"
61+ ></div>
62+
4363 <nav
44- class="absolute right-0 top-full w-[80vw] max-w-xs background_primary flex-col items-end px-4 py-4 space-y-2 shadow-lg md:hidden transition-all duration-200"
64+ id="primary-mobile-nav"
65+ aria-label="Primary"
66+ class="absolute right-0 top-full w-[80vw] max-w-xs background_primary flex-col items-end px-4 py-4 space-y-2 shadow-lg lg:hidden transition-all duration-200 z-50"
4567 style=move || if menu_open. get( ) { "display: flex;" } else { "display: none;" }
4668 >
47- <NavButton href="/getting-started" label="Getting Started" mobile=true />
48- <NavButton href="/projects" label="Projects" mobile=true />
69+ <NavButton
70+ href="/getting-started"
71+ label="Getting Started"
72+ mobile=true
73+ on_navigate=close_menu
74+ />
75+ <NavButton href="/projects" label="Projects" mobile=true on_navigate=close_menu />
4976 <ExternalNavButton
5077 href="https://opendevicepartnership.github.io/documentation/"
5178 label="Library"
5279 mobile=true
80+ on_navigate=close_menu
5381 />
54- <NavButton href="/community" label="Community" mobile=true />
55- <NavButton href="/home" label="Home" mobile=true />
82+ <NavButton href="/community" label="Community" mobile=true on_navigate=close_menu />
83+ <NavButton href="/home" label="Home" mobile=true on_navigate=close_menu />
5684 </nav>
5785 </header>
5886 }
5987}
6088
6189#[ component]
62- fn NavButton ( href : & ' static str , label : & ' static str , #[ prop( optional) ] mobile : bool ) -> impl IntoView {
90+ fn NavButton (
91+ href : & ' static str ,
92+ label : & ' static str ,
93+ #[ prop( optional) ] mobile : bool ,
94+ #[ prop( optional, into) ] on_navigate : Option < Callback < ( ) > > ,
95+ ) -> impl IntoView {
6396 let location = leptos_router:: hooks:: use_location ( ) ;
6497 let is_active = move || location. pathname . get ( ) . starts_with ( href) ;
6598
@@ -71,14 +104,25 @@ fn NavButton(href: &'static str, label: &'static str, #[prop(optional)] mobile:
71104 class: odp-header-btn-active=is_active
72105 class: odp-header-btn-active-text=is_active
73106 class: w-full=mobile
107+ attr: aria-current=move || if is_active( ) { Some ( "page" ) } else { None }
108+ on: click=move |_| {
109+ if let Some ( cb) = on_navigate {
110+ cb. run( ( ) ) ;
111+ }
112+ }
74113 >
75114 { label}
76115 </A >
77116 }
78117}
79118
80119#[ component]
81- fn ExternalNavButton ( href : & ' static str , label : & ' static str , #[ prop( optional) ] mobile : bool ) -> impl IntoView {
120+ fn ExternalNavButton (
121+ href : & ' static str ,
122+ label : & ' static str ,
123+ #[ prop( optional) ] mobile : bool ,
124+ #[ prop( optional, into) ] on_navigate : Option < Callback < ( ) > > ,
125+ ) -> impl IntoView {
82126 view ! {
83127 <a
84128 href=href
@@ -87,6 +131,12 @@ fn ExternalNavButton(href: &'static str, label: &'static str, #[prop(optional)]
87131 if mobile { " w-full" } else { "" } ,
88132 )
89133 target="_blank"
134+ rel="noopener noreferrer"
135+ on: click=move |_| {
136+ if let Some ( cb) = on_navigate {
137+ cb. run( ( ) ) ;
138+ }
139+ }
90140 >
91141 { label}
92142 </a>
0 commit comments