Skip to content

feat: migrate Combobox to Base UI#590

Open
rohanchkrabrty wants to merge 2 commits intomainfrom
migrate-combobox
Open

feat: migrate Combobox to Base UI#590
rohanchkrabrty wants to merge 2 commits intomainfrom
migrate-combobox

Conversation

@rohanchkrabrty
Copy link
Contributor

@rohanchkrabrty rohanchkrabrty commented Feb 16, 2026

Description

[Provide a brief description of the changes in this PR]

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor (no functional changes, no bug fixes just code improvements)
  • Chore (changes to the build process or auxiliary tools and libraries such as documentation generation)
  • Style (changes that do not affect the meaning of the code (white-space, formatting, etc))
  • Test (adding missing tests or correcting existing tests)
  • Improvement (Improvements to existing code)
  • Other (please specify)

How Has This Been Tested?

[Describe the tests that you ran to verify your changes]

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (.mdx files)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works

Screenshots (if appropriate):

[Add screenshots here]

Related Issues

[Link any related issues here using #issue-number]

Summary by CodeRabbit

  • New Features

    • Added Combobox examples to the playground (single, multi, grouped) and new demo variants with icons and labeled inputs
    • Enabled filtering for the Email column in the datatable demo
    • Combobox selection UI improved (checkboxes/chips) and grouping/search behaviors refined
  • Documentation

    • Updated Combobox docs with reorganized content, examples, and usage guidance

@vercel
Copy link

vercel bot commented Feb 16, 2026

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

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Feb 16, 2026 6:16am

@rohanchkrabrty rohanchkrabrty self-assigned this Feb 16, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

Migrates the Combobox from Ariakit/Radix to Base UI primitives with generic single/multiple typing; refactors context and component APIs, updates tests and docs, adds combobox demos and playground exports, adds containerRef to InputField, and enables column filtering on the DataTable Email column.

Changes

Cohort / File(s) Summary
Combobox Core (root/content/input/item/misc)
packages/raystack/components/combobox/combobox-root.tsx, .../combobox-content.tsx, .../combobox-input.tsx, .../combobox-item.tsx, .../combobox-misc.tsx
Full migration from Ariakit/Radix to @base-ui/react primitives. Introduces generic ComboboxContextValue<Value>, generic Root props (Single/MultipleComboboxProps<Value>), new context shape (inputValue, hasItems, inputContainerRef, value, onValueChange), ref type changes, and updated displayName values.
Combobox Styling & Utilities
packages/raystack/components/combobox/combobox.module.css, packages/raystack/components/dropdown-menu/utils.ts
Adds .positioner, changes .content min-width to --anchor-width, simplifies highlighted selector; widens getMatch value param to any (watch runtime string ops).
Tests
packages/raystack/components/combobox/__tests__/combobox.test.tsx
Reworks interactions to use clickOption helper (pointer events), uses defaultOpen/defaultValue in setups, removes data-selected assertions and some multiple-mode tests.
Docs & Demos
apps/www/src/content/docs/components/combobox/demo.ts, .../index.mdx, apps/www/src/components/playground/combobox-examples.tsx
Updates demo placeholders, widths, and items; adds iconDemo and withLabelDemo; restructures Combobox docs and demo exports; adds a new ComboboxExamples playground component.
Playground Exports
apps/www/src/components/playground/index.ts
Adds multiple new demo exports (amount-examples, combobox-examples, skeleton-examples, indicator-examples, input-field-examples, label-examples, link-examples, list-examples, popover-examples, radio-examples, search-examples, select-examples) and reorders exports.
InputField & DataTable Demo
packages/raystack/components/input-field/input-field.tsx, apps/www/src/components/datatable-demo.tsx
Adds `containerRef: RefObject<HTMLDivElement

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ComboboxRoot
    participant ComboboxContext
    participant ComboboxInput
    participant ComboboxContent
    participant ComboboxItem

    User->>ComboboxRoot: Mount with items, value/defaultValue
    ComboboxRoot->>ComboboxContext: Initialize context (inputValue, value, hasItems, inputContainerRef)
    ComboboxRoot->>ComboboxInput: Render Input (containerRef attached)
    User->>ComboboxInput: Type/search
    ComboboxInput->>ComboboxRoot: onInputValueChange
    ComboboxRoot->>ComboboxContext: Update inputValue
    ComboboxContent->>ComboboxItem: Render filtered items (uses getMatch)
    User->>ComboboxItem: Click/select option
    ComboboxItem->>ComboboxRoot: onValueChange
    ComboboxRoot->>ComboboxContext: Update value/computedValue
    ComboboxRoot->>ComboboxInput: Reflect selection
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

Do not merge

Suggested reviewers

  • paanSinghCoder
  • rsbh
  • rohilsurana

Poem

🐰 I hopped through roots and content wide,
Base UI made the old bits glide,
Generics tucked in every nest,
Docs and tests got a little zest,
A rabbit cheers this tidy stride! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main objective of this PR—migrating the Combobox component from Ariakit to Base UI. It is concise, specific, and directly related to the substantial changes across multiple Combobox files.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch migrate-combobox

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/raystack/components/dropdown-menu/utils.ts (1)

3-14: ⚠️ Potential issue | 🟠 Major

Runtime error when value is not a string.

Widening value to any while calling value?.toLowerCase() on Line 12 will throw a TypeError for non-string values (e.g., numbers, objects). The function should guard against non-string types before calling string methods.

🐛 Proposed fix to handle non-string values
 export const getMatch = (
   value?: any,
   children?: ReactNode,
   search?: string
 ) => {
   if (!search?.length) return true;
   const childrenValue = getChildrenValue(children)?.toLowerCase();
+  const stringValue = typeof value === 'string' ? value.toLowerCase() : String(value ?? '').toLowerCase();

   return (
-    value?.toLowerCase().includes(search.toLowerCase()) ||
+    stringValue.includes(search.toLowerCase()) ||
     childrenValue?.includes(search.toLowerCase())
   );
 };
packages/raystack/components/combobox/__tests__/combobox.test.tsx (1)

349-360: ⚠️ Potential issue | 🔴 Critical

Wire onOpenChange prop to ComboboxPrimitive.Root.

The onOpenChange callback is destructured in ComboboxRoot but never passed to the Base UI primitive, causing it to never be invoked. Pass onOpenChange to ComboboxPrimitive.Root alongside the other event handlers (onValueChange, onInputValueChange).

🤖 Fix all issues with AI agents
In `@apps/www/src/content/docs/components/combobox/demo.ts`:
- Around line 54-68: The demo includes a duplicated option label "Lemon" in the
multiple-selection Combobox example; edit the demo in
apps/www/src/content/docs/components/combobox/demo.ts and remove the extra
Combobox.Item that renders "Lemon" so there is only one <Combobox.Item> with the
label "Lemon" in the <Combobox.Content> list (leave the other Combobox.Item
entries unchanged).

In `@packages/raystack/components/combobox/combobox-root.tsx`:
- Around line 85-86: The computedValue assignment uses the nullish coalescing
operator which treats explicit null like undefined; update the logic that sets
computedValue (currently using providedValue and internalValue in
combobox-root.tsx) to only fall back to internalValue when providedValue is
strictly undefined (e.g., check providedValue === undefined), so a controlled
value of null is preserved and not replaced by internalValue; locate the
computedValue variable near the top of the component and change the fallback
condition accordingly.
🧹 Nitpick comments (1)
packages/raystack/components/combobox/combobox-input.tsx (1)

29-36: Consider avoiding the redundant type assertion.

Line 34 casts value as string[] even though Line 30 already checks Array.isArray(value). TypeScript should infer this within the conditional block. If the assertion is still needed due to closure capture, consider extracting the value first.

♻️ Suggested refinement
          chips={
            multiple && Array.isArray(value)
-             ? value.map(val => ({
+             ? value.map((val: string) => ({
                  label: val,
                  onRemove: () =>
-                   onValueChange?.((value as string[])?.filter(v => v !== val))
+                   onValueChange?.(value.filter(v => v !== val))
                }))
              : undefined
          }

Comment on lines 54 to +68
<Combobox multiple>
<Combobox.Input placeholder="Select fruits..." />
<Combobox.Input placeholder="Select fruits" width={300} />
<Combobox.Content>
<Combobox.Item>Apple</Combobox.Item>
<Combobox.Item>Banana</Combobox.Item>
<Combobox.Item>Grape</Combobox.Item>
<Combobox.Item>Orange</Combobox.Item>
<Combobox.Item>Pineapple</Combobox.Item>
<Combobox.Item>Mango</Combobox.Item>
<Combobox.Item>Pineapple</Combobox.Item>
<Combobox.Item>Strawberry</Combobox.Item>
<Combobox.Item>Watermelon</Combobox.Item>
<Combobox.Item>Kiwi</Combobox.Item>
<Combobox.Item>Lemon</Combobox.Item>
<Combobox.Item>Lime</Combobox.Item>
<Combobox.Item>Lemon</Combobox.Item>
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 | 🟡 Minor

Remove the duplicate “Lemon” option in the multiple selection demo.
It’s likely an accidental duplicate and can confuse the example.

✂️ Proposed fix
       <Combobox.Item>Kiwi</Combobox.Item>
       <Combobox.Item>Lemon</Combobox.Item>
       <Combobox.Item>Lime</Combobox.Item>
-      <Combobox.Item>Lemon</Combobox.Item>
📝 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
<Combobox multiple>
<Combobox.Input placeholder="Select fruits..." />
<Combobox.Input placeholder="Select fruits" width={300} />
<Combobox.Content>
<Combobox.Item>Apple</Combobox.Item>
<Combobox.Item>Banana</Combobox.Item>
<Combobox.Item>Grape</Combobox.Item>
<Combobox.Item>Orange</Combobox.Item>
<Combobox.Item>Pineapple</Combobox.Item>
<Combobox.Item>Mango</Combobox.Item>
<Combobox.Item>Pineapple</Combobox.Item>
<Combobox.Item>Strawberry</Combobox.Item>
<Combobox.Item>Watermelon</Combobox.Item>
<Combobox.Item>Kiwi</Combobox.Item>
<Combobox.Item>Lemon</Combobox.Item>
<Combobox.Item>Lime</Combobox.Item>
<Combobox.Item>Lemon</Combobox.Item>
<Combobox multiple>
<Combobox.Input placeholder="Select fruits" width={300} />
<Combobox.Content>
<Combobox.Item>Apple</Combobox.Item>
<Combobox.Item>Banana</Combobox.Item>
<Combobox.Item>Grape</Combobox.Item>
<Combobox.Item>Orange</Combobox.Item>
<Combobox.Item>Mango</Combobox.Item>
<Combobox.Item>Pineapple</Combobox.Item>
<Combobox.Item>Strawberry</Combobox.Item>
<Combobox.Item>Watermelon</Combobox.Item>
<Combobox.Item>Kiwi</Combobox.Item>
<Combobox.Item>Lemon</Combobox.Item>
<Combobox.Item>Lime</Combobox.Item>
</Combobox.Content>
</Combobox>
🤖 Prompt for AI Agents
In `@apps/www/src/content/docs/components/combobox/demo.ts` around lines 54 - 68,
The demo includes a duplicated option label "Lemon" in the multiple-selection
Combobox example; edit the demo in
apps/www/src/content/docs/components/combobox/demo.ts and remove the extra
Combobox.Item that renders "Lemon" so there is only one <Combobox.Item> with the
label "Lemon" in the <Combobox.Content> list (leave the other Combobox.Item
entries unchanged).

Comment on lines +85 to 86
const computedValue = providedValue ?? internalValue;

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

Respect explicit null values for controlled single-select.
Using ?? causes value={null} to fall back to internalValue, which can surface stale selections in context.

✅ Proposed fix
-  const computedValue = providedValue ?? internalValue;
+  const computedValue =
+    providedValue !== undefined ? providedValue : internalValue;
📝 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
const computedValue = providedValue ?? internalValue;
const computedValue =
providedValue !== undefined ? providedValue : internalValue;
🤖 Prompt for AI Agents
In `@packages/raystack/components/combobox/combobox-root.tsx` around lines 85 -
86, The computedValue assignment uses the nullish coalescing operator which
treats explicit null like undefined; update the logic that sets computedValue
(currently using providedValue and internalValue in combobox-root.tsx) to only
fall back to internalValue when providedValue is strictly undefined (e.g., check
providedValue === undefined), so a controlled value of null is preserved and not
replaced by internalValue; locate the computedValue variable near the top of the
component and change the fallback condition accordingly.

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: 1

🤖 Fix all issues with AI agents
In `@packages/raystack/components/combobox/combobox-root.tsx`:
- Around line 67-94: The inputValue is always from internal useState('') and
ignores externally controlled inputValue/defaultInputValue props; update
ComboboxRoot to accept inputValue and defaultInputValue props, initialize the
internal state with defaultInputValue (e.g., useState(defaultInputValue ?? '')),
derive a computedInputValue = props.inputValue ?? internalInputValue, and update
handleInputValueChange to setInternalInputValue via
setInputValue/setInternalInputValue and call onInputValueChange; ensure the
context/consumers use computedInputValue (not raw internal state) so
programmatic changes to inputValue or defaultInputValue are reflected.

Comment on lines +67 to +94
export const ComboboxRoot = <Value extends unknown | unknown[]>({
multiple = false,
children,
value: providedValue,
defaultValue = multiple ? [] : undefined,
onValueChange,
inputValue: providedInputValue,
onInputValueChange,
defaultInputValue,
open: providedOpen,
defaultOpen = false,
onOpenChange,
value: providedValue,
defaultValue,
items,
...props
}: ComboboxRootProps) => {
}: ComboboxRootProps<Value>) => {
const [inputValue, setInputValue] = useState('');
const [internalValue, setInternalValue] = useState<
string | string[] | undefined
>(defaultValue);
const [internalInputValue, setInternalInputValue] =
useState(defaultInputValue);
const [internalOpen, setInternalOpen] = useState(defaultOpen);
Value | Value[] | null | undefined
>(defaultValue ?? null);
const inputContainerRef = useRef<HTMLDivElement>(null);

const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const computedValue = providedValue ?? internalValue;

const value = providedValue ?? internalValue;
const inputValue = providedInputValue ?? internalInputValue;
const open = providedOpen ?? internalOpen;
const handleInputValueChange = useCallback(
(
value: string,
eventDetails: ComboboxPrimitive.Root.ChangeEventDetails
) => {
setInputValue(value);
onInputValueChange?.(value);
},
[onInputValueChange]
);
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

Sync context inputValue with controlled/default props.

inputValue is always sourced from internal state initialized to '', so defaultInputValue and externally controlled inputValue aren’t reflected in context. This can desync filtering/label behavior when parents programmatically set the input value.

✅ Proposed fix
 }: ComboboxRootProps<Value>) => {
-  const [inputValue, setInputValue] = useState('');
+  const providedInputValue = props.inputValue;
+  const defaultInputValue = props.defaultInputValue;
+  const [inputValue, setInputValue] = useState(defaultInputValue ?? '');
   const [internalValue, setInternalValue] = useState<
     Value | Value[] | null | undefined
   >(defaultValue ?? null);
   const inputContainerRef = useRef<HTMLDivElement>(null);

+  const computedInputValue =
+    providedInputValue !== undefined ? providedInputValue : inputValue;

   const handleInputValueChange = useCallback(
     (
       value: string,
       eventDetails: ComboboxPrimitive.Root.ChangeEventDetails
     ) => {
-      setInputValue(value);
+      if (providedInputValue === undefined) {
+        setInputValue(value);
+      }
       onInputValueChange?.(value);
     },
-    [onInputValueChange]
+    [onInputValueChange, providedInputValue]
   );

   const contextValue = useMemo(
     () => ({
       multiple,
-      inputValue,
+      inputValue: computedInputValue,
       hasItems: !!items,
       inputContainerRef,
       value: computedValue,
       onValueChange: handleValueChange
     }),
-    [multiple, inputValue, items, computedValue, handleValueChange]
+    [multiple, computedInputValue, items, computedValue, handleValueChange]
   );

Also applies to: 115-124

🤖 Prompt for AI Agents
In `@packages/raystack/components/combobox/combobox-root.tsx` around lines 67 -
94, The inputValue is always from internal useState('') and ignores externally
controlled inputValue/defaultInputValue props; update ComboboxRoot to accept
inputValue and defaultInputValue props, initialize the internal state with
defaultInputValue (e.g., useState(defaultInputValue ?? '')), derive a
computedInputValue = props.inputValue ?? internalInputValue, and update
handleInputValueChange to setInternalInputValue via
setInputValue/setInternalInputValue and call onInputValueChange; ensure the
context/consumers use computedInputValue (not raw internal state) so
programmatic changes to inputValue or defaultInputValue are reflected.

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