Skip to content

feat: migrate Select to Base UI#591

Open
rohanchkrabrty wants to merge 6 commits intomainfrom
migrate-select
Open

feat: migrate Select to Base UI#591
rohanchkrabrty wants to merge 6 commits intomainfrom
migrate-select

Conversation

@rohanchkrabrty
Copy link
Contributor

@rohanchkrabrty rohanchkrabrty commented Feb 18, 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

  • Refactor

    • Simplified Calendar dropdown rendering for improved performance
    • Enhanced Select component implementation and accessibility handling across multiple subcomponents
  • Tests

    • Improved test assertions for Calendar, Code Block, Filter Chip, and Select components
    • Enhanced test utilities and interaction patterns for better reliability and maintainability

@vercel
Copy link

vercel bot commented Feb 18, 2026

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

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Mar 9, 2026 9:37am

@rohanchkrabrty rohanchkrabrty changed the base branch from main to migrate-combobox February 18, 2026 04:12
@rohanchkrabrty rohanchkrabrty self-assigned this Feb 18, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This PR migrates the Select component from Radix UI and Ariakit primitives to Base UI, restructuring internal component logic and public interfaces to support both autocomplete and standard select modes. The Calendar component's dropdown rendering is simplified, and test patterns across multiple test files are updated to use more specific DOM selectors.

Changes

Cohort / File(s) Summary
Calendar Component
packages/raystack/components/calendar/calendar.tsx
Removed unused icon imports (ChevronDownIcon, ChevronUpIcon) and Flex dependency. Replaced Select dropdown content with direct map rendering of Select.Item elements, eliminating scroll wrapper scaffolding.
Test Updates — Query Selectors
packages/raystack/components/code-block/__tests__/code-block.test.tsx, packages/raystack/components/filter-chip/__tests__/filter-chip.test.tsx
Updated test queries to use more specific selectors (getByRole('combobox'), inputFieldWrapper descendants) instead of generic text or input queries; import order adjusted in code-block tests.
Select Component Tests
packages/raystack/components/select/__tests__/select.test.tsx
Refactored tests to follow a two-phase render+open pattern using openSelect helper. Introduced explicit flushMicrotasks for async handling. Replaced direct assertions on internal text with trigger-focused and aria-aware checks (getByRole, getAllByRole).
Select Core — Primitives Migration
packages/raystack/components/select/select-root.tsx, packages/raystack/components/select/select-content.tsx, packages/raystack/components/select/select-trigger.tsx
Migrated from Radix UI/Ariakit to Base UI primitives. Introduced dynamic primitive selection based on autocomplete context flag. Refactored internal state management (registerItem/unregisterItem, centralized contextValue). Updated public component signatures and props interfaces.
Select Core — Item & Value Rendering
packages/raystack/components/select/select-item.tsx, packages/raystack/components/select/select-value.tsx, packages/raystack/components/select/select-multiple-value.tsx
Replaced Radix-specific item/value rendering paths with generic HTMLElement-based forwardRef wrappers. Removed SelectPrimitive.Value/Item dependencies. Simplified SelectMultipleValue to a plain function component. Added public props (placeholder, className) to SelectValue.
Select Core — Miscellaneous Components
packages/raystack/components/select/select-misc.tsx
Replaced separate Radix implementations of SelectGroup, SelectLabel, SelectSeparator with unified context-aware implementations that dynamically select between Combobox and Select primitives based on autocomplete flag.
Select Exports & Styling
packages/raystack/components/select/select.tsx, packages/raystack/components/select/select.module.css
Updated exported Select API: removed ScrollUpButton/ScrollDownButton/Viewport; added ScrollUpArrow/ScrollDownArrow/List. Changed imports to @base-ui/react. CSS updated with new .positioner z-index, adjusted menu item visibility rules, and replaced :has(...) selectors to match new DOM structure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

Do not merge

