@@ -46,6 +46,45 @@ watch(
4646const isSearchFocused = ref (false )
4747const searchInputRef = ref <HTMLInputElement >()
4848
49+ const selectedIndex = ref (0 )
50+
51+ const resultCount = computed (() => visibleResults .value ?.objects .length ?? 0 )
52+
53+ function clampIndex(next : number ) {
54+ if (resultCount .value <= 0 ) return 0
55+ return Math .max (0 , Math .min (resultCount .value - 1 , next ))
56+ }
57+
58+ function focusSelectedResult() {
59+ const el = document .querySelector <HTMLElement >(` [data-result-index="${selectedIndex .value }"] ` )
60+ el ?.focus ()
61+ }
62+
63+ function handleResultsKeydown(e : KeyboardEvent ) {
64+ if (resultCount .value <= 0 ) return
65+
66+ if (e .key === ' ArrowDown' ) {
67+ e .preventDefault ()
68+ selectedIndex .value = clampIndex (selectedIndex .value + 1 )
69+ focusSelectedResult ()
70+ return
71+ }
72+
73+ if (e .key === ' ArrowUp' ) {
74+ e .preventDefault ()
75+ selectedIndex .value = clampIndex (selectedIndex .value - 1 )
76+ focusSelectedResult ()
77+ return
78+ }
79+
80+ if (e .key === ' Enter' ) {
81+ const el = document .querySelector <HTMLElement >(` [data-result-index="${selectedIndex .value }"] ` )
82+ if (! el ) return
83+ e .preventDefault ()
84+ el .click ()
85+ }
86+ }
87+
4988// Track if page just loaded (for hiding "Searching..." during view transition)
5089const hasInteracted = ref (false )
5190onMounted (() => {
@@ -147,12 +186,25 @@ function handlePageChange(page: number) {
147186 updateUrlPage (page )
148187}
149188
189+ function handleSelect(index : number ) {
190+ if (index < 0 ) return
191+ selectedIndex .value = clampIndex (index )
192+ }
193+
150194// Reset pages when query changes
151195watch (query , () => {
152196 loadedPages .value = 1
153197 hasInteracted .value = true
154198})
155199
200+ watch (
201+ () => visibleResults .value ?.objects ,
202+ objects => {
203+ if (! objects ?.length ) return
204+ selectedIndex .value = 0
205+ },
206+ )
207+
156208useSeoMeta ({
157209 title : () => (query .value ? ` Search: ${query .value } - npmx ` : ' Search Packages - npmx' ),
158210})
@@ -197,6 +249,7 @@ defineOgImageComponent('Default', {
197249 class =" w-full max-w-full bg-bg-subtle border border-border rounded-lg pl-8 pr-4 py-3 font-mono text-base text-fg placeholder:text-fg-subtle transition-all duration-300 focus:(border-border-hover outline-none) appearance-none"
198250 @focus =" isSearchFocused = true"
199251 @blur =" isSearchFocused = false"
252+ @keydown =" handleResultsKeydown"
200253 />
201254 <!-- Hidden submit button for accessibility (form must have submit button per WCAG) -->
202255 <button type =" submit" class =" sr-only" >Search</button >
@@ -209,7 +262,7 @@ defineOgImageComponent('Default', {
209262
210263 <!-- Results area with container padding -->
211264 <div class =" container py-6" >
212- <section v-if =" query" aria-label =" Search results" >
265+ <section v-if =" query" aria-label =" Search results" @keydown = " handleResultsKeydown " >
213266 <!-- Initial loading (only after user interaction, not during view transition) -->
214267 <LoadingSpinner v-if =" showSearching" text =" Searching..." />
215268
@@ -235,6 +288,7 @@ defineOgImageComponent('Default', {
235288 <PackageList
236289 v-if =" visibleResults.objects.length > 0"
237290 :results =" visibleResults.objects"
291+ :selected-index =" selectedIndex"
238292 heading-level =" h2"
239293 show-publisher
240294 :has-more =" hasMore"
@@ -243,6 +297,7 @@ defineOgImageComponent('Default', {
243297 :initial-page =" initialPage"
244298 @load-more =" loadMore"
245299 @page-change =" handlePageChange"
300+ @select =" handleSelect"
246301 />
247302 </div >
248303 </section >
0 commit comments