-
-
Notifications
You must be signed in to change notification settings - Fork 355
fix: search input initialization and re-focus bug #2148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| import { normalizeSearchParam } from '#shared/utils/url' | ||
| import { nextTick } from 'vue' | ||
| import { debounce } from 'perfect-debounce' | ||
|
|
||
| // Pages that have their own local filter using ?q | ||
| const pagesWithLocalFilter = new Set(['~username', 'org']) | ||
|
|
||
| export function useGlobalSearch(place: 'header' | 'content' = 'content') { | ||
| const { settings } = useSettings() | ||
| const { searchProvider } = useSearchProvider() | ||
| const searchProviderValue = computed(() => { | ||
|
|
@@ -15,8 +16,22 @@ | |
|
|
||
| const router = useRouter() | ||
| const route = useRoute() | ||
| const getFocusedSearchInputValue = () => { | ||
| if (!import.meta.client) return '' | ||
|
|
||
| const active = document.activeElement | ||
| if (!(active instanceof HTMLInputElement)) return '' | ||
| if (active.type !== 'search' && active.name !== 'q') return '' | ||
| return active.value | ||
| } | ||
| // Internally used searchQuery state | ||
| const searchQuery = useState<string>('search-query', () => { | ||
| // Preserve fast typing before hydration (e.g. homepage autofocus search input). | ||
| const focusedInputValue = getFocusedSearchInputValue() | ||
| if (focusedInputValue) { | ||
| return focusedInputValue | ||
| } | ||
|
|
||
| if (pagesWithLocalFilter.has(route.name as string)) { | ||
| return '' | ||
| } | ||
|
|
@@ -34,13 +49,28 @@ | |
| } | ||
| }) | ||
|
|
||
| // clean search input when navigating away from search page | ||
| // Sync URL query to input state only on search page. | ||
| // On other pages (e.g. home), keep the user's in-progress typing untouched. | ||
| watch( | ||
| () => route.query.q, | ||
| urlQuery => { | ||
| () => [route.name, route.query.q] as const, | ||
| ([routeName, urlQuery]) => { | ||
| if (routeName !== 'search') return | ||
|
|
||
| // Never clobber in-progress typing while any search input is focused. | ||
| if (import.meta.client) { | ||
| const active = document.activeElement | ||
| if ( | ||
| active instanceof HTMLInputElement && | ||
| (active.type === 'search' || active.name === 'q') | ||
| ) { | ||
| return | ||
| } | ||
| } | ||
|
|
||
| const value = normalizeSearchParam(urlQuery) | ||
| if (!value) searchQuery.value = '' | ||
| if (!searchQuery.value) searchQuery.value = value | ||
| if (searchQuery.value !== value) { | ||
| searchQuery.value = value | ||
| } | ||
| }, | ||
| ) | ||
|
|
||
|
|
@@ -101,6 +131,42 @@ | |
| }, | ||
| }) | ||
|
|
||
| // When navigating back to the homepage (e.g. via logo click from /search), | ||
| // reset the global search state so the home input starts fresh and re-focus | ||
| // the dedicated home search input. | ||
| if (import.meta.client) { | ||
| watch( | ||
| () => route.name, | ||
| name => { | ||
| if (name !== 'index') return | ||
| searchQuery.value = '' | ||
| committedSearchQuery.value = '' | ||
| // Use nextTick so we run after the homepage has rendered. | ||
| nextTick(() => { | ||
| const homeInput = document.getElementById('home-search') | ||
| if (homeInput instanceof HTMLInputElement) { | ||
| homeInput.focus() | ||
| homeInput.select() | ||
| } | ||
| }) | ||
| }, | ||
| { flush: 'post' }, | ||
| ) | ||
| } | ||
|
|
||
| // On hydration, useState can reuse SSR payload (often empty), skipping initializer. | ||
| // Recover fast-typed value from the focused input once on client mount. | ||
| if (import.meta.client) { | ||
| onMounted(() => { | ||
| const focusedInputValue = getFocusedSearchInputValue() | ||
| if (!focusedInputValue) return | ||
| if (searchQuery.value) return | ||
|
|
||
| // Use model setter path to preserve instant-search behavior. | ||
| searchQueryValue.value = focusedInputValue | ||
| }) | ||
|
Comment on lines
+137
to
+167
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard the mounted recovery after an intentional home reset. These blocks can fight each other on the |
||
| } | ||
|
|
||
| return { | ||
| model: searchQueryValue, | ||
| committedModel: committedSearchQuery, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not suppress every URL synchronisation while a search box is focused.
This also skips legitimate
/search?q=...history or programmatic changes. If focus stays in the input,searchQuerynever updates,committedSearchQuerystays stale too, and there is no later resynchronisation on blur.💡 Safer guard