Skip to content

fix: search input initialization and re-focus bug#2148

Open
nnecec wants to merge 1 commit intonpmx-dev:mainfrom
nnecec:fix/home-search-input
Open

fix: search input initialization and re-focus bug#2148
nnecec wants to merge 1 commit intonpmx-dev:mainfrom
nnecec:fix/home-search-input

Conversation

@nnecec
Copy link

@nnecec nnecec commented Mar 19, 2026

🔗 Linked issue

no issue

🧭 Context

no context

📚 Description

Fixed with Cursor, and reviewed by me.

3.19.1.mp4

Fixed two issues:

  1. After auto-focus on homepage, rapidly typed characters were being reset.
  2. Auto-focus was not triggered when redirect to homepage via logo click from other pages.

@vercel
Copy link

vercel bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 21, 2026 4:31am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 21, 2026 4:31am
npmx-lunaria Ignored Ignored Mar 21, 2026 4:31am

Request Review

@codecov
Copy link

codecov bot commented Mar 19, 2026

Codecov Report

❌ Patch coverage is 36.84211% with 24 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/useGlobalSearch.ts 36.84% 16 Missing and 8 partials ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

This PR updates app/composables/useGlobalSearch.ts to preserve client-typed input across hydration and route changes. It adds a client-only helper to read the focused search input value, uses that value to initialise searchQuery on the client, adjusts the URL→state watcher to observe [route.name, route.query.q] and only apply changes when route.name === 'search' (skipping updates if a search input is focused), normalises assignment to avoid redundant sets, clears and focuses the homepage search on return to index after navigation, and adds an onMounted recovery that restores searchQuery from a focused input. Lines changed: +71/−5.

Possibly related PRs

Suggested labels

front

Suggested reviewers

  • danielroe
  • ghostdevv
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset, identifying two specific bugs fixed in the search input initialization and re-focus behaviour.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can approve the review once all CodeRabbit's comments are resolved.

Enable the reviews.request_changes_workflow setting to automatically approve the review once all CodeRabbit's comments are resolved.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/composables/useGlobalSearch.ts (1)

60-67: Consider extracting focused input detection into a reusable helper.

The logic for detecting whether a search input is focused (lines 60-67) duplicates the pattern from getFocusedSearchInputValue (lines 22-24). Extracting a small isSearchInputFocused() helper would improve maintainability.

♻️ Proposed refactor
+const isSearchInputFocused = (): boolean => {
+  if (!import.meta.client) return false
+  const active = document.activeElement
+  if (!(active instanceof HTMLInputElement)) return false
+  return active.type === 'search' || active.name === 'q'
+}
+
 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
+  if (!isSearchInputFocused()) return ''
+  return (document.activeElement as HTMLInputElement).value
 }

Then in the watcher:

-     if (import.meta.client) {
-       const active = document.activeElement
-       if (
-         active instanceof HTMLInputElement &&
-         (active.type === 'search' || active.name === 'q')
-       ) {
-         return
-       }
-     }
+     if (isSearchInputFocused()) return

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ecd4a617-b5c7-4a12-93b5-29da7a115b93

📥 Commits

Reviewing files that changed from the base of the PR and between 5d8fcf5 and 399f570.

📒 Files selected for processing (1)
  • app/composables/useGlobalSearch.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6ac65a2-c31a-42f4-80dc-a8f836ca6980

📥 Commits

Reviewing files that changed from the base of the PR and between 399f570 and 4636625.

📒 Files selected for processing (1)
  • app/composables/useGlobalSearch.ts

Comment on lines +55 to +73
() => [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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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, searchQuery never updates, committedSearchQuery stays stale too, and there is no later resynchronisation on blur.

💡 Safer guard
-      // 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)
+      // Only skip when the focused input already reflects this URL value.
+      if (import.meta.client) {
+        const activeValue = getFocusedSearchInputValue()
+        if (activeValue && activeValue === value) {
+          return
+        }
+      }
       if (searchQuery.value !== value) {
         searchQuery.value = value
       }

Comment on lines +137 to +167
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
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard the mounted recovery after an intentional home reset.

These blocks can fight each other on the /search/ logo-click path. The index watcher clears the shared state, but the next mount can still read the previously focused header input via getFocusedSearchInputValue(); writing that back through searchQueryValue restores the old query and, with instant search enabled, can immediately navigate back to /search. A one-shot shared flag is needed so the mounted recovery is skipped for the same navigation that intentionally resets the home field.

@serhalp serhalp added the needs review This PR is waiting for a review from a maintainer label Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review This PR is waiting for a review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants