Skip to content

fix(autocomplete): resolve Firefox focus reset issue#5842

Closed
nikhil-304 wants to merge 6 commits into
heroui-inc:canaryfrom
nikhil-304:fix/autocomplete-firefox-focus-5840
Closed

fix(autocomplete): resolve Firefox focus reset issue#5842
nikhil-304 wants to merge 6 commits into
heroui-inc:canaryfrom
nikhil-304:fix/autocomplete-firefox-focus-5840

Conversation

@nikhil-304
Copy link
Copy Markdown

@nikhil-304 nikhil-304 commented Oct 23, 2025

Closes #5840

📝 Description

This PR fixes the Firefox Autocomplete input reset issue where typing after navigating into the input field using the Tab key caused the text to reset or get overwritten.
The problem occurred only in Firefox and only when focusing via keyboard - clicking into the field worked fine.

The fix ensures that the Autocomplete input now correctly handles focus events in Firefox, maintaining the input state regardless of how the field is focused.

⛳️ Current behavior (updates)

  • In Firefox, tabbing into the Autocomplete input causes the value to reset or overwrite after typing 1–2 characters.
  • The issue is reproducible both on the HeroUI website and in Storybook environments.
  • Works normally on Chrome and Edge.

📹 Before videos:
https://github.com/user-attachments/assets/1137923c-6b03-46ac-a93d-07c8a7295a94
https://github.com/user-attachments/assets/76cf2edf-8add-4e22-b8c3-b0d1f2a2100e

🚀 New behavior

  • Autocomplete input now behaves correctly in Firefox when focused using Tab.
  • Typing continues normally without resetting or losing characters.
  • The fix is verified to work consistently across all browsers (Firefox, Chrome, Edge).

📹 After video:
https://github.com/user-attachments/assets/54b31896-c4d1-4273-826a-2fa47b54bb0e

💣 Is this a breaking change (Yes/No):

No

📝 Additional Information

  • Tested on Firefox (Windows).
  • No API or behavior changes - only internal focus handling logic updated.

Summary by CodeRabbit

  • Bug Fixes

    • Refines input focus/selection behavior: Tab-committed selections now highlight text; mouse commits place the cursor at the end; cross-browser focus issues addressed.
  • New Features

    • Adds virtualization for large dropdowns (enabled for lists over ~50 items) to improve performance.
    • Introduces a new public prop, onSelectionChange, for reacting to selection events.
  • Tests

    • Adds tests validating mouse vs keyboard selection semantics.

@vercel
Copy link
Copy Markdown

vercel Bot commented Oct 23, 2025

@nikhil-304 is attempting to deploy a commit to the HeroUI Inc Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Oct 23, 2025

🦋 Changeset detected

Latest commit: 3a18c0c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@heroui/autocomplete Patch
@heroui/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 23, 2025

Walkthrough

Introduces focus and selection tracking to the Autocomplete hook: tracks Tab vs mouse interactions, conditions caret placement vs selecting text on focus, adds onSelectionChange prop, records Tab key for later focus behavior, and enables listbox virtualization when collection size > 50. Adds tests and a changeset.

Changes

