Skip to content

Commit d4dfade

Browse files
committed
fix: remove unnecessary search landmark nested in <search>
The `<search>` element’s implicit ARIA role is `search`, so setting `role=search` on the nested `<form>` is redundant and adds noise.
1 parent 0160744 commit d4dfade

3 files changed

Lines changed: 817 additions & 829 deletions

File tree

app/components/AppHeader.vue

Lines changed: 121 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
import { debounce } from 'perfect-debounce'
33
44
withDefaults(
5-
defineProps<{
6-
showLogo?: boolean
7-
showConnector?: boolean
8-
}>(),
9-
{
10-
showLogo: true,
11-
showConnector: true,
12-
},
5+
defineProps<{
6+
showLogo?: boolean
7+
showConnector?: boolean
8+
}>(),
9+
{
10+
showLogo: true,
11+
showConnector: true,
12+
},
1313
)
1414
1515
const { isConnected, npmUser } = useConnector()
@@ -21,136 +21,130 @@ const searchQuery = ref('')
2121
const isSearchFocused = ref(false)
2222
2323
const showSearchBar = computed(() => {
24-
return route.name !== 'search' && route.name !== 'index'
24+
return route.name !== 'search' && route.name !== 'index'
2525
})
2626
2727
const debouncedNavigate = debounce(async () => {
28-
const query = searchQuery.value.trim()
29-
await router.push({
30-
name: 'search',
31-
query: query ? { q: query } : undefined,
32-
})
33-
// allow time for the navigation to occur before resetting searchQuery
34-
setTimeout(() => (searchQuery.value = ''), 1000)
28+
const query = searchQuery.value.trim()
29+
await router.push({
30+
name: 'search',
31+
query: query ? { q: query } : undefined,
32+
})
33+
// allow time for the navigation to occur before resetting searchQuery
34+
setTimeout(() => (searchQuery.value = ''), 1000)
3535
}, 100)
3636
3737
async function handleSearchInput() {
38-
debouncedNavigate()
38+
debouncedNavigate()
3939
}
4040
4141
onKeyStroke(',', e => {
42-
// Don't trigger if user is typing in an input
43-
const target = e.target as HTMLElement
44-
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
45-
return
46-
}
47-
48-
e.preventDefault()
49-
router.push('/settings')
42+
// Don't trigger if user is typing in an input
43+
const target = e.target as HTMLElement
44+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
45+
return
46+
}
47+
48+
e.preventDefault()
49+
router.push('/settings')
5050
})
5151
</script>
5252

