Skip to content

Add virtualization support for Combobox and ButtonSelect#6113

Open
simeonlee wants to merge 6 commits intomainfrom
simeonlee/combobox-virtualization-core
Open

Add virtualization support for Combobox and ButtonSelect#6113
simeonlee wants to merge 6 commits intomainfrom
simeonlee/combobox-virtualization-core

Conversation

@simeonlee
Copy link
Member

@simeonlee simeonlee commented Feb 3, 2026

Summary

  • Adds @tanstack/react-virtual for efficient rendering of large lists (100+ items)
  • Creates shared useVirtualizedNavigation hook for keyboard navigation (DRY)
  • Creates VirtualizedCommandItems component for virtualized rendering
  • Adds virtualizeThreshold prop (default: 100) to both Combobox and ButtonSelect
  • Implements keyboard navigation: Arrow keys, Home/End, PageUp/PageDown
  • Includes comprehensive Storybook stories for testing virtualization
  • Adds e2e tests for keyboard navigation

Screenshots

Virtualized Dropdown (500 items)

Virtualized Dropdown

Virtualized with Creation

Virtualized with Creation

Keyboard Navigation

Keyboard Navigation

Changes

File Description
use-virtualized-navigation.ts New shared hook for keyboard navigation
virtualized-command-items.tsx New component for virtualized rendering
ComboboxMenuItems.tsx Add support for virtualization props
Combobox.tsx Use hook, add virtualizeThreshold prop
ButtonSelect.tsx Use hook, add virtualizeThreshold prop
Combobox.stories.tsx Storybook stories (virtualized, stress tests, keyboard nav)
ButtonSelect.stories.tsx Additional virtualization stories
ui.combobox-keyboard.spec.ts E2E tests for keyboard navigation

Test plan

  • Verify non-virtualized mode still works (< 100 items)
  • Test keyboard navigation in virtualized mode
  • Verify filtering works correctly with virtualization
  • Test creation mode with virtualization enabled
  • Run Storybook and verify all stories render correctly
  • E2E tests for keyboard navigation

Supersedes #5812 with DRY refactored code. Includes stories from #6116.


Note

Medium Risk
Touches shared dropdown keyboard handling and selection/highlighting behavior, so regressions could affect accessibility and basic picking flows even though the change is UI-only and gated behind a threshold.

Overview
Adds virtualization for large dropdown lists in Combobox and ButtonSelect via a new virtualizeThreshold prop (default 100, 0=always, Infinity=never) and the @tanstack/react-virtual dependency.

Introduces VirtualizedCommandItems for virtualized rendering and a shared useVirtualizedNavigation hook to provide consistent keyboard navigation (arrows, Home/End, PageUp/PageDown, Enter, Escape) with explicit highlighted state wired through ComboboxMenuItems.

Expands Storybook coverage with virtualization/stress/keyboard demos and adds a Playwright spec validating combobox keyboard navigation and Escape-to-close behavior.

Written by Cursor Bugbot for commit 024bb07. This will update automatically on new commits. Configure here.

- Add @tanstack/react-virtual for efficient rendering of large lists
- Create shared useVirtualizedNavigation hook for keyboard navigation
- Create VirtualizedCommandItems component for virtualized rendering
- Add virtualizeThreshold prop (default: 100) to both components
- Support ArrowUp/Down, Home/End, PageUp/PageDown keyboard navigation
@simeonlee simeonlee self-assigned this Feb 3, 2026
- Add Combobox.stories.tsx with default, virtualized, and edge case stories
- Add virtualization stories to ButtonSelect.stories.tsx
- Stories cover: basic usage, virtualized mode, stress testing, keyboard nav
- Merges stories from simeonlee/combobox-virtualization-stories
- Resolves merge conflict in ComboboxMenuItems.tsx
- Combines core virtualization with Storybook stories
@simeonlee
Copy link
Member Author

@codex review

@simeonlee
Copy link
Member Author

@cursor review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0307647c59

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@simeonlee simeonlee marked this pull request as ready for review February 4, 2026 19:44
- Add hasCreateOption and onSelectCreate props to useVirtualizedNavigation hook
- Index -1 now represents the create option when hasCreateOption is true
- Update Combobox and ButtonSelect to pass create callbacks
- Update ComboboxMenuItems to highlight create option when index is -1
- Fixes bug where Enter key couldn't select create option in virtualized mode
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0307647c59

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +143 to +147
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (shouldVirtualize) {
virtualizedHandleKeyDown(e);
// Still need base handler for opening dropdown on arrow keys

Choose a reason for hiding this comment

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

P2 Badge Avoid selecting item on Enter when menu is closed

When shouldVirtualize is true, the input onKeyDown always forwards events to virtualizedHandleKeyDown even if the popover is closed. That handler selects on Enter, so focusing the combobox and pressing Enter can immediately select the first filtered item without ever opening the menu. In the non-virtualized path, Enter only opens the menu via baseHandleKeyDown, so this is a behavior regression for large lists (e.g., pressing Enter to submit a form now changes the selection). Consider gating the virtualized handler on open (or special-casing Enter when closed) so Enter behaves consistently.

Useful? React with 👍 / 👎.

import clsx from "clsx";

/** Default threshold for enabling virtualization */
const DEFAULT_VIRTUALIZE_THRESHOLD = 100;
Copy link

Choose a reason for hiding this comment

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

Duplicate constant should be shared

Low Severity

DEFAULT_VIRTUALIZE_THRESHOLD = 100 is defined identically in both Combobox.tsx and ButtonSelect.tsx. This constant should be exported from a shared location (e.g., use-virtualized-navigation.ts) to avoid duplication and ensure consistency.

Fix in Cursor Fix in Web

When there are no items (e.g., all filtered out), End and PageDown
could set highlightedIndex to an invalid value. Now they correctly
return minIndex (-1 for create option, 0 otherwise) when itemCount is 0.
simeonlee added a commit that referenced this pull request Feb 4, 2026
import { useVirtualizedNavigation } from "~/components/ui/use-virtualized-navigation";

/** Default threshold for enabling virtualization */
const DEFAULT_VIRTUALIZE_THRESHOLD = 100;
Copy link

Choose a reason for hiding this comment

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

Duplicate constant defined in two files

Low Severity

DEFAULT_VIRTUALIZE_THRESHOLD = 100 is defined identically in both Combobox.tsx and ButtonSelect.tsx. This constant should be defined once in a shared location (e.g., use-virtualized-navigation.ts or a separate constants file) and imported by both components.

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

onSelectCreate: () => {
handleSelectItem(searchValue.trim(), true);
},
});
Copy link

Choose a reason for hiding this comment

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

ButtonSelect missing onClose callback for Escape key

Medium Severity

The useVirtualizedNavigation hook call in ButtonSelect is missing the onClose callback, unlike Combobox which passes onClose: closeDropdown. The hook explicitly handles Escape key by calling onClose(), but since it's not provided, pressing Escape in virtualized mode won't close the dropdown through the custom handler. The fix would be to add onClose: () => setOpen(false) to match the intended keyboard navigation behavior.

Additional Locations (1)

Fix in Cursor Fix in Web

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