Skip to content

fix(search): focus cmdk input on first modal open#1021

Open
whizzkid1452 wants to merge 2 commits into
TanStack:mainfrom
whizzkid1452:bug/search-modal-first-focus
Open

fix(search): focus cmdk input on first modal open#1021
whizzkid1452 wants to merge 2 commits into
TanStack:mainfrom
whizzkid1452:bug/search-modal-first-focus

Conversation

@whizzkid1452

@whizzkid1452 whizzkid1452 commented Jul 1, 2026

Copy link
Copy Markdown

Summary

  • Fix search modal focus targeting: the modal queried input[type="search"], but the cmdk search field renders as a text input with the cmdk-input attribute.
  • Prevent Radix dialog auto-focus from stealing focus from the search input via onOpenAutoFocus.
  • Preload the lazy-loaded search modal chunk during idle time so the first Ctrl+K open is ready sooner.
  • Add a shared search input focus helper and regression tests.

Test plan

  • pnpm test:unit (includes new search-focus tests)
  • Open tanstack.com, press Ctrl+K, and type immediately without clicking the input
  • Confirm Algolia results appear on the first query
  • Confirm the AI dock search input still receives focus when opened

Summary by CodeRabbit

  • New Features
    • Search modal behavior now preloads more aggressively after page load to make opening feel faster.
    • Search UI now focuses the search input more consistently when the modal or dock appears.
  • Bug Fixes
    • Improved focus handling to prevent unintended auto-focus and ensure the correct search field is targeted.
  • Tests
    • Added coverage for locating and focusing the search input across multiple selector fallbacks, including null/no-match cases.

The search modal looked for input[type=search], but the cmdk search field renders as a text input with cmdk-input. Focus now targets the correct input on open, preloads the modal chunk during idle time, and adds regression tests for the selector helper.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ee08d54-5c1e-4577-9e0b-caf9d3dcef3f

📥 Commits

Reviewing files that changed from the base of the PR and between 6645681 and bfab1a9.

📒 Files selected for processing (2)
  • src/components/SearchModal.tsx
  • tests/search-focus.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/search-focus.test.ts
  • src/components/SearchModal.tsx

📝 Walkthrough

Walkthrough

This PR adds a shared search-input focus helper, rewires SearchModal and AiDock to use it, prevents Radix Dialog auto-focus, preloads the lazy search modal module on mount, and adds tests covering selector priority and focus behavior.

Changes

Search Focus Refactor and Preload

Layer / File(s) Summary
Search input focus utility
src/utils/searchFocus.ts
New SearchInputContainer type and getSearchInputFromContainer/focusSearchInputInContainer helpers locate and focus a search input using multiple selector fallbacks.
SearchModal and AiDock focus wiring
src/components/SearchModal.tsx
New scheduleSearchInputFocus helper wraps nested requestAnimationFrame calls with focusSearchInputInContainer; replaces prior inline focusing logic in both SearchModal and AiDock, and prevents Radix Dialog auto-focus.
Lazy search modal preload
src/contexts/SearchContext.tsx
LazySearchModal's dynamic import is refactored via a searchModalModule helper; a new mount-time effect preloads the module using requestIdleCallback or a setTimeout fallback with cleanup.
Search focus utility tests
tests/search-focus.test.ts
New tests verify selector precedence, null handling, and correct focusing behavior using mock inputs and containers.

Estimated code review effort: 2 (Simple) | ~12 minutes

Possibly related PRs

  • TanStack/tanstack.com#978: Both PRs touch src/components/SearchModal.tsx and AiDock search focus behavior, with this change refactoring the shared focus path.

Sequence Diagram(s)

sequenceDiagram
  participant RadixDialog as Radix Dialog
  participant SearchModal
  participant scheduleSearchInputFocus
  participant focusSearchInputInContainer
  RadixDialog->>SearchModal: onOpenAutoFocus
  SearchModal->>SearchModal: preventDefault()
  SearchModal->>scheduleSearchInputFocus: contentRef.current
  scheduleSearchInputFocus->>scheduleSearchInputFocus: nested requestAnimationFrame
  scheduleSearchInputFocus->>focusSearchInputInContainer: container
  focusSearchInputInContainer->>focusSearchInputInContainer: focus({ preventScroll: true })
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing initial focus of the cmdk search input when the modal opens.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/search-focus.test.ts (2)

1-91: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing edge-case coverage: null container and no-match fallthrough.