Cohort / File(s) Summary
Autocomplete hook
packages/components/autocomplete/src/use-autocomplete.ts
Added internal state/refs (shouldSkipSelect, isMouseFocus, lastKeyRef, prevSelectedKeyRef), wired onSelectionChange prop, added onMouseDown/onKeyDown tracking, enhanced inputProps.onFocus and key handlers to place caret or select all text depending on last interaction (Tab vs mouse), reposition caret after mouse selection, and compute shouldVirtualize as isVirtualized ?? state.collection.size > 50 with virtualization config (maxListboxHeight, itemHeight). Extensive inline comments added.
Tests
packages/components/autocomplete/__tests__/autocomplete.test.tsx
Added tests validating mouse-selection keeps cursor at end (no selection) and Tab-key selection selects the full input text.
Release metadata
.changeset/fix-text-selection.md
New changeset declaring a patch for @heroui/autocomplete describing fixes: Tab selects text on commit and mouse click places cursor at end.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Input as AutocompleteInput
  participant Hook as useAutocomplete
  participant List as ListBox

  rect rgba(200,230,255,0.25)
  User->>Input: Press Tab to focus
  Input->>Hook: onKeyDown (records Tab -> lastKeyRef)
  Input->>Hook: onFocus
  Hook->>Hook: detect lastKeyRef == 'Tab'
  Hook->>Input: selectAll() (shouldSkipSelect / select-on-focus path)
  end

  rect rgba(230,255,210,0.25)
  User->>Input: Click to focus (mouse)
  Input->>Hook: onMouseDown (set isMouseFocus)
  Input->>Hook: onFocus
  Hook->>Hook: detect mouse focus -> place caret at end (no selection)
  end

  alt large collection (size > 50) or isVirtualized true
    Hook->>List: enable virtualization (maxListboxHeight, itemHeight)
  else small collection
    Hook->>List: normal rendering (no virtualization)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Inspect focus/caret logic in inputProps.onFocus and onSelectionChange for cross-browser edge cases (Firefox).
  • Verify lifecycle and resetting of lastKeyRef, shouldSkipSelect, and isMouseFocus to avoid race conditions.
  • Confirm virtualization gating and that supplied maxListboxHeight/itemHeight integrate with list rendering.
  • Review new tests for determinism and potential timing/flakiness.

Possibly related PRs

Suggested reviewers

  • jrgarciadev
  • wingkwong

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title mentions 'Firefox focus reset issue' but the actual fix addresses Tab-key text selection behavior and mouse focus handling, which are broader than just a Firefox focus reset. Consider revising the title to better reflect the core changes, such as 'fix(autocomplete): handle Tab selection and mouse focus interactions' or similar to more accurately represent the implementation.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description comprehensively covers all required sections: issue reference, clear problem description, current vs new behavior with videos, breaking change statement, and additional testing information.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #5840: fixes Firefox Tab focus handling, preserves input state during keyboard navigation, adds test cases validating both keyboard and mouse selection paths, and includes a changeset for the patch release.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the Firefox autocomplete focus issue: use-autocomplete.ts adds focus/selection logic, tests validate the fix, and the changeset documents the behavioral changes. No unrelated modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Oct 23, 2025

Open in StackBlitz

@heroui/accordion

npm i https://pkg.pr.new/@heroui/accordion@5842

@heroui/alert

npm i https://pkg.pr.new/@heroui/alert@5842

@heroui/autocomplete

npm i https://pkg.pr.new/@heroui/autocomplete@5842

@heroui/avatar

npm i https://pkg.pr.new/@heroui/avatar@5842

@heroui/badge

npm i https://pkg.pr.new/@heroui/badge@5842

@heroui/breadcrumbs

npm i https://pkg.pr.new/@heroui/breadcrumbs@5842

@heroui/button

npm i https://pkg.pr.new/@heroui/button@5842

@heroui/calendar

npm i https://pkg.pr.new/@heroui/calendar@5842

@heroui/card

npm i https://pkg.pr.new/@heroui/card@5842

@heroui/checkbox

npm i https://pkg.pr.new/@heroui/checkbox@5842

@heroui/chip

npm i https://pkg.pr.new/@heroui/chip@5842

@heroui/code

npm i https://pkg.pr.new/@heroui/code@5842

@heroui/date-input

npm i https://pkg.pr.new/@heroui/date-input@5842

@heroui/date-picker

npm i https://pkg.pr.new/@heroui/date-picker@5842

@heroui/divider

npm i https://pkg.pr.new/@heroui/divider@5842

@heroui/drawer

npm i https://pkg.pr.new/@heroui/drawer@5842

@heroui/dropdown

npm i https://pkg.pr.new/@heroui/dropdown@5842

@heroui/form

npm i https://pkg.pr.new/@heroui/form@5842

@heroui/image

npm i https://pkg.pr.new/@heroui/image@5842

@heroui/input

npm i https://pkg.pr.new/@heroui/input@5842

@heroui/input-otp

npm i https://pkg.pr.new/@heroui/input-otp@5842

@heroui/kbd

