Skip to content

Commit 9970db3

Browse files
committed
perf(nav): Optimise component for performance
1 parent 29d27cc commit 9970db3

1 file changed

Lines changed: 58 additions & 22 deletions

File tree

app/components/Navigation.vue

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
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>
@@ -22,17 +22,17 @@
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
>
@@ -55,11 +55,11 @@
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>
@@ -76,7 +76,7 @@
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
162173
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'
163174
175+
// Optimized focus trap with early returns and cached queries
164176
function 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
201221
function 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
209232
function 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
213238
function 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
224260
watch(route, () => {

0 commit comments

Comments
 (0)