There's no test for getSearchInputFromContainer(null) (the early-return branch in getSearchInputFromContainer) or for the case where none of the four selectors match. Both are cheap to add given the existing createMockContainer helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/search-focus.test.ts` around lines 1 - 91, Add edge-case tests in
search-focus.test.ts for getSearchInputFromContainer’s early return when passed
null and for the no-match fallthrough when createMockContainer exposes none of
the selectors. Reuse the existing createMockContainer and createMockInput
helpers, and assert the null case returns null and the no-match container also
returns null. Keep the new coverage alongside the current
getSearchInputFromContainer and focusSearchInputInContainer assertions so the
selector behavior is fully exercised.

47-72: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Precedence tests don't isolate each selector; last fallback (type="search") is never independently verified.

In cmdkContainer (Lines 47-52), both [cmdk-input] and input[aria-label="Search TanStack"] map to the same cmdkInput object, so the test can't tell whether the first or second selector actually matched. Similarly, in searchTypeContainer (Lines 61-66), both input[aria-label="Search"] and input[type="search"] map to the same searchTypeInput, so the type="search" fallback path is never exercised on its own.

♻️ Suggested refactor to isolate each selector
-const cmdkContainer = createMockContainer({
-  '[cmdk-input]': cmdkInput,
-  'input[aria-label="Search TanStack"]': cmdkInput,
-  'input[aria-label="Search"]': null,
-  'input[type="search"]': createMockInput({ type: 'search' }),
-})
+const cmdkContainer = createMockContainer({
+  '[cmdk-input]': cmdkInput,
+  'input[aria-label="Search TanStack"]': null,
+  'input[aria-label="Search"]': null,
+  'input[type="search"]': null,
+})
...
-const searchTypeContainer = createMockContainer({
-  '[cmdk-input]': null,
-  'input[aria-label="Search TanStack"]': null,
-  'input[aria-label="Search"]': searchTypeInput,
-  'input[type="search"]': searchTypeInput,
-})
+const searchTypeContainer = createMockContainer({
+  '[cmdk-input]': null,
+  'input[aria-label="Search TanStack"]': null,
+  'input[aria-label="Search"]': null,
+  'input[type="search"]': searchTypeInput,
+})

Consider adding a dedicated case for input[aria-label="Search TanStack"] and input[aria-label="Search"] in isolation to fully cover the four-level selector chain.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/search-focus.test.ts` around lines 47 - 72, The selector precedence
tests in getSearchInputFromContainer are not isolating each fallback, so
matching for [cmdk-input], input[aria-label="Search TanStack"],
input[aria-label="Search"], and input[type="search"] is ambiguous. Update the
tests to use distinct mock inputs per selector and add dedicated cases that
verify each step in the chain independently, especially the final type="search"
fallback. Keep the assertions tied to getSearchInputFromContainer so each
selector path is uniquely exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/SearchModal.tsx`:
- Around line 3358-3365: The scheduled focus logic in scheduleSearchInputFocus
can still run after the modal closes because it creates nested
requestAnimationFrame callbacks without any cancellation path. Update
scheduleSearchInputFocus to return a cleanup/cancel handle for the pending rAF
chain, then use that return value in both consuming effects that call it so
their cleanup cancels the scheduled focus instead of returning undefined. Make
sure the fix covers the SearchModal and AiDock callers that currently discard
cleanup, so stale focus cannot be applied to contentRef.current after close
transitions.

---

Nitpick comments:
In `@tests/search-focus.test.ts`:
- Around line 1-91: Add edge-case tests in search-focus.test.ts for
getSearchInputFromContainer’s early return when passed null and for the no-match
fallthrough when createMockContainer exposes none of the selectors. Reuse the
existing createMockContainer and createMockInput helpers, and assert the null
case returns null and the no-match container also returns null. Keep the new
coverage alongside the current getSearchInputFromContainer and
focusSearchInputInContainer assertions so the selector behavior is fully
exercised.
- Around line 47-72: The selector precedence tests in
getSearchInputFromContainer are not isolating each fallback, so matching for
[cmdk-input], input[aria-label="Search TanStack"], input[aria-label="Search"],
and input[type="search"] is ambiguous. Update the tests to use distinct mock
inputs per selector and add dedicated cases that verify each step in the chain
independently, especially the final type="search" fallback. Keep the assertions
tied to getSearchInputFromContainer so each selector path is uniquely exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e51ca2b-f0c6-4f10-b973-1c2dfbbb1730

📥 Commits

Reviewing files that changed from the base of the PR and between 3814388 and 6645681.

📒 Files selected for processing (4)
  • src/components/SearchModal.tsx
  • src/contexts/SearchContext.tsx
  • src/utils/searchFocus.ts
  • tests/search-focus.test.ts

Comment thread src/components/SearchModal.tsx Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant