Skip to content

Select to combobox revamp#1659

Merged
kotAPI merged 8 commits intorad-ui:mainfrom
GoldGroove06:select-to-combobox-revamp
Dec 21, 2025
Merged

Select to combobox revamp#1659
kotAPI merged 8 commits intorad-ui:mainfrom
GoldGroove06:select-to-combobox-revamp

Conversation

@GoldGroove06
Copy link
Copy Markdown
Collaborator

@GoldGroove06 GoldGroove06 commented Dec 20, 2025

Summary by CodeRabbit

  • New Features

    • Adds a full Combobox UI: composable subcomponents, searchable input, grouping, indicators, and theme styles.
  • Refactor

    • Introduces a new Combobox primitive and migrates internal primitives; removes Select’s built-in Search from the Select API.
  • Documentation

    • New Storybook stories for Combobox; Select stories removed or updated.
  • Tests

    • Comprehensive interaction and accessibility tests added for Combobox behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Dec 20, 2025

🦋 Changeset detected

Latest commit: f2f78d1

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

This PR includes changesets to release 1 package
Name Type
@radui/ui Minor

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 Dec 20, 2025

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the changeset: a comprehensive refactoring that transforms the Select primitive into a Combobox primitive and reorganizes the themed Select and Combobox components.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 367ae36 and f2f78d1.

📒 Files selected for processing (1)
  • .changeset/eighty-books-post.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/eighty-books-post.md
⏰ 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). (3)
  • GitHub Check: Jest (2/2)
  • GitHub Check: Jest (1/2)
  • GitHub Check: coverage

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.

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

Caution

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

⚠️ Outside diff range comments (2)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx (2)

35-36: Update ID prefix to match Combobox naming.

The fallback ID uses select-item-${index}, which is a leftover from the Select component refactoring. It should be updated to combobox-item-${index} for consistency.

🔎 Proposed fix
     // Use the value prop for the ID, fallback to index if value is not provided
-    const itemId = value || `select-item-${index}`;
+    const itemId = value || `combobox-item-${index}`;

84-101: Remove duplicate tabIndex prop.

The tabIndex is set both inside getItemProps (line 85) and directly on the element (line 101). The second declaration will override the first, making the one in getItemProps ineffective and potentially causing unexpected behavior.

🔎 Proposed fix
             {...getItemProps({
                 tabIndex: disabled ? -1 : isActive ? 0 : -1,
                 onClick: () => !disabled && handleSelect(index),
                 onKeyDown: (event: React.KeyboardEvent) => {
                     if (disabled) return;
                     if (event.key === 'Enter') {
                         event.preventDefault();
                         handleSelect(index);
                     }

                     if (event.key === ' ' && !isTypingRef.current) {
                         event.preventDefault();
                         handleSelect(index);
                     }
                 }
             })}
             {...props}
-            tabIndex={disabled ? -1 : isActive ? 0 : -1}
         >
🧹 Nitpick comments (7)
src/components/ui/Combobox/fragments/ComboboxSearch.tsx (1)

8-19: Simplify by making the component self-closing.

The ComboboxPrimitive.Search element has empty children (lines 15-17). Since no children are rendered, make it self-closing for cleaner code.

🔎 Proposed refactor
 const ComboboxSearch = React.forwardRef<ComboboxSearchElement, ComboboxSearchProps>((props, forwardedRef) => {
     const { rootClass } = useContext(ComboboxRootContext);
     return (
         <ComboboxPrimitive.Search
             className={`${rootClass}-search`}
             ref={forwardedRef}
             {...props}
-        >
-
-        </ComboboxPrimitive.Search>
+        />
     );
 });
src/components/ui/Combobox/fragments/ComboboxIndicator.tsx (1)

8-17: Consider making the indicator icon customizable.

The SVG checkmark is hardcoded. For greater flexibility, consider accepting an optional icon prop to allow users to customize the indicator.

🔎 Proposed enhancement
 type ComboboxIndicatorElement = React.ElementRef<'div'>;