npm i https://pkg.pr.new/@heroui/kbd@5842

@heroui/link

npm i https://pkg.pr.new/@heroui/link@5842

@heroui/listbox

npm i https://pkg.pr.new/@heroui/listbox@5842

@heroui/menu

npm i https://pkg.pr.new/@heroui/menu@5842

@heroui/modal

npm i https://pkg.pr.new/@heroui/modal@5842

@heroui/navbar

npm i https://pkg.pr.new/@heroui/navbar@5842

@heroui/number-input

npm i https://pkg.pr.new/@heroui/number-input@5842

@heroui/pagination

npm i https://pkg.pr.new/@heroui/pagination@5842

@heroui/popover

npm i https://pkg.pr.new/@heroui/popover@5842

@heroui/progress

npm i https://pkg.pr.new/@heroui/progress@5842

@heroui/radio

npm i https://pkg.pr.new/@heroui/radio@5842

@heroui/ripple

npm i https://pkg.pr.new/@heroui/ripple@5842

@heroui/scroll-shadow

npm i https://pkg.pr.new/@heroui/scroll-shadow@5842

@heroui/select

npm i https://pkg.pr.new/@heroui/select@5842

@heroui/skeleton

npm i https://pkg.pr.new/@heroui/skeleton@5842

@heroui/slider

npm i https://pkg.pr.new/@heroui/slider@5842

@heroui/snippet

npm i https://pkg.pr.new/@heroui/snippet@5842

@heroui/spacer

npm i https://pkg.pr.new/@heroui/spacer@5842

@heroui/spinner

npm i https://pkg.pr.new/@heroui/spinner@5842

@heroui/switch

npm i https://pkg.pr.new/@heroui/switch@5842

@heroui/table

npm i https://pkg.pr.new/@heroui/table@5842

@heroui/tabs

npm i https://pkg.pr.new/@heroui/tabs@5842

@heroui/toast

npm i https://pkg.pr.new/@heroui/toast@5842

@heroui/tooltip

npm i https://pkg.pr.new/@heroui/tooltip@5842

@heroui/user

npm i https://pkg.pr.new/@heroui/user@5842

@heroui/react

npm i https://pkg.pr.new/@heroui/react@5842

@heroui/system

npm i https://pkg.pr.new/@heroui/system@5842

@heroui/system-rsc

npm i https://pkg.pr.new/@heroui/system-rsc@5842

@heroui/theme

npm i https://pkg.pr.new/@heroui/theme@5842

@heroui/use-aria-accordion

npm i https://pkg.pr.new/@heroui/use-aria-accordion@5842

@heroui/use-aria-accordion-item

npm i https://pkg.pr.new/@heroui/use-aria-accordion-item@5842

@heroui/use-aria-button

npm i https://pkg.pr.new/@heroui/use-aria-button@5842

@heroui/use-aria-link

npm i https://pkg.pr.new/@heroui/use-aria-link@5842

@heroui/use-aria-modal-overlay

npm i https://pkg.pr.new/@heroui/use-aria-modal-overlay@5842

@heroui/use-aria-multiselect

npm i https://pkg.pr.new/@heroui/use-aria-multiselect@5842

@heroui/use-aria-overlay

npm i https://pkg.pr.new/@heroui/use-aria-overlay@5842

@heroui/use-callback-ref

npm i https://pkg.pr.new/@heroui/use-callback-ref@5842

@heroui/use-clipboard

npm i https://pkg.pr.new/@heroui/use-clipboard@5842

@heroui/use-data-scroll-overflow

npm i https://pkg.pr.new/@heroui/use-data-scroll-overflow@5842

@heroui/use-disclosure

npm i https://pkg.pr.new/@heroui/use-disclosure@5842

@heroui/use-draggable

npm i https://pkg.pr.new/@heroui/use-draggable@5842

@heroui/use-form-reset

npm i https://pkg.pr.new/@heroui/use-form-reset@5842

@heroui/use-image

npm i https://pkg.pr.new/@heroui/use-image@5842

@heroui/use-infinite-scroll

