55 :aria-label =" isMobileNavOpen ? 'Main navigation (expanded)' : 'Main navigation'"
66 >
77 <div class =" nav__wrapper" >
8- <nuxt-link to =" /" class =" nav__name" >
8+ <nuxt-link v-once to =" /" class =" nav__name" >
99 Jack Domleo
1010 <span class =" sr-only" > - Home</span >
1111 </nuxt-link >
12- <nuxt-link to =" /" class =" nav__logo" >
12+ <nuxt-link v-once to =" /" class =" nav__logo" >
1313 <Icon v-once name =" custom:j-icon" mode =" svg" />
1414 <span class =" sr-only" >Home</span >
1515 </nuxt-link >
2222 <button
2323 v-else
2424 :aria-expanded =" expandedSubmenus.has(navItem.text)"
25- :aria-controls =" `submenu-${ navItem.text.toLowerCase()}` "
25+ :aria-controls =" getSubmenuId( navItem.text) "
2626 aria-haspopup =" true"
2727 @click =" toggleSubmenu(navItem.text)"
2828 @keydown.esc =" closeAllSubmenus"
2929 >
3030 <span >{{ navItem.text }}</span >
31- <Icon class =" nav__item-chevron" name =" custom:chevron-down" mode =" svg" />
31+ <Icon v-once class =" nav__item-chevron" name =" custom:chevron-down" mode =" svg" />
3232 </button >
3333 <ul
3434 v-if =" navItem.submenu"
35- :id =" `submenu-${ navItem.text.toLowerCase()}` "
35+ :id =" getSubmenuId( navItem.text) "
3636 :aria-hidden =" !expandedSubmenus.has(navItem.text)"
3737 role =" menu"
3838 >
5555 class =" nav__hamburger"
5656 :class =" { 'nav__hamburger--open': isMobileNavOpen }"
5757 :aria-expanded =" isMobileNavOpen"
58- : aria-controls =" ' mobile-nav' "
58+ aria-controls =" mobile-nav"
5959 :aria-label =" isMobileNavOpen ? 'Close navigation menu' : 'Open navigation menu'"
6060 @click =" toggleMobileNav(!isMobileNavOpen)"
6161 >
62- <div class =" nav__hamburger-icon" >
62+ <div v-once class =" nav__hamburger-icon" >
6363 <span ></span >
6464 <span ></span >
6565 <span ></span >
7676 :aria-hidden =" !isMobileNavOpen"
7777 role =" menu"
7878 >
79- <li v-for =" navItem in getMobileNavItems() " :key =" navItem.text" role =" none" >
79+ <li v-for =" navItem in mobileNavItems " :key =" navItem.text" role =" none" >
8080 <nuxt-link
8181 :to =" navItem.url!"
8282 role =" menuitem"
@@ -150,8 +150,19 @@ const mobileNavItems = computed(() => {
150150 return items .filter (x => x .url )
151151})
152152
153- function getMobileNavItems(): Omit <INav , ' submenu' >[] {
154- return mobileNavItems .value
153+ // Memoized computed properties for static IDs
154+ const submenuIds = computed (() => {
155+ const ids = new Map <string , string >()
156+ for (const item of navItems ) {
157+ if (! item .url ) {
158+ ids .set (item .text , ` submenu-${item .text .toLowerCase ()} ` )
159+ }
160+ }
161+ return ids
162+ })
163+
164+ function getSubmenuId(navItemText : string ): string {
165+ return submenuIds .value .get (navItemText ) || ` submenu-${navItemText .toLowerCase ()} `
155166}
156167
157168// Focus trap functionality - memoized selectors
@@ -161,24 +172,32 @@ let lastFocusableElement: HTMLElement | null = null
161172
162173const FOCUSABLE_SELECTOR = ' a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'
163174
175+ // Optimized focus trap with early returns and cached queries
164176function setupFocusTrap() {
165177 if (! isMobileNavOpen .value ) return
166178
167179 const nav = document .querySelector (' nav.nav' ) as HTMLElement
168180 if (! nav ) return
169181
170- focusableElements = Array .from (nav .querySelectorAll (FOCUSABLE_SELECTOR )).filter (el => {
182+ const elements = nav .querySelectorAll (FOCUSABLE_SELECTOR )
183+ focusableElements = Array .from (elements ).filter (el => {
171184 const element = el as HTMLElement
172- return element .offsetWidth > 0 && element .offsetHeight > 0
185+ const rect = element .getBoundingClientRect ()
186+ return rect .width > 0 && rect .height > 0 && window .getComputedStyle (element ).visibility !== ' hidden'
173187 }) as HTMLElement []
174188
189+ if (focusableElements .length === 0 ) return
190+
175191 firstFocusableElement = focusableElements [0 ] || null
176192 lastFocusableElement = focusableElements [focusableElements .length - 1 ] || null
177193
178- // Focus the first element when opening (skip hamburger button)
179- const hamburger = document .querySelector (' .nav__hamburger' )
180- if (firstFocusableElement && firstFocusableElement !== hamburger ) {
181- setTimeout (() => firstFocusableElement ?.focus (), 100 )
194+ // Focus the first non-hamburger element when opening
195+ const hamburger = document .querySelector (' .nav__hamburger' ) as HTMLElement
196+ const targetElement = firstFocusableElement !== hamburger ? firstFocusableElement : focusableElements [1 ]
197+
198+ if (targetElement ) {
199+ // Use requestAnimationFrame for smoother focus
200+ requestAnimationFrame (() => targetElement .focus ())
182201 }
183202}
184203
@@ -198,27 +217,44 @@ function handleFocusTrap(event: KeyboardEvent) {
198217 }
199218}
200219
220+ // Optimized submenu toggle with early returns
201221function toggleSubmenu(submenuName : string ) {
202- if (expandedSubmenus .value .has (submenuName )) {
222+ const hasSubmenu = expandedSubmenus .value .has (submenuName )
223+ if (hasSubmenu ) {
203224 expandedSubmenus .value .delete (submenuName )
204225 } else {
226+ // Close other submenus for better UX (optional)
227+ expandedSubmenus .value .clear ()
205228 expandedSubmenus .value .add (submenuName )
206229 }
207230}
208231
209232function closeAllSubmenus() {
233+ if (expandedSubmenus .value .size === 0 ) return // Early return if already empty
210234 expandedSubmenus .value .clear ()
211235}
212236
237+ // Optimized event handlers with early returns
213238function handleGlobalKeydown(event : KeyboardEvent ) {
214- if (event .key === ' Escape' ) {
215- if (isMobileNavOpen .value ) {
216- toggleMobileNav (false )
217- }
239+ if (event .key !== ' Escape' ) {
240+ handleFocusTrap (event )
241+ return
242+ }
243+
244+ let handled = false
245+ if (isMobileNavOpen .value ) {
246+ toggleMobileNav (false )
247+ handled = true
248+ }
249+
250+ if (expandedSubmenus .value .size > 0 ) {
218251 closeAllSubmenus ()
252+ handled = true
219253 }
220254
221- handleFocusTrap (event )
255+ if (handled ) {
256+ event .preventDefault ()
257+ }
222258}
223259
224260watch (route , () => {
0 commit comments