-type ComboboxIndicatorProps = React.ComponentPropsWithoutRef<'div'>;
+type ComboboxIndicatorProps = React.ComponentPropsWithoutRef<'div'> & {
+    icon?: React.ReactNode;
+};

-const ComboboxIndicator = React.forwardRef<ComboboxIndicatorElement, ComboboxIndicatorProps>((props, forwardedRef) => {
+const ComboboxIndicator = React.forwardRef<ComboboxIndicatorElement, ComboboxIndicatorProps>(({ icon, ...props }, forwardedRef) => {
     const { rootClass } = useContext(ComboboxRootContext);
     return (
         <div className={`${rootClass}-item-indicator`} ref={forwardedRef} {...props}>
-            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-                <path d="M13.3332 4L5.99984 11.3333L2.6665 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
-            </svg>
+            {icon || (
+                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+                    <path d="M13.3332 4L5.99984 11.3333L2.6665 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
+                </svg>
+            )}
         </div>
     );
 });
src/components/ui/Combobox/fragments/ComboboxItem.tsx (1)

7-9: Remove unused customRootClass prop.

The customRootClass prop is defined in the type signature and destructured in the component, but it's never used in the implementation. Consider removing it to keep the API clean.

🔎 Proposed refactor
 type ComboboxItemProps = React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Item> & {
-    customRootClass?: string;
 };

-const ComboboxItem = React.forwardRef<ComboboxItemElement, ComboboxItemProps>(({ customRootClass, children, value, disabled, ...props }, forwardedRef) => {
+const ComboboxItem = React.forwardRef<ComboboxItemElement, ComboboxItemProps>(({ children, value, disabled, ...props }, forwardedRef) => {

Also applies to: 11-11

src/components/ui/Combobox/fragments/ComboboxTrigger.tsx (1)

8-10: Remove unused customRootClass prop.

The customRootClass prop is defined in the type signature and destructured in the component, but it's never used in the implementation. Consider removing it to keep the API clean.

🔎 Proposed refactor
 type ComboboxTriggerProps = React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Trigger> & {
-    customRootClass?: string;
     placeholder?: boolean;
 };

-const ComboboxTrigger = React.forwardRef<ComboboxTriggerElement, ComboboxTriggerProps>(({ customRootClass, children, disabled, placeholder, ...props }, forwardedRef) => {
+const ComboboxTrigger = React.forwardRef<ComboboxTriggerElement, ComboboxTriggerProps>(({ children, disabled, placeholder, ...props }, forwardedRef) => {

Also applies to: 13-13

src/components/ui/Combobox/fragments/ComboboxContent.tsx (1)

7-9: Remove unused customRootClass prop.

The customRootClass prop is defined in the type signature and destructured in the component, but it's never used in the implementation. Consider removing it to keep the API clean.

🔎 Proposed refactor
 type ComboboxContentProps = React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Content> & {
-    customRootClass?: string;
 };

-const ComboboxContent = React.forwardRef<ComboboxContentElement, ComboboxContentProps>(({ customRootClass, children, position = 'popper', ...props }, forwardedRef) => {
+const ComboboxContent = React.forwardRef<ComboboxContentElement, ComboboxContentProps>(({ children, position = 'popper', ...props }, forwardedRef) => {

Also applies to: 11-11

src/core/primitives/Combobox/stories/Combobox.stories.tsx (2)

45-45: Fix typo in trigger text.

"helo" should be "hello".

🔎 Proposed fix
                 <ComboboxPrimitive.Trigger>
-            helo
+            hello
                 </ComboboxPrimitive.Trigger>

154-169: Clarify commented Portal usage.

The ComboboxPrimitive.Portal is commented out in the SearchCombobox story. If this is intentional for demonstration purposes (e.g., showing in-place rendering vs. portal rendering), consider adding a comment explaining why. Otherwise, ensure consistency with other stories.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a80d9b and 2692aff.

📒 Files selected for processing (38)
  • src/components/ui/Combobox/Combobox.tsx (1 hunks)
  • src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1 hunks)
  • src/components/ui/Combobox/fragments/ComboboxContent.tsx (1 hunks)
  • src/components/ui/Combobox/fragments/ComboboxGroup.tsx (1 hunks)
  • src/components/ui/Combobox/fragments/ComboboxIndicator.tsx (1 hunks)
  • src/components/ui/Combobox/fragments/ComboboxItem.tsx (1 hunks)
  • src/components/ui/Combobox/fragments/ComboboxPortal.tsx (1 hunks)
  • src/components/ui/Combobox/fragments/ComboboxRoot.tsx (1 hunks)
  • src/components/ui/Combobox/fragments/ComboboxSearch.tsx (1 hunks)
  • src/components/ui/Combobox/fragments/ComboboxTrigger.tsx (1 hunks)
  • src/components/ui/Combobox/stories/Combobox.stories.tsx (1 hunks)
  • src/components/ui/Combobox/tests/Combobox.full.test.tsx (1 hunks)
  • src/components/ui/Combobox/tests/Combobox.test.tsx (2 hunks)
  • src/components/ui/Select/Select.tsx (0 hunks)
  • src/components/ui/Select/fragments/SelectContent.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectGroup.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectItem.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectPortal.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectRoot.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectSearch.tsx (0 hunks)
  • src/components/ui/Select/fragments/SelectTrigger.tsx (1 hunks)
  • src/components/ui/Select/stories/Select.stories.tsx (0 hunks)
  • src/core/primitives/Combobox/ComboboxPrimitive.tsx (1 hunks)
  • src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (2 hunks)
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveContent.tsx (2 hunks)
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveGroup.tsx (2 hunks)
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx (2 hunks)
  • src/core/primitives/Combobox/fragments/ComboboxPrimitivePortal.tsx (2 hunks)
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx (4 hunks)
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveSearch.tsx (2 hunks)
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx (2 hunks)
  • src/core/primitives/Combobox/stories/Combobox.stories.tsx (1 hunks)
  • src/core/primitives/Combobox/tests/Combobox.test.tsx (1 hunks)
  • src/core/primitives/Select/Select.tsx (0 hunks)
  • src/core/primitives/Select/stories/Select.stories.tsx (0 hunks)
  • styles/themes/components/combobox.scss (1 hunks)
  • styles/themes/components/select.scss (0 hunks)
  • styles/themes/default.scss (1 hunks)
💤 Files with no reviewable changes (6)
  • styles/themes/components/select.scss
  • src/components/ui/Select/stories/Select.stories.tsx
  • src/components/ui/Select/fragments/SelectSearch.tsx
  • src/components/ui/Select/Select.tsx
  • src/core/primitives/Select/Select.tsx
  • src/core/primitives/Select/stories/Select.stories.tsx
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2024-12-12T08:22:59.375Z
Learnt from: decipher-cs
Repo: rad-ui/ui PR: 417
File: src/components/ui/Dropdown/Dropdown.tsx:0-0
Timestamp: 2024-12-12T08:22:59.375Z
Learning: The `Dropdown.Trigger` component is customizable and needs to be used with `Dropdown.Root`.

Applied to files:

  • src/components/ui/Combobox/fragments/ComboboxRoot.tsx
  • src/components/ui/Combobox/fragments/ComboboxTrigger.tsx
  • src/components/ui/Combobox/Combobox.tsx
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx
📚 Learning: 2024-12-12T08:34:33.079Z
Learnt from: decipher-cs
Repo: rad-ui/ui PR: 417
File: src/components/ui/Dropdown/Dropdown.stories.tsx:43-50
Timestamp: 2024-12-12T08:34:33.079Z
Learning: Ensure to verify existing ARIA attributes in components before suggesting additions during code reviews, especially in the `Dropdown.Trigger` component in `src/components/ui/Dropdown/Dropdown.stories.tsx`.

Applied to files:

  • styles/themes/components/combobox.scss
  • src/core/primitives/Combobox/tests/Combobox.test.tsx
  • src/components/ui/Combobox/fragments/ComboboxTrigger.tsx
  • src/components/ui/Combobox/stories/Combobox.stories.tsx
  • src/components/ui/Combobox/tests/Combobox.test.tsx
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx
  • src/components/ui/Combobox/tests/Combobox.full.test.tsx
  • src/core/primitives/Combobox/stories/Combobox.stories.tsx
  • src/components/ui/Select/fragments/SelectTrigger.tsx
📚 Learning: 2024-11-24T06:43:42.194Z
Learnt from: kotAPI
Repo: rad-ui/ui PR: 576
File: src/core/primitives/Toggle/index.tsx:15-22
Timestamp: 2024-11-24T06:43:42.194Z
Learning: In the `TogglePrimitive` component (`src/core/primitives/Toggle/index.tsx`), when the component becomes controlled, it's acceptable to not sync the internal `isPressed` state with the external `pressed` prop.

Applied to files:

  • src/components/ui/Select/fragments/SelectPortal.tsx
  • src/core/primitives/Combobox/tests/Combobox.test.tsx
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx
  • src/components/ui/Select/fragments/SelectTrigger.tsx
📚 Learning: 2025-07-18T16:43:26.175Z
Learnt from: GoldGroove06
Repo: rad-ui/ui PR: 1215
File: src/core/primitives/CheckboxGroup/fragments/CheckboxGroupPrimitiveTrigger.tsx:25-27
Timestamp: 2025-07-18T16:43:26.175Z
Learning: In the CheckboxGroupPrimitiveTrigger component (`src/core/primitives/CheckboxGroup/fragments/CheckboxGroupPrimitiveTrigger.tsx`), the component uses two separate useEffect hooks with different purposes: the first useEffect (lines 25-27) with empty dependency array `[]` is meant to set the initial state only once on mount by syncing with the group's checked values, while the second useEffect (lines 28-34) handles ongoing state updates by propagating local `isChecked` changes back to the group's `checkedValues`. This pattern prevents infinite loops while maintaining proper synchronization.

Applied to files:

  • src/core/primitives/Combobox/tests/Combobox.test.tsx
  • src/components/ui/Combobox/fragments/ComboboxTrigger.tsx
  • src/components/ui/Combobox/fragments/ComboboxGroup.tsx
  • src/components/ui/Select/fragments/SelectGroup.tsx
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx
  • src/components/ui/Select/fragments/SelectTrigger.tsx
🧬 Code graph analysis (16)
src/components/ui/Combobox/fragments/ComboboxIndicator.tsx (1)
src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1)
  • ComboboxRootContext (7-9)
src/components/ui/Combobox/fragments/ComboboxRoot.tsx (1)
src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1)
  • ComboboxRootContext (7-9)
src/components/ui/Combobox/fragments/ComboboxItem.tsx (1)
src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1)
  • ComboboxRootContext (7-9)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveContent.tsx (1)
src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (1)
  • ComboboxPrimitiveContext (34-34)
src/components/ui/Combobox/fragments/ComboboxContent.tsx (1)
src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1)
  • ComboboxRootContext (7-9)
src/components/ui/Combobox/fragments/ComboboxTrigger.tsx (1)
src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1)
  • ComboboxRootContext (7-9)
src/components/ui/Combobox/stories/Combobox.stories.tsx (1)
src/components/ui/Select/stories/Select.stories.tsx (1)
  • Basic (11-33)
src/components/ui/Combobox/fragments/ComboboxGroup.tsx (1)
src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1)
  • ComboboxRootContext (7-9)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx (1)
src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (1)
  • ComboboxPrimitiveContext (34-34)
src/components/ui/Combobox/tests/Combobox.full.test.tsx (1)
test-utils/index.ts (1)
  • axe (56-62)
src/components/ui/Combobox/fragments/ComboboxSearch.tsx (1)
src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1)
  • ComboboxRootContext (7-9)
src/core/primitives/Combobox/stories/Combobox.stories.tsx (2)
src/components/ui/Select/stories/Select.stories.tsx (2)
  • ControlledExample (142-166)
  • GroupExample (168-191)
docs/components/layout/Documentation/helpers/ComponentHero/ComponentHero.js (1)
  • data (32-32)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveSearch.tsx (1)
src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (1)
  • ComboboxPrimitiveContext (34-34)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx (1)
src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (1)
  • ComboboxPrimitiveContext (34-34)
src/core/primitives/Combobox/fragments/ComboboxPrimitivePortal.tsx (1)
src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (1)
  • ComboboxPrimitiveContext (34-34)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx (1)
src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (1)
  • ComboboxPrimitiveContext (34-34)
🪛 GitHub Actions: Coverage
src/components/ui/Combobox/Combobox.tsx

[error] 38-38: TS2339: Property 'Search' does not exist on type 'ComboboxComponent'.

🪛 GitHub Actions: Tests
src/components/ui/Combobox/Combobox.tsx

[error] 38-38: TS2339: Property 'Search' does not exist on type 'ComboboxComponent'.

🪛 GitHub Check: lint
src/components/ui/Combobox/tests/Combobox.full.test.tsx

[warning] 147-147:
A form label must be associated with a control

🔇 Additional comments (20)
src/components/ui/Combobox/contexts/ComboboxRootContext.tsx (1)

1-9: LGTM!

The context implementation is clean and follows standard React patterns. The empty string default for rootClass is a safe choice.

src/core/primitives/Combobox/tests/Combobox.test.tsx (1)

1-42: LGTM!

The test suite comprehensively covers the key behaviors:

  • Ref forwarding to the underlying button element
  • Hidden select rendering for form integration with proper ARIA attributes
  • Clean render without console warnings

Test structure follows best practices with proper mock cleanup.

src/components/ui/Combobox/tests/Combobox.test.tsx (1)

1-42: LGTM!

The test migration from SelectPrimitive to Combobox API is clean and complete. All test cases maintain the same coverage:

  • Ref forwarding validation
  • Hidden select form integration
  • Warning-free rendering
src/core/primitives/Combobox/fragments/ComboboxPrimitiveSearch.tsx (1)

7-113: LGTM with minor type safety concern.

The component renaming from SelectPrimitiveSearch to ComboboxPrimitiveSearch and context migration are correctly implemented. However, there are multiple @ts-ignore comments (lines 19, 24, 88, 95) that suppress TypeScript errors for the type attribute and event handlers.

Consider replacing @ts-ignore with proper type assertions or fixing the underlying type definitions. For example:

// Instead of @ts-ignore
<Primitive.input
    type="search"
    onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
    {...props}
/>

If Primitive.input doesn't support these props correctly, you may need to update the Primitive type definitions.

src/components/ui/Combobox/fragments/ComboboxRoot.tsx (1)

13-34: LGTM!

The ComboboxRoot component correctly wraps ComboboxPrimitive.Root with context provider setup, forwards all necessary props, and applies the root class styling pattern consistently.

styles/themes/components/combobox.scss (1)

1-213: LGTM!

Comprehensive SCSS styling for the Combobox component with:

  • Proper accessibility support (focus states, ARIA attributes, keyboard navigation indicators)
  • Consistent theming using CSS custom properties
  • Well-organized state management (hover, focus, disabled, highlighted, selected)
  • Responsive and flexible sizing with CSS variables

The styling aligns well with the new Combobox primitive architecture.

src/core/primitives/Combobox/fragments/ComboboxPrimitiveGroup.tsx (1)

3-21: LGTM!

Clean renaming from SelectPrimitiveGroup to ComboboxPrimitiveGroup with consistent updates to type names, component declaration, and display name. The implementation remains unchanged, which is appropriate for this migration.

src/core/primitives/Combobox/fragments/ComboboxPrimitivePortal.tsx (1)

1-31: LGTM! Clean refactoring from Select to Combobox.

The portal component has been properly refactored with consistent naming updates. The conditional rendering logic based on isOpen and root element detection is correct.

src/components/ui/Combobox/stories/Combobox.stories.tsx (1)

20-33: Verify Portal wrapper is not required.

The Select component pattern (from Select.stories.tsx) wraps Select.Content inside Select.Portal. This story renders Combobox.Content directly under Combobox.Root without a Combobox.Portal wrapper. Please verify whether this is an intentional API difference or if the Portal wrapper should be added for consistency.

🔎 Expected pattern based on Select component
            <Combobox.Root>
                <Combobox.Trigger>
                    Select an option
                </Combobox.Trigger>
+               <Combobox.Portal>
                    <Combobox.Content>
                        <Combobox.Search />
                        <Combobox.Group>
                            <Combobox.Item value="g1option 1">g1Option 1</Combobox.Item>
                            <Combobox.Item value="g1option 2">g1Option 2</Combobox.Item>
                            <Combobox.Item value="g1option 3">g1Option 3</Combobox.Item>
                        </Combobox.Group>

                        <Combobox.Group>
                            <Combobox.Item value="g2option 1">g2Option 1</Combobox.Item>
                            <Combobox.Item value="g2option 2">g2Option 2</Combobox.Item>
                            <Combobox.Item value="g2option 3">g2Option 3</Combobox.Item>
                        </Combobox.Group>
                    </Combobox.Content>
+               </Combobox.Portal>
            </Combobox.Root>
src/components/ui/Combobox/fragments/ComboboxPortal.tsx (1)

1-24: LGTM! Clean portal wrapper implementation.

The component correctly wraps ComboboxPrimitive.Portal, forwards refs and props appropriately, and maintains proper TypeScript typing.

src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (1)

1-34: LGTM! Context refactoring is correct.

The context has been properly renamed from SelectPrimitiveContext to ComboboxPrimitiveContext with all type definitions updated consistently.

src/core/primitives/Combobox/fragments/ComboboxPrimitiveContent.tsx (1)

3-46: LGTM! Clean refactor from Select to Combobox.

The renaming from SelectPrimitiveContent to ComboboxPrimitiveContent is consistent throughout, and the context migration to ComboboxPrimitiveContext is correct.

src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx (1)

3-51: LGTM! Consistent refactor from Select to Combobox.

The component has been properly renamed to ComboboxPrimitiveTrigger with correct context usage (ComboboxPrimitiveContext). The implementation remains clean and functional.

src/components/ui/Combobox/Combobox.tsx (1)

12-15: LGTM! Good developer experience with direct usage warning.

The base component warns users appropriately when they attempt to use Combobox directly instead of via the composition API (e.g., Combobox.Root).

src/components/ui/Combobox/fragments/ComboboxGroup.tsx (1)

1-28: LGTM! Clean wrapper implementation.

The component correctly consumes ComboboxRootContext to apply contextual styling and forwards all props to ComboboxPrimitive.Group. The implementation follows the established pattern across other Combobox fragments.

src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx (1)

4-181: LGTM! Solid refactor with complex state management.

The component has been properly renamed to ComboboxPrimitiveRoot with correct context wiring to ComboboxPrimitiveContext. The implementation handles floating UI positioning, keyboard navigation, typeahead, and form integration correctly.

src/components/ui/Combobox/tests/Combobox.full.test.tsx (1)

1-210: Excellent comprehensive test coverage!

The test suite thoroughly validates keyboard navigation, form submission, controlled/uncontrolled values, portal rendering, accessibility (including axe-core validation), asChild usage, and disabled state handling. This ensures the Combobox component behaves correctly across all scenarios.

Note: The static analysis warning about the label on line 147 is a false positive—the label is properly associated with the combobox via aria-labelledby="label" on line 149.

src/core/primitives/Combobox/stories/Combobox.stories.tsx (1)

1-175: Excellent story coverage!

The stories comprehensively demonstrate basic usage, controlled state, form integration, grouped items, and search functionality. This provides clear examples for consumers of the Combobox primitive.

src/core/primitives/Combobox/ComboboxPrimitive.tsx (2)

10-13: LGTM! Good developer guidance with direct usage warning.

The base component appropriately warns developers against direct usage and guides them toward the composition API (e.g., Combobox.Root, Combobox.Content).


17-25: LGTM! Complete interface definition.

The ComboboxPrimitiveComponent interface correctly includes all static subcomponents, including Search. This avoids the TypeScript issue present in the non-primitive Combobox component.

Comment thread src/components/ui/Combobox/Combobox.tsx
Comment thread src/components/ui/Combobox/stories/Combobox.stories.tsx Outdated
Comment thread src/components/ui/Combobox/tests/Combobox.full.test.tsx Outdated
Comment thread src/components/ui/Select/fragments/SelectContent.tsx Outdated
Comment thread src/components/ui/Select/fragments/SelectGroup.tsx Outdated
Comment thread src/components/ui/Select/fragments/SelectItem.tsx Outdated
Comment thread src/components/ui/Select/fragments/SelectPortal.tsx Outdated
Comment thread src/components/ui/Select/fragments/SelectRoot.tsx Outdated
Comment thread src/components/ui/Select/fragments/SelectTrigger.tsx Outdated
Comment thread styles/themes/default.scss Outdated
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)
src/components/ui/Select/fragments/SelectItem.tsx (1)

3-3: API compatibility verified — all props fully supported.

The ComboboxPrimitive.Item API accepts all props being passed: value, disabled, className, data-disabled, role, aria-disabled, and ref. The migration is correctly implemented.

Note: The component redundantly sets role="option" and aria-disabled internally in ComboboxPrimitiveItem (lines 76, 82), which are also explicitly passed from SelectItem. Consider removing these from SelectItem to avoid duplication, allowing ComboboxPrimitiveItem to manage them exclusively.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9694e6 and c0abcd1.

📒 Files selected for processing (9)
  • src/components/ui/Combobox/stories/Combobox.stories.tsx (1 hunks)
  • src/components/ui/Combobox/tests/Combobox.full.test.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectContent.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectGroup.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectItem.tsx (2 hunks)
  • src/components/ui/Select/fragments/SelectPortal.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectRoot.tsx (3 hunks)
  • src/components/ui/Select/fragments/SelectTrigger.tsx (3 hunks)
  • styles/themes/default.scss (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/ui/Select/fragments/SelectTrigger.tsx
  • src/components/ui/Select/fragments/SelectGroup.tsx
  • styles/themes/default.scss
  • src/components/ui/Combobox/stories/Combobox.stories.tsx
  • src/components/ui/Select/fragments/SelectContent.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-12-12T08:34:33.079Z
Learnt from: decipher-cs
Repo: rad-ui/ui PR: 417
File: src/components/ui/Dropdown/Dropdown.stories.tsx:43-50
Timestamp: 2024-12-12T08:34:33.079Z
Learning: Ensure to verify existing ARIA attributes in components before suggesting additions during code reviews, especially in the `Dropdown.Trigger` component in `src/components/ui/Dropdown/Dropdown.stories.tsx`.

Applied to files:

  • src/components/ui/Select/fragments/SelectItem.tsx
  • src/components/ui/Combobox/tests/Combobox.full.test.tsx
🧬 Code graph analysis (2)
src/components/ui/Select/fragments/SelectItem.tsx (1)
src/components/ui/Select/contexts/SelectRootContext.tsx (1)
  • SelectRootContext (7-9)
src/components/ui/Combobox/tests/Combobox.full.test.tsx (1)
test-utils/index.ts (1)
  • axe (56-62)
🪛 GitHub Check: lint
src/components/ui/Combobox/tests/Combobox.full.test.tsx

[warning] 147-147:
A form label must be associated with a control

⏰ 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). (3)
  • GitHub Check: Jest (1/2)
  • GitHub Check: Jest (2/2)
  • GitHub Check: coverage
🔇 Additional comments (9)
src/components/ui/Select/fragments/SelectPortal.tsx (1)

3-3: LGTM! Previous import alias issue resolved.

The misleading import alias flagged in the previous review has been addressed. The import now correctly uses ComboboxPrimitive (matching the actual module), and all type references and JSX usages have been updated consistently. The component properly wraps ComboboxPrimitive.Portal while maintaining the Select public API.

src/components/ui/Combobox/tests/Combobox.full.test.tsx (7)

8-39: LGTM! Comprehensive keyboard navigation test.

The test thoroughly validates keyboard interactions, state transitions, and accessibility attributes. The typo from the previous review has been corrected.


41-74: LGTM! Form integration test is thorough.

The test validates both form submission behavior and correctly ensures disabled options cannot be selected, which is critical for accessibility and data integrity.


76-119: LGTM! Well-structured test for controlled and uncontrolled patterns.

The test validates both React state management patterns effectively, ensuring the Combobox works correctly in both scenarios.


121-142: LGTM! Portal and focus management test is solid.

The test validates both custom portal container rendering and proper focus return behavior, which are essential for accessibility.


144-167: LGTM! Accessibility test is comprehensive.

The test validates both axe-core violations and specific ARIA attributes. The label on line 147 is correctly used for aria-labelledby reference, making the static analysis warning a false positive.


169-191: LGTM! asChild behavior is validated correctly.

The test ensures that the asChild prop properly delegates rendering to the child element while preserving Combobox functionality. The type assertion on line 173 is an acceptable workaround for polymorphic ref typing.


193-209: LGTM! Disabled state test is appropriate.

The test validates that a disabled trigger correctly prevents the listbox from opening, which is essential for proper disabled state handling.

src/components/ui/Select/fragments/SelectRoot.tsx (1)

2-2: Migration to ComboboxPrimitive implemented correctly.

The import properly references ComboboxPrimitive, and all type references and JSX usage have been updated consistently. The ComboboxPrimitive.Root API supports all props being passed, including the shift prop (boolean) and onValueChange callback signature ((value: string) => void).

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import axe from 'axe-core';
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.

🛠️ Refactor suggestion | 🟠 Major

Use the custom axe wrapper from test-utils.

The import uses axe-core directly instead of the custom axe wrapper from test-utils. Based on the relevant code snippets, the project has a standardized axe helper that applies ACCESSIBILITY_TEST_TAGS configuration for consistent testing.

🔎 Proposed fix
-import axe from 'axe-core';
+import { axe } from '@/test-utils';

Then update line 163 to use the wrapper appropriately:

-        const results = await axe.run(container, { rules: { 'color-contrast': { enabled: false } } });
+        const results = await axe(container);

Note: If you need to disable the color-contrast rule, you may need to extend the custom wrapper or verify if the wrapper already handles this configuration.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/ui/Combobox/tests/Combobox.full.test.tsx around line 4, the
test imports axe directly from 'axe-core' but the project provides a custom axe
wrapper in test-utils that applies ACCESSIBILITY_TEST_TAGS and other
standardized configuration; replace the direct import with the custom wrapper
import from the project's test-utils and update the usage at line 163 to call
the wrapper's axe function (or pass options via that wrapper) so the
ACCESSIBILITY_TEST_TAGS configuration is applied consistently — if
color-contrast needs to be disabled, extend or configure the custom wrapper to
disable that rule before using it in the test.

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

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0abcd1 and 367ae36.

📒 Files selected for processing (1)
  • .changeset/eighty-books-post.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
.changeset/eighty-books-post.md

[grammar] ~5-~5: Ensure spelling is correct
Context: ...nor --- renamed the select to combobox primtive and separate themed comps for select an...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

⏰ 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). (2)
  • GitHub Check: Build
  • GitHub Check: coverage
🔇 Additional comments (1)
.changeset/eighty-books-post.md (1)

2-2: Verify version bump aligns with breaking changes.

The "minor" version bump typically indicates backward-compatible additions. However, the PR summary indicates removing Select.Search from the API and renaming primitives, which could be breaking changes. Please verify that the version bump (minor vs. major) accurately reflects the nature and scope of these changes.

Comment thread .changeset/eighty-books-post.md Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@kotAPI kotAPI merged commit 140d24d into rad-ui:main Dec 21, 2025
6 of 7 checks passed
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.

2 participants