npm i https://pkg.pr.new/@heroui/use-infinite-scroll@5842

@heroui/use-intersection-observer

npm i https://pkg.pr.new/@heroui/use-intersection-observer@5842

@heroui/use-is-mobile

npm i https://pkg.pr.new/@heroui/use-is-mobile@5842

@heroui/use-is-mounted

npm i https://pkg.pr.new/@heroui/use-is-mounted@5842

@heroui/use-measure

npm i https://pkg.pr.new/@heroui/use-measure@5842

@heroui/use-pagination

npm i https://pkg.pr.new/@heroui/use-pagination@5842

@heroui/use-real-shape

npm i https://pkg.pr.new/@heroui/use-real-shape@5842

@heroui/use-ref-state

npm i https://pkg.pr.new/@heroui/use-ref-state@5842

@heroui/use-resize

npm i https://pkg.pr.new/@heroui/use-resize@5842

@heroui/use-safe-layout-effect

npm i https://pkg.pr.new/@heroui/use-safe-layout-effect@5842

@heroui/use-scroll-position

npm i https://pkg.pr.new/@heroui/use-scroll-position@5842

@heroui/use-ssr

npm i https://pkg.pr.new/@heroui/use-ssr@5842

@heroui/use-theme

npm i https://pkg.pr.new/@heroui/use-theme@5842

@heroui/use-update-effect

npm i https://pkg.pr.new/@heroui/use-update-effect@5842

@heroui/use-viewport-size

npm i https://pkg.pr.new/@heroui/use-viewport-size@5842

@heroui/aria-utils

npm i https://pkg.pr.new/@heroui/aria-utils@5842

@heroui/dom-animation

npm i https://pkg.pr.new/@heroui/dom-animation@5842

@heroui/framer-utils

npm i https://pkg.pr.new/@heroui/framer-utils@5842

@heroui/react-rsc-utils

npm i https://pkg.pr.new/@heroui/react-rsc-utils@5842

@heroui/react-utils

npm i https://pkg.pr.new/@heroui/react-utils@5842

@heroui/shared-icons

npm i https://pkg.pr.new/@heroui/shared-icons@5842

@heroui/shared-utils

npm i https://pkg.pr.new/@heroui/shared-utils@5842

@heroui/stories-utils

npm i https://pkg.pr.new/@heroui/stories-utils@5842

@heroui/test-utils

npm i https://pkg.pr.new/@heroui/test-utils@5842

commit: 3a18c0c

@nikhil-304
Copy link
Copy Markdown
Author

