Skip to content

Commit b510770

Browse files
authored
Merge branch 'main' into fix/dependencies-complex-versions
2 parents b708b27 + cd566cb commit b510770

File tree

5 files changed

+103
-5
lines changed

5 files changed

+103
-5
lines changed

app/components/Package/Card.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ const numberFormatter = useNumberFormatter()
172172
size="small"
173173
:aria-pressed="props.filters?.keywords.includes(keyword)"
174174
:title="`Filter by ${keyword}`"
175-
:data-result-index="index"
176175
@click.stop="emit('clickKeyword', keyword)"
177176
>
178177
{{ keyword }}

app/pages/search.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,16 @@ watch(displayResults, results => {
453453
}
454454
})
455455
456+
/**
457+
* Focus the header search input
458+
*/
459+
function focusSearchInput() {
460+
const searchInput = document.querySelector<HTMLInputElement>(
461+
'input[type="search"], input[name="q"]',
462+
)
463+
searchInput?.focus()
464+
}
465+
456466
function handleResultsKeydown(e: KeyboardEvent) {
457467
// If the active element is an input, navigate to exact match or wait for results
458468
if (e.key === 'Enter' && document.activeElement?.tagName === 'INPUT') {
@@ -489,7 +499,12 @@ function handleResultsKeydown(e: KeyboardEvent) {
489499
490500
if (e.key === 'ArrowUp') {
491501
e.preventDefault()
492-
const nextIndex = currentIndex < 0 ? 0 : Math.max(currentIndex - 1, 0)
502+
// At first result or no result focused: return focus to search input
503+
if (currentIndex <= 0) {
504+
focusSearchInput()
505+
return
506+
}
507+
const nextIndex = currentIndex - 1
493508
const el = elements[nextIndex]
494509
if (el) focusElement(el)
495510
return

server/utils/code-highlight.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ interface LinkifyOptions {
167167
* @param html - The HTML to process
168168
* @param options - Dependencies map and optional relative import resolver
169169
*/
170-
function linkifyImports(html: string, options?: LinkifyOptions): string {
170+
export function linkifyModuleSpecifiers(html: string, options?: LinkifyOptions): string {
171171
const { dependencies, resolveRelative } = options ?? {}
172172

173173
const getHref = (moduleSpecifier: string): string | null => {
@@ -196,7 +196,7 @@ function linkifyImports(html: string, options?: LinkifyOptions): string {
196196
// Match: from keyword span followed by string span containing module specifier
197197
// Pattern: <span style="...">from</span><span style="..."> 'module'</span>
198198
let result = html.replace(
199-
/(<span[^>]*>from<\/span>)(<span[^>]*>) (['"][^'"]+['"])<\/span>/g,
199+
/(<span[^>]*> ?from<\/span>)(<span[^>]*>) (['"][^'"]+['"])<\/span>/g,
200200
(match, fromSpan, stringSpanOpen, moduleSpecifier) => {
201201
const href = getHref(moduleSpecifier)
202202
if (!href) return match
@@ -285,7 +285,7 @@ export async function highlightCode(
285285

286286
// Make import statements clickable for JS/TS languages
287287
if (IMPORT_LANGUAGES.has(language)) {
288-
html = linkifyImports(html, {
288+
html = linkifyModuleSpecifiers(html, {
289289
dependencies: options?.dependencies,
290290
resolveRelative: options?.resolveRelative,
291291
})

test/e2e/interactions.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,49 @@ test.describe('Search Pages', () => {
7373
await expect(page).toHaveURL(/\/(package|org|user)\/vue/)
7474
})
7575

76+
test('/search?q=vue → ArrowDown navigates only between results, not keyword buttons', async ({
77+
page,
78+
goto,
79+
}) => {
80+
await goto('/search?q=vue', { waitUntil: 'hydration' })
81+
82+
await expect(page.locator('text=/found \\d+|showing \\d+/i').first()).toBeVisible({
83+
timeout: 15000,
84+
})
85+
86+
const firstResult = page.locator('[data-result-index="0"]').first()
87+
const secondResult = page.locator('[data-result-index="1"]').first()
88+
await expect(firstResult).toBeVisible()
89+
await expect(secondResult).toBeVisible()
90+
91+
// ArrowDown from input focuses the first result
92+
await page.keyboard.press('ArrowDown')
93+
await expect(firstResult).toBeFocused()
94+
95+
// Second ArrowDown focuses the second result (not a keyword button within the first)
96+
await page.keyboard.press('ArrowDown')
97+
await expect(secondResult).toBeFocused()
98+
})
99+
100+
test('/search?q=vue → ArrowUp from first result returns focus to search input', async ({
101+
page,
102+
goto,
103+
}) => {
104+
await goto('/search?q=vue', { waitUntil: 'hydration' })
105+
106+
await expect(page.locator('text=/found \\d+|showing \\d+/i').first()).toBeVisible({
107+
timeout: 15000,
108+
})
109+
110+
// Navigate to first result
111+
await page.keyboard.press('ArrowDown')
112+
await expect(page.locator('[data-result-index="0"]').first()).toBeFocused()
113+
114+
// ArrowUp returns to the search input
115+
await page.keyboard.press('ArrowUp')
116+
await expect(page.locator('input[type="search"]')).toBeFocused()
117+
})
118+
76119
test('/search?q=vue → "/" focuses the search input from results', async ({ page, goto }) => {
77120
await goto('/search?q=vue', { waitUntil: 'hydration' })
78121

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { linkifyModuleSpecifiers } from '../../../../server/utils/code-highlight'
3+
4+
describe('linkifyModuleSpecifiers', () => {
5+
const dependencies = {
6+
'vue': { version: '3.4.0' },
7+
'@unocss/webpack': { version: '0.65.3' },
8+
}
9+
10+
it('should linkify import ... from "package"', () => {
11+
// Shiki output for: import { ref } from "vue"
12+
const html =
13+
'<span class="line">' +
14+
'<span style="color:#F97583">import</span>' +
15+
'<span style="color:#E1E4E8"> { ref }</span>' +
16+
'<span style="color:#F97583">from</span>' +
17+
'<span style="color:#9ECBFF"> "vue"</span>' +
18+
'</span>'
19+
20+
const result = linkifyModuleSpecifiers(html, { dependencies })
21+
expect(result).toContain('<a href="/package-code/vue/v/3.4.0" class="import-link">')
22+
})
23+
24+
it('should linkify export * from "package"', () => {
25+
// Shiki output for: export * from "@unocss/webpack"
26+
// Note: Shiki puts a leading space before "from" in the same span
27+
const html =
28+
'<span class="line">' +
29+
'<span style="color:#F97583">export</span>' +
30+
'<span style="color:#E1E4E8"> *</span>' +
31+
'<span style="color:#F97583"> from</span>' +
32+
'<span style="color:#9ECBFF"> "@unocss/webpack"</span>' +
33+
'<span style="color:#E1E4E8">;</span>' +
34+
'</span>'
35+
36+
const result = linkifyModuleSpecifiers(html, { dependencies })
37+
expect(result).toContain(
38+
'<a href="/package-code/@unocss/webpack/v/0.65.3" class="import-link">',
39+
)
40+
})
41+
})

0 commit comments

Comments
 (0)