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

  • 新建 Features
    • 搜索 modal behavior now preloads more aggressively after page load to make opening feel faster.
    • 搜索 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

📥 提交

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

📒 Files selected for processing (2)
  • src/components/搜索Modal.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/搜索Modal.tsx

📝 Walkthrough

Walkthrough

This PR adds a shared search-input focus helper, rewires 搜索Modal 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

搜索 Focus Refactor and Preload

Layer / File(s) Summary
搜索 input focus utility
src/utils/searchFocus.ts
新建 搜索InputContainer type and get搜索InputFromContainer/focus搜索InputInContainer helpers locate and focus a search input using multiple selector fallbacks.
搜索Modal and AiDock focus wiring
src/components/搜索Modal.tsx
新建 schedule搜索InputFocus helper wraps nested requestAnimationFrame calls with focus搜索InputInContainer; replaces prior inline focusing logic in both 搜索Modal and AiDock, and prevents Radix Dialog auto-focus.
Lazy search modal preload
src/contexts/搜索Context.tsx
Lazy搜索Modal'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.
搜索 focus utility tests
tests/search-focus.test.ts
新建 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/搜索Modal.tsx and AiDock search focus behavior, with this change refactoring the shared focus path.

Sequence Diagram(s)

sequenceDiagram
  participant RadixDialog as Radix Dialog
  participant 搜索Modal
  participant schedule搜索InputFocus
  participant focus搜索InputInContainer
  RadixDialog->>搜索Modal: onOpenAutoFocus
  搜索Modal->>搜索Modal: preventDefault()
  搜索Modal->>schedule搜索InputFocus: contentRef.current
  schedule搜索InputFocus->>schedule搜索InputFocus: nested requestAnimationFrame
  schedule搜索InputFocus->>focus搜索InputInContainer: container
  focus搜索InputInContainer->>focus搜索InputInContainer: 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 问题 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 get搜索InputFromContainer(null) (the early-return branch in get搜索InputFromContainer) 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 get搜索InputFromContainer’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
get搜索InputFromContainer and focus搜索InputInContainer 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="搜索 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="搜索"] 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="搜索 TanStack"]': cmdkInput,
-  'input[aria-label="搜索"]': null,
-  'input[type="search"]': createMockInput({ type: 'search' }),
-})
+const cmdkContainer = createMockContainer({
+  '[cmdk-input]': cmdkInput,
+  'input[aria-label="搜索 TanStack"]': null,
+  'input[aria-label="搜索"]': null,
+  'input[type="search"]': null,
+})
...
-const searchTypeContainer = createMockContainer({
-  '[cmdk-input]': null,
-  'input[aria-label="搜索 TanStack"]': null,
-  'input[aria-label="搜索"]': searchTypeInput,
-  'input[type="search"]': searchTypeInput,
-})
+const searchTypeContainer = createMockContainer({
+  '[cmdk-input]': null,
+  'input[aria-label="搜索 TanStack"]': null,
+  'input[aria-label="搜索"]': null,
+  'input[type="search"]': searchTypeInput,
+})

Consider adding a dedicated case for input[aria-label="搜索 TanStack"] and input[aria-label="搜索"] 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 get搜索InputFromContainer are not isolating each fallback, so
matching for [cmdk-input], input[aria-label="搜索 TanStack"],
input[aria-label="搜索"], 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 get搜索InputFromContainer 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/搜索Modal.tsx`:
- Around line 3358-3365: The scheduled focus logic in schedule搜索InputFocus
can still run after the modal closes because it creates nested
requestAnimationFrame callbacks without any cancellation path. Update
schedule搜索InputFocus 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 搜索Modal 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
get搜索InputFromContainer’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 get搜索InputFromContainer and
focus搜索InputInContainer assertions so the selector behavior is fully
exercised.
- Around line 47-72: The selector precedence tests in
get搜索InputFromContainer are not isolating each fallback, so matching for
[cmdk-input], input[aria-label="搜索 TanStack"], input[aria-label="搜索"],
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 get搜索InputFromContainer 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

📥 提交

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

📒 Files selected for processing (4)
  • src/components/搜索Modal.tsx
  • src/contexts/搜索Context.tsx
  • src/utils/searchFocus.ts
  • tests/search-focus.test.ts

Comment thread src/components/搜索Modal.tsx Outdated
注册 for free to join this conversation on GitHub. Already have an account? 登录 to comment

标签

None yet

项目

None yet

Development

Successfully merging this pull request may close these issues.

1 participant