Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const route = useRoute()
const isMobile = useIsMobile()
const isSearchExpandedManually = shallowRef(false)
const searchBoxRef = useTemplateRef('searchBoxRef')
const searchContainerRef = useTemplateRef('searchContainerRef')

// On search page, always show search expanded on mobile
const isOnHomePage = computed(() => route.name === 'index')
Expand Down Expand Up @@ -188,6 +189,12 @@ function handleSearchBlur() {
}
}

onClickOutside(searchContainerRef, () => {
if (isMobile.value && !isOnSearchPage.value) {
isSearchExpandedManually.value = false
}
})

function handleSearchFocus() {
showFullSearch.value = true
}
Expand Down Expand Up @@ -215,7 +222,7 @@ onKeyStroke(
<div class="absolute inset-0 bg-bg/80 backdrop-blur-md" />
<nav
:aria-label="$t('nav.main_navigation')"
class="relative container min-h-14 flex items-center gap-2 z-1 justify-end"
class="relative container min-h-14 flex items-center gap-2 justify-end"
>
<!-- Mobile: Logo (navigates home) -->
<NuxtLink
Expand Down Expand Up @@ -260,6 +267,7 @@ onKeyStroke(

<!-- Center: Search bar + nav items -->
<div
ref="searchContainerRef"
class="flex-1 flex items-center md:gap-6"
:class="{
'hidden sm:flex': !isSearchExpanded,
Expand All @@ -277,7 +285,7 @@ onKeyStroke(
/>
<ul
v-if="!isSearchExpanded && isConnected && npmUser"
:class="{ hidden: showFullSearch }"
:class="{ 'invisible pointer-events-none': showFullSearch }"
class="hidden sm:flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
>
<!-- Packages dropdown (when connected) -->
Expand Down
51 changes: 51 additions & 0 deletions test/nuxt/components/AppHeader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import AppHeader from '~/components/AppHeader.vue'

describe('AppHeader', () => {
it('renders a search input area', async () => {
const wrapper = await mountSuspended(AppHeader, {
route: '/',
})

// The search container with ref="searchContainerRef" should be rendered
// and contain an input element for the search functionality
const html = wrapper.html()
// The header renders a search form/input
expect(html).toContain('search')
})

it('nav links use invisible instead of hidden to prevent layout shift', async () => {
const wrapper = await mountSuspended(AppHeader, {
route: '/',
})

const html = wrapper.html()
// The nav list should use 'invisible pointer-events-none' (not 'hidden')
// when the search is expanded, to prevent layout shifts
// Verify the nav list element exists at all
const navList = wrapper.find('nav ul, ul[class*="list-none"]')
// When not on a search page and search is not expanded,
// the nav list should be visible (not invisible)
expect(navList.exists() || html.includes('list-none')).toBe(true)
})

it('renders the header element', async () => {
const wrapper = await mountSuspended(AppHeader, {
route: '/',
})

expect(wrapper.find('header').exists()).toBe(true)
})

it('renders logo link when showLogo is true', async () => {
const wrapper = await mountSuspended(AppHeader, {
route: '/package/react',
props: { showLogo: true },
})

// There should be a link to the home page (logo)
const homeLink = wrapper.find('a[href="/"]')
expect(homeLink.exists()).toBe(true)
})
})
Loading