Skip to content

Commit 2f74680

Browse files
authored
perf: use focus to navigate search results (#587)
1 parent 5cc9f20 commit 2f74680

6 files changed

Lines changed: 45 additions & 170 deletions

File tree

app/components/PackageCard.vue

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const props = defineProps<{
77
/** Whether to show the publisher username */
88
showPublisher?: boolean
99
prefetch?: boolean
10-
selected?: boolean
1110
index?: number
1211
/** Search query for highlighting exact matches */
1312
searchQuery?: string
@@ -20,17 +19,12 @@ const isExactMatch = computed(() => {
2019
const name = props.result.package.name.toLowerCase()
2120
return query === name
2221
})
23-
24-
const emit = defineEmits<{
25-
focus: [index: number]
26-
}>()
2722
</script>
2823

2924
<template>
3025
<article
31-
class="group card-interactive scroll-mt-48 scroll-mb-6 relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50"
26+
class="group card-interactive scroll-mt-48 scroll-mb-6 relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50 focus-within:bg-bg-muted focus-within:border-border-hover"
3227
:class="{
33-
'bg-bg-muted border-border-hover': selected,
3428
'border-accent/30 bg-accent/5': isExactMatch,
3529
}"
3630
>
@@ -50,8 +44,6 @@ const emit = defineEmits<{
5044
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
5145
class="decoration-none scroll-mt-48 scroll-mb-6 after:content-[''] after:absolute after:inset-0"
5246
:data-result-index="index"
53-
@focus="index != null && emit('focus', index)"
54-
@mouseenter="index != null && emit('focus', index)"
5547
>{{ result.package.name }}</NuxtLink
5648
>
5749
<span

app/components/PackageList.vue

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ const props = defineProps<{
2929
pageSize?: PageSize
3030
/** Initial page to scroll to (1-indexed) */
3131
initialPage?: number
32-
/** Selected result index (for keyboard navigation) */
33-
selectedIndex?: number
3432
/** Search query for highlighting exact matches */
3533
searchQuery?: string
3634
/** View mode: cards or table */
@@ -48,8 +46,6 @@ const emit = defineEmits<{
4846
'loadMore': []
4947
/** Emitted when the visible page changes */
5048
'pageChange': [page: number]
51-
/** Emitted when a result is hovered/focused */
52-
'select': [index: number]
5349
/** Emitted when sort option changes (table view) */
5450
'update:sortOption': [option: SortOption]
5551
/** Emitted when a keyword is clicked */
@@ -153,9 +149,7 @@ defineExpose({
153149
:results="displayedResults"
154150
:columns="columns"
155151
v-model:sort-option="sortOption"
156-
:selected-index="selectedIndex"
157152
:is-loading="isLoading"
158-
@select="emit('select', $event)"
159153
@click-keyword="emit('clickKeyword', $event)"
160154
/>
161155
</template>
@@ -179,12 +173,10 @@ defineExpose({
179173
:result="item as NpmSearchResult"
180174
:heading-level="headingLevel"
181175
:show-publisher="showPublisher"
182-
:selected="index === (selectedIndex ?? -1)"
183176
:index="index"
184177
:search-query="searchQuery"
185178
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
186179
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
187-
@focus="emit('select', $event)"
188180
/>
189181
</div>
190182
</template>
@@ -199,7 +191,6 @@ defineExpose({
199191
:result="item"
200192
:heading-level="headingLevel"
201193
:show-publisher="showPublisher"
202-
:selected="index === (selectedIndex ?? -1)"
203194
:index="index"
204195
:search-query="searchQuery"
205196
/>
@@ -230,12 +221,10 @@ defineExpose({
230221
:result="item"
231222
:heading-level="headingLevel"
232223
:show-publisher="showPublisher"
233-
:selected="index === (selectedIndex ?? -1)"
234224
:index="index"
235225
:search-query="searchQuery"
236226
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
237227
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
238-
@focus="emit('select', $event)"
239228
/>
240229
</li>
241230
</ol>

app/components/PackageTable.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ import { buildSortOption, parseSortOption, toggleDirection } from '#shared/types
66
const props = defineProps<{
77
results: NpmSearchResult[]
88
columns: ColumnConfig[]
9-
selectedIndex?: number
109
isLoading?: boolean
1110
}>()
1211
1312
const sortOption = defineModel<SortOption>('sortOption')
1413
1514
const emit = defineEmits<{
16-
select: [index: number]
1715
clickKeyword: [keyword: string]
1816
}>()
1917
@@ -318,9 +316,7 @@ function getColumnLabelKey(id: ColumnId): string {
318316
:key="result.package.name"
319317
:result="result"
320318
:columns="columns"
321-
:selected="selectedIndex === index"
322319
:index="index"
323-
@focus="emit('select', index)"
324320
@click-keyword="emit('clickKeyword', $event)"
325321
/>
326322
</template>

app/components/PackageTableRow.vue

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import type { ColumnConfig } from '#shared/types/preferences'
55
const props = defineProps<{
66
result: NpmSearchResult
77
columns: ColumnConfig[]
8-
selected?: boolean
98
index?: number
109
}>()
1110
1211
const emit = defineEmits<{
13-
focus: []
1412
clickKeyword: [keyword: string]
1513
}>()
1614
@@ -45,10 +43,9 @@ const allMaintainersText = computed(() => {
4543

4644
<template>
4745
<tr
48-
class="border-b border-border hover:bg-bg-muted transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset focus-visible:outline-none"
49-
:class="{ 'bg-bg-muted': selected }"
46+
class="border-b border-border hover:bg-bg-muted transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset focus-visible:outline-none focus:bg-bg-muted"
5047
tabindex="0"
51-
@focus="emit('focus')"
48+
:data-result-index="index"
5249
>
5350
<!-- Name (always visible) -->
5451
<td class="py-2 px-3">

app/components/SearchSuggestionCard.vue

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,17 @@ defineProps<{
44
type: 'user' | 'org'
55
/** The name (username or org name) */
66
name: string
7-
/** Whether this suggestion is currently selected (keyboard nav) */
8-
selected?: boolean
97
/** Whether this is an exact match for the query */
108
isExactMatch?: boolean
119
/** Index for keyboard navigation */
1210
index?: number
1311
}>()
14-
15-
const emit = defineEmits<{
16-
focus: [index: number]
17-
}>()
1812
</script>
1913

2014
<template>
2115
<article
22-
class="group card-interactive relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50"
16+
class="group card-interactive relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50 focus-within:bg-bg-muted focus-within:border-border-hover"
2317
:class="{
24-
'bg-bg-muted border-border-hover': selected,
2518
'border-accent/30 bg-accent/5': isExactMatch,
2619
}"
2720
>
@@ -35,8 +28,6 @@ const emit = defineEmits<{
3528
:to="type === 'user' ? `/~${name}` : `/@${name}`"
3629
:data-suggestion-index="index"
3730
class="flex items-center gap-4 focus-visible:outline-none after:content-[''] after:absolute after:inset-0"
38-
@focus="index != null && emit('focus', index)"
39-
@mouseenter="index != null && emit('focus', index)"
4031
>
4132
<!-- Avatar placeholder -->
4233
<div

0 commit comments

Comments
 (0)