Suggested reviewers

  • rsbh
  • paanSinghCoder

Poem

🐰 From Radix's realm to Base UI's home,
The Select now roams where primitives bloom,
With autocomplete flags and context flows,
This migration grows and grows and grows! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: migrate Select to Base UI' directly and accurately reflects the main change in the changeset—a comprehensive migration of the Select component from Radix UI to Base UI primitives.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch migrate-select

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.

Base automatically changed from migrate-combobox to main February 20, 2026 06:42
@ravisuhag
Copy link
Member

@rohanchkrabrty What is blocking this?

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

🧹 Nitpick comments (1)
packages/raystack/components/select/select.module.css (1)

256-259: Consider scoping the generic [data-disabled] selector.

The unscoped [data-disabled] selector could inadvertently affect any element within this module that has this attribute. If this is intentional for consistency, it's fine. Otherwise, consider scoping it to specific components like .trigger[data-disabled], .menuitem[data-disabled].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/select/select.module.css` around lines 256 -
259, The generic [data-disabled] selector is too broad and may style unrelated
elements; restrict it to the select component's specific elements by replacing
it with scoped selectors such as .trigger[data-disabled],
.menuitem[data-disabled] (and any other component-specific classes like
.option[data-disabled] if present) so only those elements get cursor:
not-allowed and pointer-events: none; update the rule in select.module.css
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/raystack/components/select/select-root.tsx`:
- Around line 204-224: The autocomplete branch is not forwarding the public
BaseSelectProps form props so name and required are missing; update the
ComboboxPrimitive.Root invocation to receive the same form props as the
non-autocomplete path by adding name and required (or include them in
commonProps) so ComboboxPrimitive.Root gets name and required like
SelectPrimitive.Root does; locate the autocomplete conditional using
autocomplete, commonProps, ComboboxPrimitive.Root and ensure name and required
from the component props (or from commonProps) are passed through.
- Around line 204-217: The autocomplete branch leaves the Combobox uncontrolled:
when using ComboboxPrimitive.Root you must pass both the current value and
change handler; update the ComboboxPrimitive.Root invocation in select-root.tsx
(the block using autocomplete) to include the inputValue prop bound to the
tracked state (inputValue={searchValue}) alongside the existing
onInputValueChange={handleSearchValueChange} so the combobox input stays
synchronized with the component's searchValue/defaultSearchValue state.

In `@packages/raystack/components/select/select-trigger.tsx`:
- Around line 49-50: The component currently spreads iconProps after setting
className={styles.triggerIcon}, which allows consumer iconProps.className to
overwrite the base styling; update both occurrences (the iconProps spread at the
first occurrence and the second occurrence around lines 72-76) to merge class
names instead of overwriting — e.g., compute className={clsx(styles.triggerIcon,
iconProps.className)} (or use the project's existing cx/clsx helper) and spread
the rest of iconProps without its className, keeping symbols iconProps and
styles.triggerIcon and the SelectTrigger/trigger rendering logic intact.

In `@packages/raystack/components/select/select-value.tsx`:
- Around line 26-34: The current useMemo in select-value.tsx can include
undefined entries because items[v] may not be populated yet; update the logic in
the useMemo that computes item (and the analogous logic around lines 65-66) to
filter out falsy/undefined results from itemValues (e.g., const itemValues =
value.map(v => items[v]).filter(Boolean)) before returning, and ensure you still
return undefined when the filtered array is empty, return the single item when
length === 1, or return the filtered array for multiple values so
SelectMultipleValue never receives undefined entries.
- Around line 18-20: SelectValue returns block-level divs inside a span
(forwardRef<HTMLSpanElement, SelectValueProps> component) which breaks HTML
semantics and SSR hydration; update the component to use a consistent block
wrapper (e.g., change the outer element from span to div and adjust the
forwarded ref type accordingly) or ensure inner elements are inline (replace
inner divs with spans) so no div is nested inside a span; locate the SelectValue
function that destructures ({ children, placeholder, className, ...props }) and
the useSelectContext() usage and make the wrapper change there and update any
ref typing and className/props passthrough to match the new element.

In `@packages/raystack/components/select/select.module.css`:
- Around line 40-42: The dropdown lacks a visual cue for selected items in
single-select mode because the CSS rule in select.module.css is commented out
and Item rendering in select-item.tsx never sets the data-selected attribute;
fix by uncommenting or restoring the CSS rule that targets
.menuitem[data-selected] and update the ItemPrimitive render in select-item.tsx
to set data-selected={state.selected || undefined} (or implement an alternate
visual indicator) so selected items show the intended background; ensure this
change only applies when data-multiselectable != "true" to preserve the existing
multi-select Checkbox behavior.

---

Nitpick comments:
In `@packages/raystack/components/select/select.module.css`:
- Around line 256-259: The generic [data-disabled] selector is too broad and may
style unrelated elements; restrict it to the select component's specific
elements by replacing it with scoped selectors such as .trigger[data-disabled],
.menuitem[data-disabled] (and any other component-specific classes like
.option[data-disabled] if present) so only those elements get cursor:
not-allowed and pointer-events: none; update the rule in select.module.css
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35fe040c-9236-4051-b414-3d9b18adaeaf

📥 Commits

Reviewing files that changed from the base of the PR and between 707f483 and 34390db.

📒 Files selected for processing (13)
  • packages/raystack/components/calendar/calendar.tsx
  • packages/raystack/components/code-block/__tests__/code-block.test.tsx
  • packages/raystack/components/filter-chip/__tests__/filter-chip.test.tsx
  • packages/raystack/components/select/__tests__/select.test.tsx
  • packages/raystack/components/select/select-content.tsx
  • packages/raystack/components/select/select-item.tsx
  • packages/raystack/components/select/select-misc.tsx
  • packages/raystack/components/select/select-multiple-value.tsx
  • packages/raystack/components/select/select-root.tsx
  • packages/raystack/components/select/select-trigger.tsx
  • packages/raystack/components/select/select-value.tsx
  • packages/raystack/components/select/select.module.css
  • packages/raystack/components/select/select.tsx

Comment on lines +204 to +224
if (autocomplete) {
return (
<SelectContext.Provider value={contextValue}>
<ComboboxPrimitive.Root
{...commonProps}
onInputValueChange={handleSearchValueChange}
filter={itemsProp ? undefined : null}
items={itemsProp}
loopFocus={false}
// @ts-ignore BaseUI types are not up to date
autoHighlight='always'
>
{children}
</ComboboxPrimitive.Root>
</SelectContext.Provider>
);
}

return (
<SelectContext.Provider
value={{
value: computedValue,
registerItem,
unregisterItem,
autocomplete,
autocompleteMode,
searchValue,
multiple,
items,
updateSelectionInProgress,
setValue
}}
>
<SelectPrimitive.Root
autoComplete={htmlAutoComplete}
value={radixValue}
onValueChange={setValue}
open={open}
onOpenChange={handleOpenChange}
{...rest}
>
{autocomplete ? element : children}
<SelectContext.Provider value={contextValue}>
<SelectPrimitive.Root {...commonProps} required={required} name={name}>
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

🧩 Analysis chain

🌐 Web query:

In @base-ui/react, does Combobox.Rootsupport form participation vianameandrequired? If not, what is the recommended pattern to submit and validate combobox values in forms?

💡 Result:

Yes. Combobox.Root supports native form participation via both:

  • name: “Identifies the field when a form is submitted.” [1]
  • required: “Whether the user must choose a value before submitting a form.” [1]

It participates using a hidden input (you can access it with inputRef). [1]

Recommended pattern for submitting + validating in forms

Use Base UI’s Form + Field.Root to define the field name and show validation UI, and put the combobox inside it:

  • Put the field name on <Field.Root name="…"> so the wrapped control is included on submit. [2]
  • Put required on the combobox (or other constraint props where supported), and render <Field.Error /> for messages. [1][2]
  • If your combobox values are objects, provide itemToStringValue so the hidden input can submit a string value. [1]
  • For correct native validation bubble positioning, ensure the field has a name and (optionally) wrap the control area in a relatively positioned container (Base UI notes this for hidden-input-based controls). [2]

Minimal sketch:

<Form>
  <Field.Root name="country">
    <Field.Label>Country</Field.Label>
    <div className="relative">
      <Combobox.Root required /* items=... */ />
    </div>
    <Field.Error />
  </Field.Root>
</Form>

If you’re using a form library (e.g. React Hook Form): keep the combobox controlled (value / onValueChange) and drive Field.Root’s error/invalid state from the library; Base UI’s forms guide shows this “Controller + Field.Root + Combobox” pattern. [2]

Sources: Combobox API reference [1], Base UI Forms handbook [2].


🏁 Script executed:

# Check the full select-root.tsx file to understand the context
wc -l packages/raystack/components/select/select-root.tsx

Repository: raystack/apsara

Length of output: 114


🏁 Script executed:

# Read the complete file to understand the structure and form handling pattern
cat -n packages/raystack/components/select/select-root.tsx

Repository: raystack/apsara

Length of output: 7125


🏁 Script executed:

# Search for SelectRoot usage to understand if Form/Field.Root wrapping is expected at call site
rg "SelectRoot|<Select\.Root" --type tsx --type ts -B 2 -A 2 | head -100

Repository: raystack/apsara

Length of output: 86


🏁 Script executed:

# Search for SelectRoot usage without type flag
rg "SelectRoot|<Select\.Root" -B 2 -A 2 | head -100

Repository: raystack/apsara

Length of output: 3167


Autocomplete mode drops name and required props needed for form participation.

These props are part of the public BaseSelectProps API and are forwarded to SelectPrimitive.Root in the non-autocomplete path (line 224), but they're not included in commonProps and thus missing from ComboboxPrimitive.Root in the autocomplete path. Since ComboboxPrimitive.Root supports both props for native form submission and validation, this inconsistency causes autocomplete selects to behave differently in forms.

Suggested fix
       <ComboboxPrimitive.Root
         {...commonProps}
+        name={name}
+        required={required}
         onInputValueChange={handleSearchValueChange}
         filter={itemsProp ? undefined : null}
         items={itemsProp}
📝 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
if (autocomplete) {
return (
<SelectContext.Provider value={contextValue}>
<ComboboxPrimitive.Root
{...commonProps}
onInputValueChange={handleSearchValueChange}
filter={itemsProp ? undefined : null}
items={itemsProp}
loopFocus={false}
// @ts-ignore BaseUI types are not up to date
autoHighlight='always'
>
{children}
</ComboboxPrimitive.Root>
</SelectContext.Provider>
);
}
return (
<SelectContext.Provider
value={{
value: computedValue,
registerItem,
unregisterItem,
autocomplete,
autocompleteMode,
searchValue,
multiple,
items,
updateSelectionInProgress,
setValue
}}
>
<SelectPrimitive.Root
autoComplete={htmlAutoComplete}
value={radixValue}
onValueChange={setValue}
open={open}
onOpenChange={handleOpenChange}
{...rest}
>
{autocomplete ? element : children}
<SelectContext.Provider value={contextValue}>
<SelectPrimitive.Root {...commonProps} required={required} name={name}>
if (autocomplete) {
return (
<SelectContext.Provider value={contextValue}>
<ComboboxPrimitive.Root
{...commonProps}
name={name}
required={required}
onInputValueChange={handleSearchValueChange}
filter={itemsProp ? undefined : null}
items={itemsProp}
loopFocus={false}
// `@ts-ignore` BaseUI types are not up to date
autoHighlight='always'
>
{children}
</ComboboxPrimitive.Root>
</SelectContext.Provider>
);
}
return (
<SelectContext.Provider value={contextValue}>
<SelectPrimitive.Root {...commonProps} required={required} name={name}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/select/select-root.tsx` around lines 204 - 224,
The autocomplete branch is not forwarding the public BaseSelectProps form props
so name and required are missing; update the ComboboxPrimitive.Root invocation
to receive the same form props as the non-autocomplete path by adding name and
required (or include them in commonProps) so ComboboxPrimitive.Root gets name
and required like SelectPrimitive.Root does; locate the autocomplete conditional
using autocomplete, commonProps, ComboboxPrimitive.Root and ensure name and
required from the component props (or from commonProps) are passed through.