Hey @jrgarciadev 👋
Just following up on this PR - it fixes the Firefox autocomplete focus reset issue (#5840) and I’ve tested it with before/after videos (storybook + website).
Would love your review whenever you get a chance. Thanks again for maintaining HeroUI - really appreciate your time! 🙏

@vercel
Copy link
Copy Markdown

vercel Bot commented Oct 28, 2025

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

Project Deployment Preview Comments Updated (UTC)
heroui Ready Ready Preview Comment Nov 14, 2025 10:17am
heroui-sb Ready Ready Preview Comment Nov 14, 2025 10:17am

@wingkwong wingkwong changed the title fix(autocomplete): resolve Firefox focus reset issue (#5840) fix(autocomplete): resolve Firefox focus reset issue Oct 28, 2025
@wingkwong wingkwong self-assigned this Oct 28, 2025
Copy link
Copy Markdown
Member

@wingkwong wingkwong left a comment

Choose a reason for hiding this comment

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

  1. please add changeset
  2. please include a test case
  3. your change will alter the existing behaviour. If there is a value in autocomplete, when you tab it to focus, the expected behavior is the whole text got selected and users can type without producing the incorrect input after 1-2 characters (referring to the linked issue). You can also see this behaviour in other components like input.

Copy link
Copy Markdown
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: 0

🧹 Nitpick comments (3)
packages/components/autocomplete/src/use-autocomplete.ts (2)

149-152: Variable naming could be clearer.

The variable shouldSkipSelect is confusing because when it's false, text selection actually occurs, and when it's true, selection is skipped. Consider renaming to something more intuitive like shouldPlaceCursorAtEnd or inverting the logic with shouldSelectText to improve code readability.


513-537: Consider simplifying the onFocus logic.

The onFocus handler has multiple conditional branches that could be refactored for better readability. Consider extracting this logic into a separate helper function with clear documentation explaining each case:

  1. When to place cursor at end (mouse selection)
  2. When to select all text (Tab selection with matching value)
  3. Default fallback behavior

Apply this refactor if you want to improve readability:

+const handleTextSelection = (
+  target: HTMLInputElement,
+  shouldSkipSelect: boolean,
+  selectedItem: any,
+) => {
+  if (!target.value) return;
+
+  const length = target.value.length;
+
+  // Place cursor at end if mouse selection
+  if (shouldSkipSelect) {
+    target.setSelectionRange(length, length);
+    return;
+  }
+
+  // Select text if Tab selection with matching value
+  if (selectedItem && target.value === selectedItem.textValue) {
+    target.select();
+    return;
+  }
+
+  // Default: place cursor at end
+  target.setSelectionRange(length, length);
+};

 onFocus: chain(
   inputProps.onFocus,
   otherProps.onFocus,
   (e: React.FocusEvent<HTMLInputElement>) => {
-    if (shouldSkipSelect) {
-      if (e.target.value) {
-        const length = e.target.value.length;
-        e.target.setSelectionRange(length, length);
-      }
-    } else if (
-      e.target.value &&
-      state.selectedItem &&
-      e.target.value === state.selectedItem.textValue
-    ) {
-      e.target.select();
-    } else if (e.target.value) {
-      const length = e.target.value.length;
-      e.target.setSelectionRange(length, length);
-    }
+    handleTextSelection(e.target, shouldSkipSelect, state.selectedItem);
     setShouldSkipSelect(false);
   },
 ),
packages/components/autocomplete/__tests__/autocomplete.test.tsx (1)

1092-1142: Consider adding edge case tests.

While the current tests cover the primary scenarios, consider adding tests for edge cases:

  1. Clicking into the field after a previous selection (does it preserve cursor position?)
  2. Tab selection followed by blur and re-focus (is selection preserved/reset correctly?)
  3. Rapid switching between mouse and keyboard selection methods

These additional tests would provide more confidence in the robustness of the fix, especially for the Firefox-specific focus behavior.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c60b080 and 3917a39.

📒 Files selected for processing (3)
  • .changeset/fix-text-selection.md (1 hunks)
  • packages/components/autocomplete/__tests__/autocomplete.test.tsx (1 hunks)
  • packages/components/autocomplete/src/use-autocomplete.ts (5 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-27T21:48:35.308Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.308Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/components/autocomplete/src/use-autocomplete.ts
📚 Learning: 2025-10-25T17:11:59.338Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.338Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.

Applied to files:

  • packages/components/autocomplete/src/use-autocomplete.ts
📚 Learning: 2025-10-27T21:52:33.324Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:115-125
Timestamp: 2025-10-27T21:52:33.324Z
Learning: In packages/components/tabs/src/tabs.tsx, the useEffect dependency array at line 125 intentionally uses `domRef.current` rather than `domRef` because domRef.current can change between renders (when React sets it during the commit phase), whereas domRef itself has stable identity and won't change.

Applied to files:

  • packages/components/autocomplete/src/use-autocomplete.ts
📚 Learning: 2025-10-25T17:08:46.283Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:156-157
Timestamp: 2025-10-25T17:08:46.283Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes `variant` and `isVertical` to prevent potential side-effects, even though they might appear redundant based on static analysis.

Applied to files:

  • packages/components/autocomplete/src/use-autocomplete.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Continuous Release
  • GitHub Check: TypeScript
  • GitHub Check: Build
  • GitHub Check: ESLint
🔇 Additional comments (6)
packages/components/autocomplete/src/use-autocomplete.ts (3)

218-227: Text selection logic works correctly.

The logic correctly handles the two selection paths:

  • Mouse selection: sets shouldSkipSelect = true → cursor placed at end on next focus
  • Tab selection: sets shouldSkipSelect = false → text selected on next focus

This addresses the Firefox focus reset issue by ensuring proper text selection behavior based on interaction method.


431-434: Tab key tracking implementation is correct.

Tracking the Tab key press in onKeyDown allows the component to differentiate between keyboard and mouse-based selections. The lastKeyRef is appropriately reset in onSelectionChange after being consumed.


540-559: The virtualization logic predates the Firefox focus fix—it was not introduced by those changes.

The virtualization feature (isVirtualized prop with auto-enable threshold at 50 items) already existed before the Firefox focus fix commit (c60b080). The Firefox fix specifically added only the onFocus handler to manage text selection behavior during focus events. These are separate, unrelated features. The virtualization logic is a performance optimization for large lists and has no connection to the Firefox focus reset issue, which involves keyboard/selection behavior management.

Likely an incorrect or invalid review comment.

packages/components/autocomplete/__tests__/autocomplete.test.tsx (2)

1092-1117: Mouse selection test validates cursor positioning correctly.

The test confirms that selecting an item via mouse click places the cursor at the end of the value without selecting text. This is the expected behavior for mouse-based interactions.


1119-1142: Tab selection test validates text selection correctly.

The test confirms that committing a selection via Tab key fully selects the text (selectionStart = 0, selectionEnd = length). This addresses the Firefox focus reset issue by ensuring the text is in a selected state, allowing immediate typing to replace the value.

.changeset/fix-text-selection.md (1)

1-5: Changeset documentation is clear and appropriate.

The changeset correctly documents the fix as a patch release and clearly describes the two behavioral changes: text selection on Tab key and cursor positioning on mouse click. This aligns well with the changes implemented in the code.

@nikhil-304
Copy link
Copy Markdown
Author

Hi @wingkwong @jrgarciadev,
I’ve implemented all the requested updates - added the changeset, included test cases, and ensured the Tab focus behavior now correctly selects the text (verified with a demo video as well).
When you get a chance, could you please take another look?
Really appreciate your earlier guidance and review 🙌

firefox-autocomplete-focus-fix-demo.mp4

… focus improvements

- Fix Firefox focus reset issues with intelligent caret positioning
- Add mouse/keyboard interaction differentiation for better UX
- Ensure consistent text selection across all browsers
- Tab navigation now selects full text for replacement workflows
Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (1)
packages/components/autocomplete/src/use-autocomplete.ts (1)

301-307: Consider filtering mouse button types.

The onMouseDown handler currently triggers for all mouse buttons. While the focus event typically only fires for left-clicks, you might want to explicitly check for left-button clicks to be more precise:

-        onMouseDown: () => setIsMouseFocus(true),
+        onMouseDown: (e: React.MouseEvent) => {
+          if (e.button === 0) {
+            setIsMouseFocus(true);
+          }
+        },
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3917a39 and 3a18c0c.

📒 Files selected for processing (1)
  • packages/components/autocomplete/src/use-autocomplete.ts (7 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-27T21:48:35.308Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.308Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/components/autocomplete/src/use-autocomplete.ts
📚 Learning: 2025-10-25T17:11:59.338Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.338Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.

Applied to files:

  • packages/components/autocomplete/src/use-autocomplete.ts
📚 Learning: 2025-10-27T21:52:33.324Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:115-125
Timestamp: 2025-10-27T21:52:33.324Z
Learning: In packages/components/tabs/src/tabs.tsx, the useEffect dependency array at line 125 intentionally uses `domRef.current` rather than `domRef` because domRef.current can change between renders (when React sets it during the commit phase), whereas domRef itself has stable identity and won't change.

Applied to files:

  • packages/components/autocomplete/src/use-autocomplete.ts
📚 Learning: 2025-10-25T17:08:46.283Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:156-157
Timestamp: 2025-10-25T17:08:46.283Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes `variant` and `isVertical` to prevent potential side-effects, even though they might appear redundant based on static analysis.

Applied to files:

  • packages/components/autocomplete/src/use-autocomplete.ts
🧬 Code graph analysis (1)
packages/components/autocomplete/src/use-autocomplete.ts (1)
packages/hooks/use-safe-layout-effect/src/index.ts (1)
  • useSafeLayoutEffect (3-3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Continuous Release
  • GitHub Check: ESLint
  • GitHub Check: TypeScript
  • GitHub Check: Build
🔇 Additional comments (4)
packages/components/autocomplete/src/use-autocomplete.ts (4)

149-156: Well-structured state management for interaction tracking.

The combination of useState for values that affect rendering (shouldSkipSelect, isMouseFocus) and useRef for tracking values without re-renders (lastKeyRef) is appropriate. The inline documentation clearly explains each variable's purpose.


475-484: Tab key tracking implementation looks good.

The selective tracking of only the Tab key (not other keys) correctly implements the requirement to distinguish Tab navigation from other keyboard interactions. The inline comments clearly explain the purpose and lifecycle of lastKeyRef.


563-617: Comprehensive focus handler with clear interaction-based logic.

The enhanced onFocus implementation effectively distinguishes between mouse and keyboard interactions, applies the appropriate selection behavior, and properly resets state flags. The extensive inline documentation makes the complex conditional logic understandable and maintainable.

The condition at line 602 (e.target.value === state.selectedItem.textValue) is a good safeguard to only select text when it matches the selected item, avoiding unexpected selection of user-typed content.


221-238: No action required—behavior is intentional and test-verified.

The conditional Tab vs. mouse selection behavior you questioned is a deliberate design feature, not an inconsistency. The codebase includes explicit tests validating both cases:

  • Tab key selection (line 1119): Text is selected (selectionStart=0, selectionEnd=length)
  • Mouse selection (line 1092): Caret is placed at end (selectionStart=end, selectionEnd=end)

This allows Tab navigation to select all text for easy replacement, while mouse selections position the caret for continued editing. The implementation aligns with its documented intent.

Comment on lines +426 to +450
/**
* Post-selection caret positioning for dropdown interactions:
*
* PROBLEM: When selecting items from dropdown via mouse, the input remains focused
* but React Aria doesn't trigger onFocus again, so our focus handler doesn't run.
*
* SOLUTION: Watch for selection changes and position caret at end for mouse interactions.
* - Uses useSafeLayoutEffect to run after DOM updates but before paint
* - Only applies when selection changed and lastKeyRef is null (mouse interaction)
* - Ensures input is still focused before setting selection
* - Provides consistent behavior regardless of how dropdown items are selected
*/
const prevSelectedKeyRef = useRef(state.selectedKey);

useSafeLayoutEffect(() => {
if (state.selectedKey !== prevSelectedKeyRef.current && lastKeyRef.current === null) {
// Mouse selection from dropdown: ensure caret is at end
if (inputRef.current && document.activeElement === inputRef.current) {
const length = inputRef.current.value.length;

inputRef.current.setSelectionRange(length, length);
}
}
prevSelectedKeyRef.current = state.selectedKey;
}, [state.selectedKey]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading comment: effect applies to all selections, not just mouse.

The comment on line 442 states "Mouse selection from dropdown: ensure caret is at end" and line 434 claims "Only applies when selection changed and lastKeyRef is null (mouse interaction)". However, lastKeyRef.current is reset to null in onSelectionChange (line 237) before this effect runs, so the condition lastKeyRef.current === null will be true for all selections (mouse, keyboard, Tab-initiated), not just mouse selections.

If the intent is to position the caret at the end for all dropdown selections (and rely on shouldSkipSelect to control the next focus behavior), please update the comments to reflect this. Otherwise, if you truly want to distinguish mouse from Tab-initiated selections here, you should check shouldSkipSelect instead:

-  useSafeLayoutEffect(() => {
-    if (state.selectedKey !== prevSelectedKeyRef.current && lastKeyRef.current === null) {
-      // Mouse selection from dropdown: ensure caret is at end
+  useSafeLayoutEffect(() => {
+    if (state.selectedKey !== prevSelectedKeyRef.current && shouldSkipSelect) {
+      // Non-Tab selection from dropdown: ensure caret is at end
       if (inputRef.current && document.activeElement === inputRef.current) {

Alternatively, update the comments to clarify that this runs for all selections:

   /**
-   * Post-selection caret positioning for dropdown interactions:
+   * Post-selection caret positioning for all dropdown interactions:
    *
-   * PROBLEM: When selecting items from dropdown via mouse, the input remains focused
+   * PROBLEM: When selecting items from dropdown, the input remains focused
    * but React Aria doesn't trigger onFocus again, so our focus handler doesn't run.
    *
-   * SOLUTION: Watch for selection changes and position caret at end for mouse interactions.
+   * SOLUTION: Watch for selection changes and position caret at end.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Post-selection caret positioning for dropdown interactions:
*
* PROBLEM: When selecting items from dropdown via mouse, the input remains focused
* but React Aria doesn't trigger onFocus again, so our focus handler doesn't run.
*
* SOLUTION: Watch for selection changes and position caret at end for mouse interactions.
* - Uses useSafeLayoutEffect to run after DOM updates but before paint
* - Only applies when selection changed and lastKeyRef is null (mouse interaction)
* - Ensures input is still focused before setting selection
* - Provides consistent behavior regardless of how dropdown items are selected
*/
const prevSelectedKeyRef = useRef(state.selectedKey);
useSafeLayoutEffect(() => {
if (state.selectedKey !== prevSelectedKeyRef.current && lastKeyRef.current === null) {
// Mouse selection from dropdown: ensure caret is at end
if (inputRef.current && document.activeElement === inputRef.current) {
const length = inputRef.current.value.length;
inputRef.current.setSelectionRange(length, length);
}
}
prevSelectedKeyRef.current = state.selectedKey;
}, [state.selectedKey]);
/**
* Post-selection caret positioning for all dropdown interactions:
*
* PROBLEM: When selecting items from dropdown, the input remains focused
* but React Aria doesn't trigger onFocus again, so our focus handler doesn't run.
*
* SOLUTION: Watch for selection changes and position caret at end.
* - Uses useSafeLayoutEffect to run after DOM updates but before paint
* - Applies when selection changed and lastKeyRef is null
* - Ensures input is still focused before setting selection
* - Provides consistent behavior regardless of how dropdown items are selected
*/
const prevSelectedKeyRef = useRef(state.selectedKey);
useSafeLayoutEffect(() => {
if (state.selectedKey !== prevSelectedKeyRef.current && lastKeyRef.current === null) {
// Selection from dropdown: ensure caret is at end
if (inputRef.current && document.activeElement === inputRef.current) {
const length = inputRef.current.value.length;
inputRef.current.setSelectionRange(length, length);
}
}
prevSelectedKeyRef.current = state.selectedKey;
}, [state.selectedKey]);

@nikhil-304
Copy link
Copy Markdown
Author

Hi @wingkwong @jrgarciadev ,
I’ve pushed another round of improvements focused on refining the Firefox focus behavior and ensuring consistent text selection across browsers.

I also took some time to study how Firefox and Chromium-based browsers handle input selection differently, and adjusted the caret logic to behave consistently across all browsers.

I know things are busy with the HeroUI v3 beta launch (which looks amazing 🎉), but whenever you both get a moment, I’d really appreciate it if you could take another look 🙏

@nikhil-304
Copy link
Copy Markdown
Author

Hi @wingkwong,
Just a gentle follow-up - I’ve pushed the latest improvements (including cross-browser caret and selection behavior), and everything is green on CI.

I know things are super busy with the v3 beta release, but whenever you get a moment I’d really appreciate another review 🙏

Copy link
Copy Markdown
Member

@wingkwong wingkwong left a comment

Choose a reason for hiding this comment

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

This PR seems adding more complexity to the component while the issue should be resolved from the upstream as it is also reproducible in https://react-spectrum.adobe.com/react-aria/Autocomplete.html#value

@nikhil-304
Copy link
Copy Markdown
Author

Got it, thanks for the clarification.
I’ll check React Aria upstream since the issue originates there.

@wingkwong wingkwong closed this Nov 15, 2025
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.

[BUG] - Tabbing into autocomplete on firefox causes input to constantly reset

2 participants