Skip to content

refactor: forward refs in Select components#1430

Closed
kotAPI wants to merge 2 commits intomainfrom
kotapi/refactor-select-component-with-forwardref
Closed

refactor: forward refs in Select components#1430
kotAPI wants to merge 2 commits intomainfrom
kotapi/refactor-select-component-with-forwardref

Conversation

@kotAPI
Copy link
Copy Markdown
Collaborator

@kotAPI kotAPI commented Sep 5, 2025

Summary

  • refactor Select primitives and UI fragments to forward refs
  • add tests for ref forwarding and hidden native select accessibility

Testing

  • npm test

https://chatgpt.com/codex/tasks/task_e_68b9baa8268083319e5073c99aa10cba

Summary by CodeRabbit

  • Refactor

    • Modernized Select and subcomponents to support ref forwarding and clearer, type-safe props.
    • Introduced namespaced subcomponents for cleaner composition (e.g., Select.Root, Select.Content, Select.Item, Trigger, Portal, Group, Indicator, Search).
    • Improved className handling and prop passthrough; added display names for easier debugging.
  • Tests

    • Added comprehensive tests covering ref forwarding (Root, Trigger), accessibility within forms (hidden native select), and snapshot stability.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 5, 2025

Warning

Rate limit exceeded

@kotAPI has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 12 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 8cb6ac8 and aa3cbf4.