Comment on lines +204 to +217
if (autocomplete) {
return (
<SelectContext.Provider value={contextValue}>
<ComboboxPrimitive.Root
{...commonProps}
onInputValueChange={handleSearchValueChange}
filter={itemsProp ? undefined : null}
items={itemsProp}
loopFocus={false}
// @ts-ignore BaseUI types are not up to date
autoHighlight='always'
>
{children}
</ComboboxPrimitive.Root>
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

🧩 Analysis chain

🌐 Web query:

For @base-ui/react Combobox.Root, what is the correct prop for controlling the input text value together with onInputValueChange?

💡 Result:

Use the inputValue prop (controlled), together with onInputValueChange. [1]

Example:

<Combobox.Root inputValue={inputValue} onInputValueChange={setInputValue} />

[1] (base-ui.com)

Citations:


🏁 Script executed:

cat -n packages/raystack/components/select/select-root.tsx | sed -n '95,220p'

Repository: raystack/apsara

Length of output: 3999


Autocomplete search state is not bound to the combobox input.

Lines 95-119 and 137-143 track searchValue/defaultSearchValue, but the autocomplete branch (204-217) only passes onInputValueChange to ComboboxPrimitive.Root without the corresponding inputValue prop. This leaves the combobox input uncontrolled, allowing the rendered query and internal search state to diverge.

The @base-ui/react Combobox requires both inputValue and onInputValueChange together for proper controlled behavior. Add inputValue={searchValue} to restore synchronization.

Suggested fix
       <ComboboxPrimitive.Root
         {...commonProps}
+        inputValue={searchValue}
         onInputValueChange={handleSearchValueChange}
         filter={itemsProp ? undefined : null}
         items={itemsProp}
         loopFocus={false}
📝 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
if (autocomplete) {
return (
<SelectContext.Provider value={contextValue}>
<ComboboxPrimitive.Root
{...commonProps}
onInputValueChange={handleSearchValueChange}
filter={itemsProp ? undefined : null}
items={itemsProp}
loopFocus={false}
// @ts-ignore BaseUI types are not up to date
autoHighlight='always'
>
{children}
</ComboboxPrimitive.Root>
if (autocomplete) {
return (
<SelectContext.Provider value={contextValue}>
<ComboboxPrimitive.Root
{...commonProps}
inputValue={searchValue}
onInputValueChange={handleSearchValueChange}
filter={itemsProp ? undefined : null}
items={itemsProp}
loopFocus={false}
// `@ts-ignore` BaseUI types are not up to date
autoHighlight='always'
>
{children}
</ComboboxPrimitive.Root>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/select/select-root.tsx` around lines 204 - 217,
The autocomplete branch leaves the Combobox uncontrolled: when using
ComboboxPrimitive.Root you must pass both the current value and change handler;
update the ComboboxPrimitive.Root invocation in select-root.tsx (the block using
autocomplete) to include the inputValue prop bound to the tracked state
(inputValue={searchValue}) alongside the existing
onInputValueChange={handleSearchValueChange} so the combobox input stays
synchronized with the component's searchValue/defaultSearchValue state.

Comment on lines 49 to 50
iconProps = {},
asChild,
'aria-label': ariaLabel,
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

Merge iconProps.className instead of overwriting the base icon styles.

iconProps is spread after className={styles.triggerIcon}, so any consumer-supplied class replaces the built-in trigger icon styling. packages/raystack/components/calendar/calendar.tsx already does this for the dropdown trigger, so the base icon positioning/animation styles are dropped there.

🎨 Proposed fix
   (
     {
@@
-      iconProps = {},
+      iconProps = {},
       'aria-label': ariaLabel,
       ...props
     },
     ref
   ) => {
     const { multiple, autocomplete } = useSelectContext();
+    const { className: iconClassName, ...restIconProps } = iconProps;
@@
         <Flex className={styles.triggerContent} align='center' gap={2}>
           {children}
         </Flex>
         <ChevronDownIcon
-          className={styles.triggerIcon}
+          className={cva(styles.triggerIcon)({ className: iconClassName })}
           aria-hidden='true'
-          {...iconProps}
+          {...restIconProps}
         />
       </TriggerPrimitive>

Also applies to: 72-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/select/select-trigger.tsx` around lines 49 - 50,
The component currently spreads iconProps after setting
className={styles.triggerIcon}, which allows consumer iconProps.className to
overwrite the base styling; update both occurrences (the iconProps spread at the
first occurrence and the second occurrence around lines 72-76) to merge class
names instead of overwriting — e.g., compute className={clsx(styles.triggerIcon,
iconProps.className)} (or use the project's existing cx/clsx helper) and spread
the rest of iconProps without its className, keeping symbols iconProps and
styles.triggerIcon and the SelectTrigger/trigger rendering logic intact.

Comment on lines +18 to +20
export const SelectValue = forwardRef<HTMLSpanElement, SelectValueProps>(
({ children, placeholder, className, ...props }, ref) => {
const { value, items, multiple } = useSelectContext();
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

🧩 Analysis chain

🏁 Script executed:

cat -n packages/raystack/components/select/select-value.tsx

Repository: raystack/apsara

Length of output: 2804


Use a block wrapper consistently in SelectValue.

The component returns div elements nested inside span elements, which violates HTML content models and can cause DOM normalization and hydration issues in SSR environments. This occurs at lines 70-77 (where a div directly wraps inside span), and potentially at lines 51-53 and 59-61 if the children prop contains block-level elements.

♻️ Proposed fix
-export const SelectValue = forwardRef<HTMLSpanElement, SelectValueProps>(
+export const SelectValue = forwardRef<HTMLDivElement, SelectValueProps>(
   ({ children, placeholder, className, ...props }, ref) => {
@@
-        <span
+        <div
           ref={ref}
           data-placeholder=''
           className={cx(styles.placeholder, className)}
           {...props}
         >
           {placeholder}
-        </span>
+        </div>
@@
-        <span ref={ref} className={className} {...props}>
+        <div ref={ref} className={className} {...props}>
           {children(item)}
-        </span>
+        </div>
@@
-        <span ref={ref} className={className} {...props}>
+        <div ref={ref} className={className} {...props}>
           {children}
-        </span>
+        </div>
@@
-      <span ref={ref} className={className} {...props}>
+      <div ref={ref} className={className} {...props}>
         <div className={cx(styles.valueContent)}>
           {typeof item?.children === 'string' && item?.leadingIcon && (
             <div className={styles.itemIcon}>{item.leadingIcon}</div>
           )}
           {item?.children ?? value}
         </div>
-      </span>
+      </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/select/select-value.tsx` around lines 18 - 20,
SelectValue returns block-level divs inside a span (forwardRef<HTMLSpanElement,
SelectValueProps> component) which breaks HTML semantics and SSR hydration;
update the component to use a consistent block wrapper (e.g., change the outer
element from span to div and adjust the forwarded ref type accordingly) or
ensure inner elements are inline (replace inner divs with spans) so no div is
nested inside a span; locate the SelectValue function that destructures ({
children, placeholder, className, ...props }) and the useSelectContext() usage
and make the wrapper change there and update any ref typing and className/props
passthrough to match the new element.

Comment on lines +26 to +34
const item = useMemo(() => {
if (!value) return undefined;
if (multiple && Array.isArray(value)) {
const itemValues = value.map(v => items[v]);
if (itemValues.length === 1) return itemValues[0];
return itemValues;
}
return items[value as string];
}, [value, items, multiple]);
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 | 🔴 Critical

Guard unresolved multi-select items before rendering chips.

items[v] is populated later by SelectItem's layout effect, so the first render can still produce undefined entries here. In multiple mode that array is passed straight to SelectMultipleValue, which dereferences every item, so a controlled select with preset values can throw before the registry is ready.

🐛 Proposed fix
     const item = useMemo(() => {
       if (!value) return undefined;
       if (multiple && Array.isArray(value)) {
         const itemValues = value.map(v => items[v]);
-        if (itemValues.length === 1) return itemValues[0];
-        return itemValues;
+        if (!itemValues.every(Boolean)) return undefined;
+        return itemValues.length === 1 ? itemValues[0] : itemValues;
       }
       return items[value as string];
     }, [value, items, multiple]);

Also applies to: 65-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/select/select-value.tsx` around lines 26 - 34,
The current useMemo in select-value.tsx can include undefined entries because
items[v] may not be populated yet; update the logic in the useMemo that computes
item (and the analogous logic around lines 65-66) to filter out falsy/undefined
results from itemValues (e.g., const itemValues = value.map(v =>
items[v]).filter(Boolean)) before returning, and ensure you still return
undefined when the filtered array is empty, return the single item when length
=== 1, or return the filtered array for multiple values so SelectMultipleValue
never receives undefined entries.

Comment on lines +40 to +42
/* .content:not([data-multiselectable="true"]) .menuitem[data-selected] {
background: var(--rs-color-background-neutral-secondary);
}
} */
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if data-selected attribute is manually added in select-item.tsx
rg -n 'data-selected' packages/raystack/components/select/

Repository: raystack/apsara

Length of output: 189


🏁 Script executed:

cat -n packages/raystack/components/select/select-item.tsx | head -100

Repository: raystack/apsara

Length of output: 3152


🏁 Script executed:

cat -n packages/raystack/components/select/select.module.css

Repository: raystack/apsara

Length of output: 9117


🏁 Script executed:

rg -n 'menuitem' packages/raystack/components/select/select.module.css

Repository: raystack/apsara

Length of output: 411


Add visual distinction for selected items in single-select mode.

The [data-selected] styling rule is currently commented out (lines 40-42), and the data-selected attribute is never added to menu items in select-item.tsx. This means selected items in single-select mode have no visual distinction within the dropdown.

In multi-select mode, a Checkbox component provides visual feedback (line 88), but single-select mode relies only on the trigger displaying the value. To fix this, either:

  1. Uncomment the rule and add data-selected={state.selected || undefined} to the ItemPrimitive render (line 79)
  2. Remove the commented rule and document that visual selection indication is intentionally omitted in single-select mode
  3. Implement an alternative visual indicator
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/select/select.module.css` around lines 40 - 42,
The dropdown lacks a visual cue for selected items in single-select mode because
the CSS rule in select.module.css is commented out and Item rendering in
select-item.tsx never sets the data-selected attribute; fix by uncommenting or
restoring the CSS rule that targets .menuitem[data-selected] and update the
ItemPrimitive render in select-item.tsx to set data-selected={state.selected ||
undefined} (or implement an alternate visual indicator) so selected items show
the intended background; ensure this change only applies when
data-multiselectable != "true" to preserve the existing multi-select Checkbox
behavior.

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