5353
<template>
54-
<header
55-
:aria-label="$t('header.site_header')"
56-
class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border"
57-
>
58-
<nav :aria-label="$t('nav.main_navigation')" class="container h-14 flex items-center">
59-
<!-- Left: Logo -->
60-
<div class="flex-shrink-0">
61-
<NuxtLink
62-
v-if="showLogo"
63-
to="/"
64-
:aria-label="$t('header.home')"
65-
class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
66-
>
67-
<span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx
68-
</NuxtLink>
69-
<!-- Spacer when logo is hidden -->
70-
<span v-else class="w-1" />
71-
</div>
72-
73-
<!-- Center: Search bar + nav items -->
74-
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
75-
<!-- Search bar (shown on all pages except home and search) -->
76-
<search v-if="showSearchBar" class="hidden sm:block flex-1 max-w-md">
77-
<form
78-
role="search"
79-
method="GET"
80-
action="/search"
81-
class="relative"
82-
@submit.prevent="handleSearchInput"
83-
>
84-
<label for="header-search" class="sr-only">
85-
{{ $t('search.label') }}
86-
</label>
87-
88-
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
89-
<div class="search-box relative flex items-center">
90-
<span
91-
class="absolute left-3 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 motion-reduce:transition-none group-focus-within:text-accent z-1"
92-
>
93-
/
94-
</span>
95-
96-
<input
97-
id="header-search"
98-
v-model="searchQuery"
99-
type="search"
100-
name="q"
101-
:placeholder="$t('search.placeholder')"
102-
v-bind="noCorrect"
103-
class="w-full bg-bg-subtle border border-border rounded-md pl-7 pr-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-border-color duration-300 motion-reduce:transition-none focus:border-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
104-
@input="handleSearchInput"
105-
@focus="isSearchFocused = true"
106-
@blur="isSearchFocused = false"
107-
/>
108-
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
109-
</div>
110-
</div>
111-
</form>
112-
</search>
113-
114-
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
115-
<!-- Packages dropdown (when connected) -->
116-
<li v-if="isConnected && npmUser" class="flex items-center">
117-
<HeaderPackagesDropdown :username="npmUser" />
118-
</li>
119-
120-
<!-- Orgs dropdown (when connected) -->
121-
<li v-if="isConnected && npmUser" class="flex items-center">
122-
<HeaderOrgsDropdown :username="npmUser" />
123-
</li>
124-
</ul>
125-
</div>
126-
127-
<!-- Right: User status + GitHub -->
128-
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ml-auto sm:ml-0">
129-
<NuxtLink
130-
to="/about"
131-
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
132-
>
133-
{{ $t('footer.about') }}
134-
</NuxtLink>
135-
136-
<NuxtLink
137-
to="/settings"
138-
class="link-subtle font-mono text-sm inline-flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
139-
aria-keyshortcuts=","
140-
>
141-
{{ $t('nav.settings') }}
142-
<kbd
143-
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
144-
aria-hidden="true"
145-
>
146-
,
147-
</kbd>
148-
</NuxtLink>
149-
150-
<div v-if="showConnector" class="hidden sm:block">
151-
<ConnectorStatus />
152-
</div>
153-
</div>
154-
</nav>
155-
</header>
54+
<header
55+
:aria-label="$t('header.site_header')"
56+
class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border"
57+
>
58+
<nav :aria-label="$t('nav.main_navigation')" class="container h-14 flex items-center">
59+
<!-- Left: Logo -->
60+
<div class="flex-shrink-0">
61+
<NuxtLink
62+
v-if="showLogo"
63+
to="/"
64+
:aria-label="$t('header.home')"
65+
class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
66+
>
67+
<span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx
68+
</NuxtLink>
69+
<!-- Spacer when logo is hidden -->
70+
<span v-else class="w-1" />
71+
</div>
72+
73+
<!-- Center: Search bar + nav items -->
74+
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
75+
<!-- Search bar (shown on all pages except home and search) -->
76+
<search v-if="showSearchBar" class="hidden sm:block flex-1 max-w-md">
77+
<form method="GET" action="/search" class="relative" @submit.prevent="handleSearchInput">
78+
<label for="header-search" class="sr-only">
79+
{{ $t('search.label') }}
80+
</label>
81+
82+
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
83+
<div class="search-box relative flex items-center">
84+
<span
85+
class="absolute left-3 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 motion-reduce:transition-none group-focus-within:text-accent z-1"
86+
>
87+
/
88+
</span>
89+
90+
<input
91+
id="header-search"
92+
v-model="searchQuery"
93+
type="search"
94+
name="q"
95+
:placeholder="$t('search.placeholder')"
96+
v-bind="noCorrect"
97+
class="w-full bg-bg-subtle border border-border rounded-md pl-7 pr-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-border-color duration-300 motion-reduce:transition-none focus:border-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
98+
@input="handleSearchInput"
99+
@focus="isSearchFocused = true"
100+
@blur="isSearchFocused = false"
101+
/>
102+
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
103+
</div>
104+
</div>
105+
</form>
106+
</search>
107+
108+
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
109+
<!-- Packages dropdown (when connected) -->
110+
<li v-if="isConnected && npmUser" class="flex items-center">
111+
<HeaderPackagesDropdown :username="npmUser" />
112+
</li>
113+
114+
<!-- Orgs dropdown (when connected) -->
115+
<li v-if="isConnected && npmUser" class="flex items-center">
116+
<HeaderOrgsDropdown :username="npmUser" />
117+
</li>
118+
</ul>
119+
</div>
120+
121+
<!-- Right: User status + GitHub -->
122+
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ml-auto sm:ml-0">
123+
<NuxtLink
124+
to="/about"
125+
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
126+
>
127+
{{ $t('footer.about') }}
128+
</NuxtLink>
129+
130+
<NuxtLink
131+
to="/settings"
132+
class="link-subtle font-mono text-sm inline-flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
133+
aria-keyshortcuts=","
134+
>
135+
{{ $t('nav.settings') }}
136+
<kbd
137+
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
138+
aria-hidden="true"
139+
>
140+
,
141+
</kbd>
142+
</NuxtLink>
143+
144+
<div v-if="showConnector" class="hidden sm:block">
145+
<ConnectorStatus />
146+
</div>
147+
</div>
148+
</nav>
149+
</header>
156150
</template>

0 commit comments

Comments
 (0)