Skip to content

Commit 4636625

Browse files
committed
feat(search): enhance global search input handling and state management
1 parent 291c22d commit 4636625

File tree

1 file changed

+71
-5
lines changed

1 file changed

+71
-5
lines changed

app/composables/useGlobalSearch.ts

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { normalizeSearchParam } from '#shared/utils/url'
2+
import { nextTick } from 'vue'
23
import { debounce } from 'perfect-debounce'
34

45
// Pages that have their own local filter using ?q
@@ -15,8 +16,22 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
1516

1617
const router = useRouter()
1718
const route = useRoute()
19+
const getFocusedSearchInputValue = () => {
20+
if (!import.meta.client) return ''
21+
22+
const active = document.activeElement
23+
if (!(active instanceof HTMLInputElement)) return ''
24+
if (active.type !== 'search' && active.name !== 'q') return ''
25+
return active.value
26+
}
1827
// Internally used searchQuery state
1928
const searchQuery = useState<string>('search-query', () => {
29+
// Preserve fast typing before hydration (e.g. homepage autofocus search input).
30+
const focusedInputValue = getFocusedSearchInputValue()
31+
if (focusedInputValue) {
32+
return focusedInputValue
33+
}
34+
2035
if (pagesWithLocalFilter.has(route.name as string)) {
2136
return ''
2237
}
@@ -34,13 +49,28 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
3449
}
3550
})
3651

37-
// clean search input when navigating away from search page
52+
// Sync URL query to input state only on search page.
53+
// On other pages (e.g. home), keep the user's in-progress typing untouched.
3854
watch(
39-
() => route.query.q,
40-
urlQuery => {
55+
() => [route.name, route.query.q] as const,
56+
([routeName, urlQuery]) => {
57+
if (routeName !== 'search') return
58+
59+
// Never clobber in-progress typing while any search input is focused.
60+
if (import.meta.client) {
61+
const active = document.activeElement
62+
if (
63+
active instanceof HTMLInputElement &&
64+
(active.type === 'search' || active.name === 'q')
65+
) {
66+
return
67+
}
68+
}
69+
4170
const value = normalizeSearchParam(urlQuery)
42-
if (!value) searchQuery.value = ''
43-
if (!searchQuery.value) searchQuery.value = value
71+
if (searchQuery.value !== value) {
72+
searchQuery.value = value
73+
}
4474
},
4575
)
4676

@@ -101,6 +131,42 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
101131
},
102132
})
103133

134+
// When navigating back to the homepage (e.g. via logo click from /search),
135+
// reset the global search state so the home input starts fresh and re-focus
136+
// the dedicated home search input.
137+
if (import.meta.client) {
138+
watch(
139+
() => route.name,
140+
name => {
141+
if (name !== 'index') return
142+
searchQuery.value = ''
143+
committedSearchQuery.value = ''
144+
// Use nextTick so we run after the homepage has rendered.
145+
nextTick(() => {
146+
const homeInput = document.getElementById('home-search')
147+
if (homeInput instanceof HTMLInputElement) {
148+
homeInput.focus()
149+
homeInput.select()
150+
}
151+
})
152+
},
153+
{ flush: 'post' },
154+
)
155+
}
156+
157+
// On hydration, useState can reuse SSR payload (often empty), skipping initializer.
158+
// Recover fast-typed value from the focused input once on client mount.
159+
if (import.meta.client) {
160+
onMounted(() => {
161+
const focusedInputValue = getFocusedSearchInputValue()
162+
if (!focusedInputValue) return
163+
if (searchQuery.value) return
164+
165+
// Use model setter path to preserve instant-search behavior.
166+
searchQueryValue.value = focusedInputValue
167+
})
168+
}
169+
104170
return {
105171
model: searchQueryValue,
106172
committedModel: committedSearchQuery,

0 commit comments

Comments
 (0)