11import { normalizeSearchParam } from '#shared/utils/url'
2+ import { nextTick } from 'vue'
23import { 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