📒 Files selected for processing (10)
  • src/components/ui/Select/fragments/SelectContent.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectGroup.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectIndicator.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectRoot.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectSearch.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectTrigger.tsx (1 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveContent.tsx (1 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveRoot.tsx (4 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveSearch.tsx (1 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveTrigger.tsx (1 hunks)

Walkthrough

Refactors Select and its fragments/primitives to use React.forwardRef, adds typed props, attaches static subcomponents to the Select export, and introduces displayName metadata. Updates core Select primitives to forward refs and tighten prop types. Adds tests validating ref forwarding, hidden native select in forms, and snapshots.

Changes

Cohort / File(s) Summary
UI Select compound export
src/components/ui/Select/Select.tsx
Converts Select to a forwardRef component, defines SelectComponent type, attaches static subcomponents (Root, Content, Item, Trigger, Portal, Group, Indicator, Search), sets displayName, default-exports.
UI Select fragments (forwardRef + typings)
src/components/ui/Select/fragments/* (SelectContent.tsx, SelectGroup.tsx, SelectIndicator.tsx, SelectItem.tsx, SelectRoot.tsx, SelectSearch.tsx, SelectTrigger.tsx)
Converts each fragment to React.forwardRef, introduces/updates typed props (extending underlying primitives), forwards refs to underlying elements, preserves className/context logic, adds displayName.
Core Select primitives (forwardRef + ref merge + props tighten)
src/core/primitives/Select/fragments/* (SelectPrimitiveContent.tsx, SelectPrimitiveGroup.tsx, SelectPrimitiveItem.tsx, SelectPrimitiveRoot.tsx, SelectPrimitiveSearch.tsx, SelectPrimitiveTrigger.tsx)
Refactors primitives to React.forwardRef, replaces permissive props with React.ComponentPropsWithoutRef-based types, merges internal/external refs via Floater.useMergeRefs, keeps behavior, adds displayName.
Tests
src/components/ui/Select/tests/Select.test.tsx
Adds tests for ref forwarding (Root/Trigger), hidden native select in a form, console warning/error guards, and snapshot for basic composition.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant App as App
  participant Sel as Select.Root/Trigger
  participant Prim as SelectPrimitive (Root/Trigger/Content)
  participant Flo as Floater

  U->>Sel: Click Trigger
  Sel->>Prim: Forward props + ref
  Prim->>Flo: useMergeRefs([internal, forwarded])
  Prim->>Prim: Toggle isOpen
  Prim->>Prim: Render Content (when open)
  Prim->>U: Display options

  rect rgba(200,240,255,0.3)
    note over App,Prim: New: refs are forwarded from App through Select to primitives
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • GoldGroove06

Poem

A twitch of the ear, a hop, then two,
I forward my refs like carrots I strew.
Roots and Triggers line up in a row—
Floater lifts Content, options aglow.
With tidy types and names that sing,
This Select now feels like spring. 🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kotapi/refactor-select-component-with-forwardref

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

Caution

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

⚠️ Outside diff range comments (6)
src/core/primitives/Select/fragments/SelectPrimitiveSearch.tsx (2)

47-81: Guard DOM queries and fix effect deps.

  • Access refs.floating.current with optional chaining.
  • Avoid [refs.floating.current] in deps; it’s unstable. Depend on the ref object or hasContext.
-    React.useEffect(() => {
-        if (!refs.floating.current) return;
+    React.useEffect(() => {
+        if (!context?.refs?.floating?.current) return;
 
-        const floatingElement = refs.floating.current;
+        const floatingElement = context.refs.floating.current!;
@@
-        context.setActiveIndex(null);
-        elementsRef.current = visible.map(item => item.element);
-        labelsRef.current = visible.map(item => item.label);
-    }, [search, refs.floating.current]);
+        context.setActiveIndex(null);
+        context.elementsRef.current = visible.map(item => item.element);
+        context.labelsRef.current = visible.map(item => item.label);
+    }, [search, context?.refs?.floating]);

39-46: Call hooks unconditionally; guard side effects instead of early-returning.

This effect can run unconditionally and no-op when context is missing.

-    React.useEffect(() => {
-        setHasSearch(true);
-        // Reset navigation state
-        if (context) {
-            context.setActiveIndex(0);
-        }
-    }, [setHasSearch]);
+    React.useEffect(() => {
+        if (!context) return;
+        context.setHasSearch(true);
+        context.setActiveIndex(0);
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [context]);
src/core/primitives/Select/fragments/SelectPrimitiveItem.tsx (3)

19-27: Remove early return before hooks; fix wrong ref type.

The early return violates the Rules of Hooks, and itemRef should be HTMLDivElement (root is Primitive.div). Also, don’t call hooks inside JSX.

-    if (!context) {
-        console.error('SelectPrimitiveItem must be used within a SelectPrimitive');
-        return null;
-    }
+    // Assuming provider is always present. If not, make context default safe rather than early-returning here.

-    const itemRef = React.useRef<HTMLButtonElement>(null);
-    const { ref: listItemRef, index } = Floater.useListItem({ label: value });
+    const itemRef = React.useRef<HTMLDivElement>(null);
+    const { ref: listItemRef, index } = Floater.useListItem({ label: value });
+    const mergedRef = Floater.useMergeRefs([ref, listItemRef, itemRef]);

41-67: Respect disabled state, fix strict equality, and avoid calling hooks in JSX.

Disabled items should be non-interactive and unfocusable. Also prefer strict equality and use the precomputed mergedRef.

-        <Primitive.div
-            ref={Floater.useMergeRefs([ref, listItemRef, itemRef])}
+        <Primitive.div
+            ref={mergedRef}
             id={itemId}
             role="option"
             className={className}
             data-value={value}
-            data-active={!hasSearch ? isActive : virtualItemRef.current?.id == itemId }
-            aria-selected={isSelected}
-            tabIndex={isActive ? 0 : -1}
+            data-active={!hasSearch ? isActive : virtualItemRef.current?.id === itemId}
+            aria-selected={isSelected}
+            aria-disabled={disabled || undefined}
+            data-disabled={disabled ? '' : undefined}
+            tabIndex={disabled ? -1 : (isActive ? 0 : -1)}
             {...getItemProps({
-                onClick: () => handleSelect(index),
+                onClick: () => { if (!disabled) handleSelect(index); },
                 onKeyDown: (event: React.KeyboardEvent) => {
-                    if (event.key === 'Enter') {
+                    if (disabled) return;
+                    if (event.key === 'Enter') {
                         event.preventDefault();
                         handleSelect(index);
                     }
 
-                    if (event.key === ' ' && !isTypingRef.current) {
+                    if (event.key === ' ' && !isTypingRef.current) {
                         event.preventDefault();
                         handleSelect(index);
                     }
                 }
             })}
             {...props}
         >

1-75: Hoist Floater.useMergeRefs calls out of JSX attributes
Move all Floater.useMergeRefs([...]) invocations to top-level constants before the return in the following components:
• SelectPrimitiveTrigger.tsx (l.20)
• SelectPrimitiveSearch.tsx (l.26, l.92)
• SelectPrimitiveItem.tsx (l.42)
• SelectPrimitiveRoot.tsx (l.142)
• SelectPrimitiveContent.tsx (l.22)
• MenuPrimitiveTrigger.tsx (l.25)

src/core/primitives/Select/fragments/SelectPrimitiveRoot.tsx (1)

25-31: Submit the option value (not label) via hidden select; keep label separate.

Currently, the form submits the label and onValueChange receives the label. Track value separately and update both on selection.

-    const [selectedLabel, setSelectedLabel] = useControllableState(
-        value,
-        defaultValue,
-        onValueChange
-    );
+    // Controlled "value" for form/onValueChange; label for display only.
+    const [selectedValue, setSelectedValue] = useControllableState(
+        value,
+        defaultValue,
+        onValueChange
+    );
+    const [selectedLabel, setSelectedLabel] = React.useState<string>('');
@@
-    const handleSelect = React.useCallback((index: number | null) => {
-        setSelectedIndex(index);
-        setIsOpen(false);
-        if (index !== null) {
-            const label = labelsRef.current[index];
-            if (label) {
-                setSelectedLabel(label);
-            }
-        }
-    }, []);
+    const handleSelect = React.useCallback((index: number | null) => {
+        setSelectedIndex(index);
+        setIsOpen(false);
+        if (index !== null) {
+            const label = labelsRef.current[index];
+            const nextValue = valuesRef.current[index];
+            if (label != null) setSelectedLabel(label);
+            if (nextValue != null) setSelectedValue(nextValue);
+        }
+    }, [labelsRef, valuesRef, setSelectedValue]);
@@
-                        <select
+                        <select
                             name={name}
-                            value={selectedLabel}
+                            value={selectedValue}
                             hidden
                             aria-hidden="true"
                             tabIndex={-1}
                         >
-                            <option value={selectedLabel}>{selectedLabel}</option>
+                            <option value={selectedValue}>{selectedLabel || selectedValue}</option>
                         </select>

Also add selectedValue to context if needed by other fragments:

         const values = {
@@
-        selectedLabel,
+        selectedLabel,
+        selectedValue,

Also applies to: 65-74, 148-156

🧹 Nitpick comments (20)
src/components/ui/Select/fragments/SelectGroup.tsx (1)

5-7: Optional: avoid redundant children typing.

children is already included in React.ComponentPropsWithoutRef<typeof SelectPrimitive.Group>. You can drop the explicit children to reduce duplication.

-export type SelectGroupProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Group> & {
-    children: React.ReactNode
-};
+export type SelectGroupProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Group>;
src/core/primitives/Select/fragments/SelectPrimitiveGroup.tsx (1)

3-6: Simplify props type; avoid duplicating built-in div props.

React.ComponentPropsWithoutRef<'div'> already includes children and className. Duplicating them adds noise and risks divergence.

-export type SelectPrimitiveGroupProps = React.ComponentPropsWithoutRef<'div'> & {
-    children: React.ReactNode,
-    className?: string,
-}
+export type SelectPrimitiveGroupProps = React.ComponentPropsWithoutRef<'div'>;
src/core/primitives/Select/fragments/SelectPrimitiveSearch.tsx (1)

93-97: Type-safe onChange; remove ts-ignore.

Use a typed ChangeEvent param and drop @ts-ignore.

-                // @ts-ignore
-                onChange={(e) => setSearch(e.target.value)}
+                onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
src/components/ui/Select/tests/Select.test.tsx (3)

26-41: Always restore console spies via try/finally; also assert the hidden select’s name.

Prevents cross-test leakage and strengthens the accessibility/form assertion.

-    test('renders hidden native select for accessibility inside forms', () => {
-        const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
-        const { container } = render(
+    test('renders hidden native select for accessibility inside forms', () => {
+        const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+        try {
+        const { container } = render(
             <form>
                 <Select.Root name="test">
                     <Select.Trigger>Trigger</Select.Trigger>
                     <Select.Content>
                         <Select.Item value="one">One</Select.Item>
                     </Select.Content>
                 </Select.Root>
             </form>
-        );
-        const hiddenSelect = container.querySelector('select[aria-hidden="true"]');
-        expect(hiddenSelect).toBeInTheDocument();
-        errorSpy.mockRestore();
+        );
+        const hiddenSelect = container.querySelector('select[aria-hidden="true"]') as HTMLSelectElement | null;
+        expect(hiddenSelect).toBeInTheDocument();
+        expect(hiddenSelect).toHaveAttribute('name', 'test');
+        } finally {
+          errorSpy.mockRestore();
+        }
     });

43-55: Use try/finally when mocking console to avoid side effects on failure.

-    test('renders without warnings', () => {
-        const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
-        const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
-        render(
-            <Select.Root>
-                <Select.Trigger>Trigger</Select.Trigger>
-            </Select.Root>
-        );
-        expect(warnSpy).not.toHaveBeenCalled();
-        expect(errorSpy).not.toHaveBeenCalled();
-        warnSpy.mockRestore();
-        errorSpy.mockRestore();
-    });
+    test('renders without warnings', () => {
+        const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+        const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+        try {
+          render(
+              <Select.Root>
+                  <Select.Trigger>Trigger</Select.Trigger>
+              </Select.Root>
+          );
+          expect(warnSpy).not.toHaveBeenCalled();
+          expect(errorSpy).not.toHaveBeenCalled();
+        } finally {
+          warnSpy.mockRestore();
+          errorSpy.mockRestore();
+        }
+    });

57-64: Snapshot is brittle; prefer explicit assertions on roles/attributes.

Consider asserting Trigger role/aria rather than snapshot.

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

11-18: Minor: self-close empty element.

-            <SelectPrimitive.Search
-                className={`${rootClass}-search`}
-                ref={ref}
-                {...props}
-            >
-
-            </SelectPrimitive.Search>
+            <SelectPrimitive.Search className={mergedClassName} ref={ref} {...props} />
src/core/primitives/Select/fragments/SelectPrimitiveTrigger.tsx (1)

6-9: children is already in button props; remove redundancy.

-export type SelectPrimitiveTriggerProps = React.ComponentPropsWithoutRef<'button'> & {
-    children: React.ReactNode;
-    className?: string;
-};
+export type SelectPrimitiveTriggerProps = React.ComponentPropsWithoutRef<'button'> & {
+    className?: string;
+};
src/components/ui/Select/fragments/SelectItem.tsx (2)

10-29: Avoid className overwrite; wire up customRootClass.

props.className currently overrides ${rootClass}-item due to spread order, and customRootClass is unused. Merge class names and honor customRootClass.

-const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, SelectItemProps>(
-    ({ customRootClass, children, value, disabled, ...props }, ref) => {
+const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, SelectItemProps>(
+    ({ customRootClass, className, children, value, disabled, ...props }, ref) => {
         const { rootClass } = useContext(SelectRootContext);
+        const baseClass = customRootClass ?? rootClass;
 
         return (
             <SelectPrimitive.Item
-                className={`${rootClass}-item`}
+                className={`${baseClass}-item${className ? ` ${className}` : ''}`}
                 value={value}
                 disabled={disabled}
                 data-disabled={disabled ? '' : undefined}
                 role="option"
-                aria-disabled={disabled ? 'true' : undefined}
+                aria-disabled={disabled || undefined}
                 ref={ref}
                 {...props}
             >
-                <span className={`${rootClass}-text`}>{children}</span>
+                <span className={`${baseClass}-text`}>{children}</span>
             </SelectPrimitive.Item>
         );
     }
 );

15-26: Consider removing redundant ARIA/role from UI layer.

If SelectPrimitive.Item already sets role/aria, duplicating them here is unnecessary and risks divergence.

src/core/primitives/Select/fragments/SelectPrimitiveItem.tsx (1)

31-33: Type vs comment mismatch for itemId.

value is typed as required (string), so the “fallback to index” comment is misleading. Either make value optional or drop the fallback verbiage.

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

19-21: Minor: aria-disabled is redundant on a disabled button.

You can drop aria-disabled when disabled is present.

src/components/ui/Select/Select.tsx (2)

23-26: Gate the console.warn to dev and avoid unused ref.

Prevents production noise and satisfies linters.

-const Select = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((_, ref) => {
-    console.warn('Direct usage of Select is not supported. Please use Select.Root, Select.Content, etc. instead.');
+const Select = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((_, _ref) => {
+    if (process.env.NODE_ENV !== 'production') {
+        // Direct usage is not supported. Use Select.Root, Select.Content, etc.
+        console.warn('Direct usage of Select is not supported. Please use Select.Root, Select.Content, etc. instead.');
+    }
     return null;
 }) as SelectComponent;

29-36: Optional: attach statics via Object.assign for a single expression.

Keeps the value immutable and avoids post-assignment mutation.

// Alternative pattern:
const Select = Object.assign(
  React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((_, _ref) => {
    if (process.env.NODE_ENV !== 'production') console.warn('Direct usage of Select is not supported. Please use Select.Root, Select.Content, etc. instead.');
    return null;
  }),
  { Root: SelectRoot, Content: SelectContent, Item: SelectItem, Trigger: SelectTrigger, Portal: SelectPortal, Group: SelectGroup, Indicator: SelectIndicator, Search: SelectSearch }
) as SelectComponent;
src/core/primitives/Select/fragments/SelectPrimitiveRoot.tsx (2)

31-31: Avoid any in refs.

Use a concrete DOM type.

-    const selectedItemRef = React.useRef<any>(null);
+    const selectedItemRef = React.useRef<HTMLElement | null>(null);

105-115: Effect dependencies should not use ref.current.

selectedItemRef.current won’t trigger re-runs; recompute when selection/open state changes.

-    }, [selectedItemRef.current, shift]);
+    }, [shift, selectedIndex, isOpen]);
src/components/ui/Select/fragments/SelectRoot.tsx (4)

16-31: Stabilize context value to avoid redundant re-renders

Provider gets a new object each render; memoize to prevent unnecessary downstream updates.

-      return (
-        <SelectRootContext.Provider value={{ rootClass }}>
+      const ctxValue = React.useMemo(() => ({ rootClass }), [rootClass]);
+      return (
+        <SelectRootContext.Provider value={ctxValue}>

8-10: Export props type for consumers

This component is part of a UI library; exporting the prop type helps downstream typing and docs.

-type SelectRootProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> & {
+export type SelectRootProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> & {
   customRootClass?: string;
 };

20-22: Dev-only guard: avoid passing both value and defaultValue

Helps catch controlled/uncontrolled misuse without affecting production.

-    ({ customRootClass, className, children, defaultValue, value, onValueChange, shift, ...props }, ref) => {
+    ({ customRootClass, className, children, defaultValue, value, onValueChange, shift, ...props }, ref) => {
       const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
+      if (process.env.NODE_ENV !== 'production' && value !== undefined && defaultValue !== undefined) {
+          // eslint-disable-next-line no-console
+          console.warn('SelectRoot: avoid passing both value and defaultValue at the same time.');
+      }

6-6: Minor naming consistency

COMPONENT_NAME = "Select" while displayName = "SelectRoot". If CSS naming relies on "Select", keeping both is fine; otherwise consider aligning them.

Also applies to: 35-35

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 03295bd and 8cb6ac8.

⛔ Files ignored due to path filters (1)
  • src/components/ui/Select/tests/__snapshots__/Select.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (15)
  • src/components/ui/Select/Select.tsx (2 hunks)
  • src/components/ui/Select/fragments/SelectContent.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectGroup.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectIndicator.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectItem.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectRoot.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectSearch.tsx (1 hunks)
  • src/components/ui/Select/fragments/SelectTrigger.tsx (1 hunks)
  • src/components/ui/Select/tests/Select.test.tsx (1 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveContent.tsx (1 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveGroup.tsx (1 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveItem.tsx (4 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveRoot.tsx (4 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveSearch.tsx (2 hunks)
  • src/core/primitives/Select/fragments/SelectPrimitiveTrigger.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-12-12T08:34:33.079Z
Learnt from: decipher-cs
PR: rad-ui/ui#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/SelectTrigger.tsx
📚 Learning: 2024-12-12T08:22:59.375Z
Learnt from: decipher-cs
PR: rad-ui/ui#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/Select/fragments/SelectTrigger.tsx
🧬 Code graph analysis (10)
src/components/ui/Select/fragments/SelectSearch.tsx (1)
src/components/ui/Select/contexts/SelectRootContext.tsx (1)
  • SelectRootContext (7-9)
src/components/ui/Select/fragments/SelectRoot.tsx (1)
src/components/ui/Select/contexts/SelectRootContext.tsx (1)
  • SelectRootContext (7-9)
src/components/ui/Select/fragments/SelectContent.tsx (1)
src/components/ui/Select/contexts/SelectRootContext.tsx (1)
  • SelectRootContext (7-9)
src/core/primitives/Select/fragments/SelectPrimitiveTrigger.tsx (1)
src/core/primitives/Select/contexts/SelectPrimitiveContext.tsx (1)
  • SelectPrimitiveContext (32-32)
src/components/ui/Select/fragments/SelectIndicator.tsx (1)
src/components/ui/Select/contexts/SelectRootContext.tsx (1)
  • SelectRootContext (7-9)
src/components/ui/Select/fragments/SelectGroup.tsx (1)
src/components/ui/Select/contexts/SelectRootContext.tsx (1)
  • SelectRootContext (7-9)
src/components/ui/Select/fragments/SelectItem.tsx (1)
src/components/ui/Select/contexts/SelectRootContext.tsx (1)
  • SelectRootContext (7-9)
src/core/primitives/Select/fragments/SelectPrimitiveSearch.tsx (1)
src/core/primitives/Select/contexts/SelectPrimitiveContext.tsx (1)
  • SelectPrimitiveContext (32-32)
src/components/ui/Select/fragments/SelectTrigger.tsx (1)
src/components/ui/Select/contexts/SelectRootContext.tsx (1)
  • SelectRootContext (7-9)
src/core/primitives/Select/fragments/SelectPrimitiveContent.tsx (1)
src/core/primitives/Select/contexts/SelectPrimitiveContext.tsx (1)
  • SelectPrimitiveContext (32-32)
🪛 Biome (2.1.2)
src/core/primitives/Select/fragments/SelectPrimitiveSearch.tsx

[error] 26-26: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 92-92: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

src/core/primitives/Select/fragments/SelectPrimitiveContent.tsx

[error] 22-22: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

src/core/primitives/Select/fragments/SelectPrimitiveItem.tsx

[error] 26-26: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 42-42: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ 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). (1)
  • GitHub Check: build
🔇 Additional comments (15)
src/core/primitives/Select/fragments/SelectPrimitiveGroup.tsx (1)

8-14: LGTM on forwardRef conversion.

Ref forwarding and prop passthrough look correct.

src/core/primitives/Select/fragments/SelectPrimitiveSearch.tsx (1)

94-95: valuesRef.current assignments not found in root or search—unable to confirm it stores element IDs. Ensure aria-activedescendant uses an actual element id (e.g., elementsRef.current[activeIndex]?.id) rather than option values.

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

38-40: jest-dom matchers are configured
Import of @testing-library/jest-dom is present in src/setupTests.ts, so toBeInTheDocument is available.

src/core/primitives/Select/fragments/SelectPrimitiveTrigger.tsx (1)

20-21: No changes needed for useMergeRefs
Floater correctly re-exports useMergeRefs from @floating-ui/react, so the existing ref={Floater.useMergeRefs([...])} usage is valid.

src/core/primitives/Select/fragments/SelectPrimitiveItem.tsx (5)

31-39: Selected ref assignment effect runs after early return path.

This effect must remain unconditional (it is now) but was previously guarded by an early return above. Removing the early return resolves the hook-order issue.


8-13: Prop surface looks good.

ForwardRef typing to Primitive.div and explicit props are clear.


15-16: Name alignment and displayName are correct.

displayName addition aids debugging.


24-30: Active/selected computation is straightforward.

No issues spotted here.


50-64: Keyboard handling is solid.

Enter/Space behaviors align with listbox expectations.

src/core/primitives/Select/fragments/SelectPrimitiveContent.tsx (1)

16-35: Conditional rendering approach looks good.

Mounting only when open keeps the tree light while preserving focus management via Floater.

src/components/ui/Select/fragments/SelectTrigger.tsx (2)

12-23: ForwardRef conversion: solid.

Ref correctly forwards to the underlying Trigger.


19-21: Revisit data-placeholder semantics.

Setting data-placeholder based only on the prop (not selection state) can mis-style a non-empty value. Consider deriving this from context/selection instead.

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

12-21: API surface definition looks good.

Typed static subcomponents on the Select export are clear and consistent.

src/core/primitives/Select/fragments/SelectPrimitiveRoot.tsx (1)

42-43: Verify dynamic updates of isFormChild
Ensure useIsInsideForm(rootRef.current) tracks the transition from null to the mounted element and updates isFormChild accordingly. If it does not, either refactor the hook to accept the ref object (rootRef) or defer the call until after mount. Applies to lines 42–43 and 146–158.

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

12-13: Ref forwarding + typing look correct

Generic params and element ref inference are spot on.

Comment thread src/components/ui/Select/fragments/SelectContent.tsx
Comment thread src/components/ui/Select/fragments/SelectContent.tsx
Comment thread src/components/ui/Select/fragments/SelectGroup.tsx
Comment thread src/components/ui/Select/fragments/SelectIndicator.tsx Outdated
Comment thread src/components/ui/Select/fragments/SelectRoot.tsx
Comment thread src/components/ui/Select/fragments/SelectTrigger.tsx
Comment thread src/core/primitives/Select/fragments/SelectPrimitiveContent.tsx
Comment thread src/core/primitives/Select/fragments/SelectPrimitiveRoot.tsx Outdated
Comment thread src/core/primitives/Select/fragments/SelectPrimitiveSearch.tsx Outdated
Comment thread src/core/primitives/Select/fragments/SelectPrimitiveTrigger.tsx
@kotAPI
Copy link
Copy Markdown
Collaborator Author

kotAPI commented Sep 5, 2025

closing as stale

@kotAPI kotAPI closed this Sep 5, 2025
@kotAPI kotAPI deleted the kotapi/refactor-select-component-with-forwardref branch September 5, 2025 06:01
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