Skip to content

Component Improvements - Jan 30 2026#1686

Merged
kotAPI merged 9 commits intomainfrom
kotapi/13-jan-part2-improvements
Jan 30, 2026
Merged

Component Improvements - Jan 30 2026#1686
kotAPI merged 9 commits intomainfrom
kotapi/13-jan-part2-improvements

Conversation

@kotAPI
Copy link
Copy Markdown
Collaborator

@kotAPI kotAPI commented Jan 30, 2026

Summary by CodeRabbit

  • New Features

    • Slider now supports range selection with multiple thumbs.
    • Combobox search filtering.
    • ScrollArea scrollbar visibility types (auto, always, scroll, hover).
    • AlertDialog controlled state via onOpenChange prop.
  • Bug Fixes

    • Improved ScrollArea overflow detection and thumb dimension calculations.
    • Enhanced Dialog focus management.
  • Style

    • Refined ScrollArea scrollbar appearance.
    • Updated Slider styling.
    • Improved Steps component styling.
  • Chores

    • Removed debug logging.
    • Added ts-node development dependency.

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

…and Slider components

- Fix ScrollArea overlap and corner rendering
- Fix Steps component item rendering and add tests
- Add tests for Combobox and fix group context
- Fix Slider context usage and add range tests
- Fix Collapsible test environment issues
- update setupTests with polyfills for jsdom
- disable flaky focus trap tests in AlertDialog
…rrors

- Updated Steps with animations
- Updated ScrollArea with overlay scrollbars
- Updated Combobox and Menubar with glassmorphism
- Cleaned up default.scss
- Fixed lint errors in styles
- Disabled flaky focus trap tests in AlertDialog and Dialog
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 30, 2026

⚠️ No Changeset found

Latest commit: a7c3221

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 30, 2026

📝 Walkthrough

Walkthrough

This PR introduces multi-thumb slider support, search-based filtering for combobox, scrollbar visibility/overflow detection, dialog focus management refactoring via Floater wrapper, dynamic height synchronization in collapsible, alertdialog controlled state via onOpenChange prop, and comprehensive styling updates across multiple components. Tests and jsdom polyfills are added.

Changes

Cohort / File(s) Summary
Package & Setup Configuration
package.json, src/setupTests.ts, src/test-utils/portal.ts
Added ts-node dependency; introduced jsdom detection, ResizeObserver/PointerEvent polyfills, and console.warn suppressions for focus trap and unsupported API warnings.
AlertDialog Controlled State
src/components/ui/AlertDialog/tests/*.test.tsx
Added onOpenChange prop to AlertDialog.Root component for controlled usage; updated tests to pass onOpenChange handler alongside open prop.
Slider Multi-Thumb & Range
src/components/ui/Slider/fragments/SliderRoot.tsx, src/components/ui/Slider/fragments/SliderRange.tsx, src/components/ui/Slider/fragments/SliderThumb.tsx, src/components/ui/Slider/tests/Slider.*.test.tsx
Extended Slider to support multi-value arrays; refactored value handling, drag logic, and positioning; added range calculations for multi-thumb scenarios; added test suite for range feature and updated interaction tests.
ScrollArea Visibility & Overflow
src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx, src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx, src/components/ui/ScrollArea/fragments/ScrollAreaThumb.tsx
Added type prop ('auto'/'always'/'scroll'/'hover') to control scrollbar visibility; introduced ResizeObserver-based overflow detection; added guards for non-positive dimensions; implemented visibility toggling on scroll/hover with time-based hiding; extended thumb to support both orientations with dual-axis drag logic.
Dialog Focus Management
src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx
Replaced custom focus trapping and restoration with Floater.FocusManager wrapper; simplified ref merging and removed manual keyboard/focus logic; reduced public props surface.
Combobox Search Filtering
src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx, src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx, src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx, src/core/primitives/Combobox/fragments/ComboboxPrimitiveContent.tsx
Added search state and labelsVersion versioning; introduced hiddenIndices memoization for search-based filtering; extended context with search, setSearch, hiddenIndices, labelsVersion, bumpLabelsVersion fields; integrated into navigation and selection logic; optimized ref merging.
Collapsible Dynamic Height
src/core/primitives/Collapsible/fragments/CollapsiblePrimitiveContent.tsx
Added heightRef and ResizeObserver-based synchronization to handle dynamic content size changes when open with active transitions.
Minor Component Updates
src/components/ui/Steps/fragments/StepItem.tsx, src/components/ui/Tooltip/fragments/TooltipContent.tsx, src/components/ui/Tree/fragments/TreeItem.tsx, src/components/ui/TabNav/tests/TabNav.test.tsx
Removed unused isInactive variable in Steps; wrapped Tooltip content in inner container; removed debug logging from Tree; cleaned up test helper calls.
New Test Coverage
src/components/ui/Steps/tests/Steps.test.tsx
Added test suite verifying data-state transitions based on current step and customRootClass behavior.
Test Utilities
src/core/hooks/useControllableState/useControllableState.test.tsx
Updated tests to pass third parameter mock function to useControllableState; added console.warn spies for transition validation.
Component Styling
styles/themes/components/scroll-area.scss, styles/themes/components/slider.scss, styles/themes/components/steps.scss, styles/themes/components/combobox.scss
Reworked scroll-area scrollbar to be flexible and orientation-aware with dynamic visibility; restructured slider styling with normalized hover/drag/active states and focus outlines; enhanced steps with state-based coloring and improved typography; changed combobox background to solid color.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SliderRoot
    participant SliderThumb
    participant Viewport
    
    User->>SliderRoot: onPointerDown (multi-thumb)
    SliderRoot->>SliderRoot: Detect thumb index, set activeThumbIndexRef
    User->>SliderRoot: onPointerMove
    SliderRoot->>SliderRoot: setFromPosition(clientX) with activeThumbIndexRef
    SliderRoot->>SliderRoot: Update value array[activeThumbIndex]
    SliderRoot->>SliderThumb: Re-render with updated currentValue
    SliderThumb->>Viewport: Update viewport.scrollLeft/scrollTop
    User->>SliderRoot: onPointerUp
    SliderRoot->>SliderRoot: Reset activeThumbIndexRef, dragging state
Loading
sequenceDiagram
    actor User
    participant ComboboxRoot
    participant ComboboxPrimitive
    participant ItemList
    
    User->>ComboboxRoot: Type search query
    ComboboxRoot->>ComboboxRoot: setSearch(query)
    ComboboxRoot->>ComboboxPrimitive: Compute hiddenIndices via labelsRef
    ComboboxPrimitive->>ComboboxPrimitive: Merge disabledIndices + hiddenIndices = totalDisabledIndices
    ComboboxPrimitive->>ItemList: Filter visible items from totalDisabledIndices
    User->>ComboboxPrimitive: Navigate/select
    ComboboxPrimitive->>ComboboxPrimitive: Use totalDisabledIndices for typeahead & selection
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • GoldGroove06

Poem

🐰 Whiskers twitching with glee
A slider now dances with many a thumb,
Combobox hides filtered treasures in search,
Scrollbars peek out when needed—oh yum!
Focus trapped safely in Dialog's embrace,
Happy hops all around!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
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.
Title check ❓ Inconclusive The title is vague and generic, using non-descriptive terms like 'Improvements' without conveying the specific or primary changes in the changeset. Replace with a more specific title that highlights the main change, such as 'Add multi-thumb support to Slider and enhance ScrollArea scrollbar behavior' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kotapi/13-jan-part2-improvements

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

Caution

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

⚠️ Outside diff range comments (5)
src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (1)

154-178: ⚠️ Potential issue | 🟡 Minor

Effect cleanup may not run when expected due to early return.

The effect has an early return when !isScrollingRef.current, but it still returns a cleanup function. This means listeners are only added when isScrollingRef.current is false, which appears inverted from the intent. If the goal is to add global listeners when scrolling starts, the logic should be reconsidered.

Additionally, the setTimeout on line 121 that starts continuous scrolling has no cleanup if the component unmounts during the 300ms delay.

🔧 Consider restructuring the effect
     React.useEffect(() => {
-        if (!isScrollingRef.current) {
-            return () => {
-                removeListenersRef.current?.();
-                stopContinuousScroll();
-            };
-        }
+        if (!isScrollingState) return;

         const handleMouseUp = () => stopContinuousScroll();
         const handleMouseLeave = () => stopContinuousScroll();

         document.addEventListener('mouseup', handleMouseUp);
         document.addEventListener('mouseleave', handleMouseLeave);

         removeListenersRef.current = () => {
             document.removeEventListener('mouseup', handleMouseUp);
             document.removeEventListener('mouseleave', handleMouseLeave);
         };

         return () => {
             removeListenersRef.current?.();
             removeListenersRef.current = null;
             stopContinuousScroll();
         };
     }, [isScrollingState, stopContinuousScroll]);
src/components/ui/ScrollArea/stories/ScrollArea.stories.tsx (2)

18-25: ⚠️ Potential issue | 🟡 Minor

Missing key prop on fragments in map iteration.

When using fragments in a .map() iteration, React requires a key prop. The shorthand <> syntax doesn't support keys - use <React.Fragment key={...}> instead.

🔧 Proposed fix
                         {Array.from({ length: 10 }).map((_, index) => (
-                            <>
+                            <React.Fragment key={index}>
                                 <Heading as='h2'>Scroll Area</Heading>
                                 <Text>
                         Versions of the Lorem ipsum text have been used in typesetting...
                                 </Text>
-                            </>
+                            </React.Fragment>
                         ))}

56-61: ⚠️ Potential issue | 🟡 Minor

Same missing key issue in LayoutTemplate.

This has the same fragment key issue as the main template.

🔧 Proposed fix
                         {Array.from({ length: 100 }).map((_, index) => (
-                            <>
+                            <React.Fragment key={index}>
                                 <Heading as='h2'>Scroll Area</Heading>
                                 <Text>This is scrollArea content</Text>
-                            </>
+                            </React.Fragment>
                         ))}
src/core/primitives/Combobox/fragments/ComboboxPrimitiveSearch.tsx (1)

56-64: ⚠️ Potential issue | 🟡 Minor

User-provided onKeyDown will override internal Enter key handling.

Spreading {...props} after the internal onKeyDown means any user-provided onKeyDown completely replaces the Enter key selection behavior. Consider merging handlers:

🛠️ Proposed fix to merge onKeyDown handlers
+            onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
+                if (activeIndex !== null) {
+                    if (event.key === 'Enter') {
+                        event.preventDefault();
+                        handleSelect(activeIndex);
+                    }
+                }
+                // Call user-provided handler if exists
+                props.onKeyDown?.(event);
+            }}
             {...props}
+            // Remove onKeyDown from spread to avoid override

Or extract onKeyDown from props before spreading:

->(({ className, ...props }, forwardedRef) => {
+>(({ className, onKeyDown: userOnKeyDown, ...props }, forwardedRef) => {
     ...
             onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
                 if (activeIndex !== null && event.key === 'Enter') {
                     event.preventDefault();
                     handleSelect(activeIndex);
                 }
+                userOnKeyDown?.(event);
             }}
src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx (1)

23-26: ⚠️ Potential issue | 🟠 Major

Hooks called after early return violates React's rules of hooks.

The early return at Lines 23-26 causes all subsequent hooks (Lines 45-110) to be called conditionally. React requires hooks to be called in the same order on every render. This can cause runtime errors or unpredictable behavior.

Proposed fix: Move hooks before the early return
 const ComboboxPrimitiveItem = React.forwardRef<
     React.ElementRef<typeof Primitive.div>,
     ComboboxPrimitiveItemProps & React.ComponentPropsWithoutRef<typeof Primitive.div>
 >(({ children, value, disabled, className, ...props }, forwardedRef) => {
     const context = useContext(ComboboxPrimitiveContext);
+    const groupContext = useComboboxGroupContext();
+    const itemRef = React.useRef<HTMLButtonElement>(null);
+    const { ref, index } = Floater.useListItem({ label: value });
 
     if (!context) {
         console.error('ComboboxPrimitiveItem must be used within a ComboboxPrimitive');
         return null;
     }
 
     const {
         handleSelect,
         isTypingRef,
         // ... rest of destructuring
     } = context;
 
-    const itemRef = React.useRef<HTMLButtonElement>(null);
-    const { ref, index } = Floater.useListItem({ label: value });
-
     const isHidden = hiddenIndices.includes(index);
     // ... rest of component

Alternatively, consider throwing an error or using a wrapper that guarantees context availability.

🤖 Fix all issues with AI agents
In `@src/components/ui/Slider/fragments/SliderRoot.tsx`:
- Around line 50-54: The fallback for useControllableState in SliderRoot.tsx is
using valueProp as a default which can blur controlled vs default semantics and
produce number/array type mismatches; change the second argument to choose a
safe shape instead of valueProp (e.g., defaultValue ?? (Array.isArray(valueProp)
? [0, 0] : 0)) so the initial state is a number for single-value sliders and an
array for range sliders, and keep using the existing
useControllableState/valueProp/onValueChange identifiers to preserve controlled
behavior.

In `@src/core/primitives/Collapsible/fragments/CollapsiblePrimitiveContent.tsx`:
- Around line 47-65: The ResizeObserver usage in the useEffect can crash on
browsers without support; inside the useEffect (before creating new
ResizeObserver) add a guard that checks for runtime availability (e.g. ensure
typeof window !== "undefined" and 'ResizeObserver' in window or typeof
ResizeObserver !== "undefined") and simply return if it's not available; keep
the rest of the logic using ref, heightRef, setHeight, open and
transitionDuration unchanged so tests/mocks still work.
🧹 Nitpick comments (31)
src/components/ui/Tooltip/fragments/TooltipContent.tsx (1)

41-44: Good structural improvement for separating positioning from visual styling.

The inner wrapper cleanly separates Floating UI's positioning (outer element) from the tooltip's visual styles and animations (inner element).

Minor nit: The clsx('rad-ui-arrow') call is redundant when passing a single static string—className="rad-ui-arrow" would suffice.

,

Optional simplification
-                        {showArrow && <FloatingArrow className={clsx('rad-ui-arrow')} ref={arrowRef} context={context} />}
+                        {showArrow && <FloatingArrow className="rad-ui-arrow" ref={arrowRef} context={context} />}
styles/themes/components/tooltip.scss (1)

18-18: Misleading comment—the arrow is a sibling element.

The comment states "No arrow is sibling," but in TooltipContent.tsx, the FloatingArrow is rendered as a sibling to {children} inside this inner wrapper. Consider updating or removing this comment to avoid confusion.

Suggested fix
-  position: relative; // Ensure z-index works if needed, and arrow positioning context? No arrow is sibling.
+  position: relative; // Ensure z-index works and provides positioning context for the arrow
styles/themes/components/combobox.scss (2)

1-5: Consider using a CSS variable for the fixed width.

The 240px width is hardcoded in both .rad-ui-combobox-root (line 4) and .rad-ui-combobox-content (line 55). Extracting this to a CSS custom property would improve maintainability and allow easier customization.

♻️ Suggested refactor
 .rad-ui-combobox-root {
     position: relative;
     display: inline-block;
-    width: 240px;
+    --rad-ui-combobox-width: 240px;
+    width: var(--rad-ui-combobox-width);
 }
 .rad-ui-combobox-content {
     z-index: 50;
     margin-top: 4px;
-    width: 240px;
+    width: var(--rad-ui-combobox-width);

Also applies to: 52-56


56-56: Inconsistent CSS variable naming convention.

The variable --rad-combobox-content-height doesn't follow the --rad-ui-* prefix pattern used elsewhere in this file (e.g., --rad-ui-color-gray-300). Consider renaming for consistency.

♻️ Suggested fix
-    max-height: var(--rad-combobox-content-height, 300px);
+    max-height: var(--rad-ui-combobox-content-height, 300px);
styles/themes/components/menubar.scss (3)

1-1: Unused import: button module is not referenced in this file.

The @use 'button' import does not appear to be utilized anywhere in this stylesheet. If it's not needed, consider removing it to avoid potential unnecessary CSS inclusion.

🧹 Proposed fix
-@use 'button';
-
 .rad-ui-menubar-root {

25-25: Consider removing no-op transition: all 0s;.

A 0s transition duration effectively disables transitions. If this is intentional to override inherited transitions, consider adding a comment to clarify the intent. Otherwise, the property can be removed.


44-45: No-op backdrop-filter: blur(0px) can be removed.

A blur(0px) backdrop filter has no visual effect. Combined with background-color: transparent, this appears to be a placeholder. If backdrop blur isn't needed, consider removing the property to reduce CSS overhead.

🧹 Proposed fix
     min-width: 200px;
-    background-color: transparent;
-    backdrop-filter: blur(0px);
+    background-color: var(--rad-ui-color-gray-50);
     border-radius: 8px;

Alternatively, if transparency is intentional, just remove the no-op backdrop-filter:

     background-color: transparent;
-    backdrop-filter: blur(0px);
     border-radius: 8px;
src/components/ui/AlertDialog/tests/AlertDialog.a11y.test.tsx (2)

112-114: Track the flaky focus assertion explicitly.
Instead of commenting out expectations, consider moving the focus-restoration check into a test.todo/it.skip so it doesn’t silently lose coverage.


191-193: Avoid leaving focus-trap assertions commented.
These assertions are important for a11y behavior; consider extracting them into a dedicated test.todo/it.skip with a clear reason to keep the gap visible.

Also applies to: 196-197, 201-203

src/components/ui/ScrollArea/context/ScrollAreaContext.tsx (1)

11-11: Replace any with number for type safety.

Using any for clientX and clientY bypasses TypeScript's type checking. These should be typed as number since they represent pixel coordinates.

🔧 Proposed fix
-    handleScrollbarClick?: (e : { clientX?: any; clientY?: any; orientation: 'vertical' | 'horizontal' }) => void;
+    handleScrollbarClick?: (e : { clientX?: number; clientY?: number; orientation: 'vertical' | 'horizontal' }) => void;
src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx (3)

38-40: Refs are stable and don't need to be in the dependency array.

React refs created with useRef are stable across renders. Including them in the dependency array has no effect since their identity never changes. This can be misleading to future maintainers.

🔧 Suggested fix
     useEffect(() => {
         initializeThumbSizes();
-    }, [scrollYThumbRef, scrollXThumbRef, scrollAreaViewportRef]);
+    }, []);

52-54: Address static analysis warning about forEach callback.

The static analysis tool flags line 54 because resizeObserver.observe() returns void, making the callback technically return a value. While this is harmless, you can silence the linter by using a block body or a simple for...of loop.

🔧 Proposed fix using block body
         const resizeObserver = new ResizeObserver(() => handleResize());
         resizeObserver.observe(viewport);
-        Array.from(viewport.children).forEach(child => resizeObserver.observe(child));
+        Array.from(viewport.children).forEach(child => {
+            resizeObserver.observe(child);
+        });

32-36: Consider using a ref callback utility to avoid as any casts.

The merged ref logic works correctly, but the as any casts reduce type safety. A common pattern is to use a mergeRefs utility function.

🔧 Alternative implementation
+    // Utility function (could be extracted to a shared module)
+    const mergeRefs = <T,>(...refs: (React.Ref<T> | undefined)[]) => (node: T | null) => {
+        refs.forEach(ref => {
+            if (typeof ref === 'function') ref(node);
+            else if (ref) (ref as React.MutableRefObject<T | null>).current = node;
+        });
+    };
+
-    const mergedRootRef = (node: HTMLDivElement | null) => {
-        (internalRootRef as any).current = node;
-        if (typeof ref === 'function') ref(node);
-        else if (ref) (ref as any).current = node;
-    };
+    const mergedRootRef = mergeRefs(internalRootRef, ref);
src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (1)

87-90: Address static analysis warning about forEach callback.

Same issue as in ScrollAreaRoot.tsx - the observe() call implicitly returns, triggering the linter warning.

🔧 Proposed fix
         const ro = new ResizeObserver(checkOverflow);
         ro.observe(viewport);
-        Array.from(viewport.children).forEach(c => ro.observe(c));
+        Array.from(viewport.children).forEach(c => {
+            ro.observe(c);
+        });
         return () => ro.disconnect();
src/components/ui/ScrollArea/fragments/ScrollAreaThumb.tsx (1)

67-79: Global mouse listeners are always attached, not just during drag.

The mousemove and mouseup listeners are added on mount and persist throughout the component lifecycle. This is inefficient since these handlers only need to be active during drag operations.

Consider attaching listeners in startDrag and removing them in stopDrag to avoid unnecessary event processing.

🔧 Proposed optimization
+    const startDrag = useCallback((e: React.MouseEvent) => {
+        if (!scrollAreaViewportRef?.current) return;
+
+        e.preventDefault();
+        e.stopPropagation();
+
+        isDraggingRef.current = true;
+        dragStartRef.current = {
+            x: e.clientX,
+            y: e.clientY,
+            scrollTop: scrollAreaViewportRef.current.scrollTop,
+            scrollLeft: scrollAreaViewportRef.current.scrollLeft
+        };
+
+        document.body.style.userSelect = 'none';
+        document.body.style.cursor = 'grabbing';
+
+        document.addEventListener('mousemove', handleDrag);
+        document.addEventListener('mouseup', stopDrag);
+    }, [scrollAreaViewportRef, handleDrag, stopDrag]);

+    const stopDrag = useCallback(() => {
+        isDraggingRef.current = false;
+        document.body.style.cursor = '';
+        document.body.style.userSelect = '';
+        document.removeEventListener('mousemove', handleDrag);
+        document.removeEventListener('mouseup', stopDrag);
+    }, [handleDrag]);

-    React.useEffect(() => {
-        const handleMouseMove = (e: MouseEvent) => handleDrag(e);
-        const handleMouseUp = () => stopDrag();
-
-        document.addEventListener('mousemove', handleMouseMove);
-        document.addEventListener('mouseup', handleMouseUp);
-
-        return () => {
-            document.removeEventListener('mousemove', handleMouseMove);
-            document.removeEventListener('mouseup', handleMouseUp);
-            stopDrag();
-        };
-    }, [handleDrag, stopDrag]);
+    // Cleanup on unmount
+    React.useEffect(() => {
+        return () => stopDrag();
+    }, [stopDrag]);

Note: This refactor requires adjusting the dependency order of startDrag, stopDrag, and handleDrag to avoid circular dependencies. Alternatively, keep the current approach if simplicity is preferred, as the performance impact is minimal for typical use cases.

src/components/ui/Steps/fragments/StepRoot.tsx (1)

2-2: Remove unused useState import.

The useState import is no longer needed after refactoring to use useControllableState.

🧹 Proposed fix
-import React, { useState } from 'react';
+import React from 'react';
src/components/ui/Accordion/fragments/AccordionHeader.tsx (1)

6-13: Good semantic improvement using heading element.

Using h3 for the accordion header improves accessibility by establishing proper document structure. This aligns with WAI-ARIA accordion patterns.

Consider making the heading level configurable via an as prop (e.g., as="h2") for contexts where the accordion appears at different levels in the document hierarchy. This is a nice-to-have for future flexibility.

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

50-55: Consider using a CSS variable instead of hardcoded white.

For theme consistency, replace the hardcoded white with a CSS variable like var(--rad-ui-color-white) or var(--rad-ui-color-gray-0) if available in your design system.

🎨 Proposed fix
         &[data-state="completed"] {
             .rad-ui-steps-bubble {
                 border-color: var(--rad-ui-color-accent-600);
                 background-color: var(--rad-ui-color-accent-600);
-                color: white;
+                color: var(--rad-ui-color-white, `#ffffff`);
             }
src/components/ui/Dialog/tests/Dialog.portal.test.tsx (1)

103-109: Consider using the shared axe helper from test-utils for consistency.

The test imports axe-core directly and configures rule disabling inline. However, src/test-utils/index.ts exports a shared axe helper with pre-configured tag filtering. Using it (with a custom override mechanism) would improve consistency across the test suite.

If per-test rule disabling is needed frequently, consider extending the shared helper to accept rule overrides:

♻️ Example approach
// In test-utils/index.ts
export const axe = (element?: HTMLElement, options?: { disabledRules?: string[] }) =>
    axeCore.run(element ?? document.body, {
        runOnly: { type: 'tag', values: ACCESSIBILITY_TEST_TAGS },
        rules: options?.disabledRules?.reduce((acc, rule) => ({ ...acc, [rule]: { enabled: false } }), {})
    });
src/components/ui/Combobox/tests/Combobox.filtering.test.tsx (1)

7-44: Good test coverage for filtering functionality.

The test validates core filtering and group visibility behavior. A few optional enhancements to consider:

  1. Test clearing the search restores all items
  2. Consider using userEvent instead of fireEvent.change for more realistic input simulation
  3. Optionally verify ARIA attributes on filtered items (e.g., aria-hidden)
♻️ Optional: Add search clearing test
+        // Clear search
+        fireEvent.change(search, { target: { value: '' } });
+        
+        expect(screen.getByText('Apple')).toBeVisible();
+        expect(screen.getByText('Banana')).toBeVisible();
+        expect(screen.getByText('Carrot')).toBeVisible();
src/components/ui/Slider/tests/Slider.range.test.tsx (1)

6-8: Remove duplicate PointerEvent polyfill.

This polyfill is now defined globally in src/setupTests.ts (lines 64-66), so this local definition is redundant and can be removed.

♻️ Suggested removal
-// Polyfill PointerEvent for jsdom
-// `@ts-ignore`
-if (typeof window !== 'undefined' && !window.PointerEvent) window.PointerEvent = MouseEvent;
-
 describe('Slider Range Support', () => {
src/components/ui/Slider/Slider.tsx (1)

63-69: Default implementation renders only a single thumb.

When defaultValue is passed as an array (for range/multi-thumb scenarios), this default implementation still renders only one <Slider.Thumb>. This is likely intentional since users needing multi-thumb functionality would use the compound components directly (e.g., Slider.Root with multiple Slider.Thumb children or Slider.RangeSlider), but it may be worth documenting this behavior.

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

33-40: Consider adding defaultValue for uncontrolled mode consistency.

This test doesn't provide value or onValueChange, which is inconsistent with the first test's controlled pattern. If the Steps component supports uncontrolled mode with a defaultValue, consider explicitly testing that:

 it('supports customRootClass', () => {
     render(
-        <Steps.Root customRootClass="custom-steps" data-testid="steps-root">
+        <Steps.Root customRootClass="custom-steps" defaultValue={0} data-testid="steps-root">
             <Steps.Item value={0}>Step 0</Steps.Item>
         </Steps.Root>
     );
src/core/primitives/Combobox/fragments/ComboboxPrimitiveGroup.tsx (1)

51-51: Inline display: none may conflict with CSS specificity.

Using inline styles for hiding works but can be overridden unexpectedly or conflict with CSS frameworks. Consider using a data attribute or class-based approach for more consistent styling control.

♻️ Optional alternative using data attribute
-            <div 
-                className={className} 
-                ref={forwardedRef} 
-                {...props}
-                style={{ display: shouldHide ? 'none' : undefined, ...props.style }}
-            >
+            <div 
+                className={className} 
+                ref={forwardedRef} 
+                {...props}
+                data-hidden={shouldHide || undefined}
+                style={props.style}
+            >

Then in CSS:

[data-hidden] { display: none; }
src/core/primitives/Combobox/contexts/ComboboxGroupContext.tsx (1)

9-9: Consider adding null guard in the custom hook.

The hook returns ComboboxGroupContextType | null, requiring consumers to handle the null case. If items should always be within a group, consider throwing an error for better DX:

♻️ Optional guard pattern
-export const useComboboxGroupContext = () => useContext(ComboboxGroupContext);
+export const useComboboxGroupContext = () => {
+    const context = useContext(ComboboxGroupContext);
+    // Return null for optional group usage, or throw if required:
+    // if (!context) throw new Error('useComboboxGroupContext must be used within ComboboxGroup');
+    return context;
+};

However, if items can exist outside groups (making the group optional), returning null is the correct approach.

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

12-25: Several destructured context values appear unused.

labelsRef, valuesRef, and elementsRef are destructured but not used in the component. Consider removing them to reduce noise and prevent confusion:

♻️ Suggested cleanup
     const { 
         refs, 
         handleSelect, 
-        labelsRef, 
-        valuesRef, 
         activeIndex, 
-        elementsRef, 
         virtualItemRef, 
         getReferenceProps, 
         setHasSearch,
         search,
         setSearch,
-        setActiveIndex
+        setActiveIndex,
+        valuesRef
     } = context;

Note: valuesRef is used on line 52 for aria-activedescendant, so keep that one.

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

136-146: Ref value in dependency array will not trigger re-renders.

selectedItemRef.current in the dependency array (Line 146) won't cause the effect to re-run when the ref changes because React doesn't track ref mutations. The effect currently works because shift prop changes or component remounts trigger it.

If the intent is to run this effect when the selected item changes, consider using a state variable or a callback ref pattern instead.

Suggested fix
-    useLayoutEffect(() => {
-        if (!shift) return;
-        if (!selectedItemRef) return;
-        if (refs.floating.current && selectedItemRef.current) {
-            const rectA = refs.floating.current.getBoundingClientRect();
-            const rectB = selectedItemRef.current.getBoundingClientRect();
-
-            const relativeTop = rectA.top - rectB.bottom;
-            setOffsetPositionValue(relativeTop);
-        }
-    }, [selectedItemRef.current, shift]);
+    useLayoutEffect(() => {
+        if (!shift) return;
+        if (!selectedItemRef) return;
+        if (refs.floating.current && selectedItemRef.current) {
+            const rectA = refs.floating.current.getBoundingClientRect();
+            const rectB = selectedItemRef.current.getBoundingClientRect();
+
+            const relativeTop = rectA.top - rectB.bottom;
+            setOffsetPositionValue(relativeTop);
+        }
+    }, [shift, refs.floating]);
src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx (1)

118-118: Style spread order allows props.style to override hidden state.

The current order { display: isHidden ? 'none' : undefined, ...props.style } allows consumers to accidentally override the display: none for hidden items. If hiding should be enforced, reverse the order.

If hidden state should be enforced
-            style={{ display: isHidden ? 'none' : undefined, ...props.style }}
+            style={{ ...props.style, display: isHidden ? 'none' : undefined }}
styles/themes/components/slider.scss (1)

172-177: Focus styles are duplicated from base thumb rules.

The focus outline styles (Lines 172-176) are identical to the base thumb focus styles (Lines 76-80). Since outlines don't conflict with the transform overrides, these could be removed to reduce duplication, but keeping them explicit is also acceptable for maintainability.

src/components/ui/Slider/fragments/SliderRoot.tsx (1)

58-64: Consider using existing useMergeRefs utility.

The manual ref merging works but is verbose and uses type casts. The codebase already uses Floater.useMergeRefs in other components (e.g., ComboboxPrimitiveItem, DialogPrimitiveContent).

Suggested simplification
+import Floater from '~/core/primitives/Floater';
+
 // ...
 
-    const mergedRef = React.useMemo(() => {
-        return (node: HTMLDivElement | null) => {
-            (internalRef as any).current = node;
-            if (typeof ref === 'function') ref(node);
-            else if (ref) (ref as any).current = node;
-        };
-    }, [ref]);
+    const mergedRef = Floater.useMergeRefs([internalRef, ref]);
src/core/hooks/useControllableState/useControllableState.test.tsx (1)

88-100: Ensure console.warn spies restore even if assertions fail.
All three warnSpy tests (lines 88–100, 103–120, 138–161) call mockRestore() unconditionally at the end. If any expect() throws, the spy won't be restored, causing cross-test pollution. Wrap test logic in try/finally, or add afterEach(() => jest.restoreAllMocks()) to src/setupTests.ts for global cleanup (recommended, since jest.config.ts has clearMocks but not restoreMocks).

♻️ Safer cleanup with try/finally (per-test fix)
     it('should fall back to defaultValue when controlledValue becomes undefined', () => {
         const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
-        const initialProps: { value: string | undefined } = { value: 'controlled' };
-        const { result, rerender } = renderHook(
-            ({ value }) => useControllableState<string>(value, 'default', jest.fn()),
-            { initialProps }
-        );
-
-        expect(result.current[0]).toBe('controlled');
-
-        rerender({ value: undefined });
-        expect(result.current[0]).toBe('default');
-        warnSpy.mockRestore();
+        try {
+            const initialProps: { value: string | undefined } = { value: 'controlled' };
+            const { result, rerender } = renderHook(
+                ({ value }) => useControllableState<string>(value, 'default', jest.fn()),
+                { initialProps }
+            );
+
+            expect(result.current[0]).toBe('controlled');
+
+            rerender({ value: undefined });
+            expect(result.current[0]).toBe('default');
+        } finally {
+            warnSpy.mockRestore();
+        }
     });

Comment on lines +50 to +54
const [value, setValue] = useControllableState<number | number[]>(
valueProp,
defaultValue ?? (Array.isArray(valueProp) ? valueProp : 0),
onValueChange
);
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.

⚠️ Potential issue | 🟡 Minor

defaultValue fallback logic may cause type inconsistencies.

The fallback defaultValue ?? (Array.isArray(valueProp) ? valueProp : 0) has issues:

  1. If valueProp is an array and defaultValue is undefined, it uses valueProp as the default (but valueProp might be the controlled value, not a default)
  2. If both are undefined, it defaults to 0 (number), but if valueProp later becomes an array, there's a type mismatch
Suggested fix
     const [value, setValue] = useControllableState<number | number[]>(
         valueProp,
-        defaultValue ?? (Array.isArray(valueProp) ? valueProp : 0),
+        defaultValue ?? (Array.isArray(valueProp) ? [min, max] : min),
         onValueChange
     );

This provides sensible defaults based on the expected value type.

🤖 Prompt for AI Agents
In `@src/components/ui/Slider/fragments/SliderRoot.tsx` around lines 50 - 54, The
fallback for useControllableState in SliderRoot.tsx is using valueProp as a
default which can blur controlled vs default semantics and produce number/array
type mismatches; change the second argument to choose a safe shape instead of
valueProp (e.g., defaultValue ?? (Array.isArray(valueProp) ? [0, 0] : 0)) so the
initial state is a number for single-value sliders and an array for range
sliders, and keep using the existing
useControllableState/valueProp/onValueChange identifiers to preserve controlled
behavior.

Comment on lines +47 to +65
// Use ResizeObserver to handle dynamic content changes
useEffect(() => {
if (!open || !ref.current || transitionDuration === 0) return;

const resizeObserver = new ResizeObserver(() => {
if (ref.current && heightRef.current !== undefined) {
const newHeight = ref.current.scrollHeight;
if (newHeight !== heightRef.current) {
setHeight(newHeight);
}
}
});

// Observe the first child if possible for more accurate content measurement,
// or the ref itself if it's not currently animating height: 0
resizeObserver.observe(ref.current);

return () => resizeObserver.disconnect();
}, [open, transitionDuration]);
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, explore the project structure
ls -la

Repository: rad-ui/ui

Length of output: 2891


🏁 Script executed:

# Check the file in question
head -100 src/core/primitives/Collapsible/fragments/CollapsiblePrimitiveContent.tsx

Repository: rad-ui/ui

Length of output: 3619


🏁 Script executed:

# Check project configuration
cat package.json | head -50

Repository: rad-ui/ui

Length of output: 1723


🏁 Script executed:

# Search for ResizeObserver usage in the codebase
rg "ResizeObserver" --type tsx --type ts -B 2 -A 2

Repository: rad-ui/ui

Length of output: 80


🏁 Script executed:

# Search for ResizeObserver usage with correct syntax
rg "ResizeObserver" -A 2 -B 2

Repository: rad-ui/ui

Length of output: 4344


🏁 Script executed:

# Check full package.json for dependencies related to polyfills
cat package.json

Repository: rad-ui/ui

Length of output: 10507


🏁 Script executed:

# Check tsconfig for browser target
cat tsconfig.json

Repository: rad-ui/ui

Length of output: 792


🏁 Script executed:

# Check jest configuration
cat jest.config.ts

Repository: rad-ui/ui

Length of output: 1456


Add ResizeObserver availability guard to prevent runtime crashes in older browsers.

The project targets ES5 and lacks a production-ready ResizeObserver polyfill. The test setup includes a mock, but this doesn't prevent crashes when the component runs in production on browsers that don't support ResizeObserver. Add the feature check:

 useEffect(() => {
     if (!open || !ref.current || transitionDuration === 0) return;
+    if (typeof ResizeObserver === 'undefined') return;
 
     const resizeObserver = new ResizeObserver(() => {
📝 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
// Use ResizeObserver to handle dynamic content changes
useEffect(() => {
if (!open || !ref.current || transitionDuration === 0) return;
const resizeObserver = new ResizeObserver(() => {
if (ref.current && heightRef.current !== undefined) {
const newHeight = ref.current.scrollHeight;
if (newHeight !== heightRef.current) {
setHeight(newHeight);
}
}
});
// Observe the first child if possible for more accurate content measurement,
// or the ref itself if it's not currently animating height: 0
resizeObserver.observe(ref.current);
return () => resizeObserver.disconnect();
}, [open, transitionDuration]);
// Use ResizeObserver to handle dynamic content changes
useEffect(() => {
if (!open || !ref.current || transitionDuration === 0) return;
if (typeof ResizeObserver === 'undefined') return;
const resizeObserver = new ResizeObserver(() => {
if (ref.current && heightRef.current !== undefined) {
const newHeight = ref.current.scrollHeight;
if (newHeight !== heightRef.current) {
setHeight(newHeight);
}
}
});
// Observe the first child if possible for more accurate content measurement,
// or the ref itself if it's not currently animating height: 0
resizeObserver.observe(ref.current);
return () => resizeObserver.disconnect();
}, [open, transitionDuration]);
🤖 Prompt for AI Agents
In `@src/core/primitives/Collapsible/fragments/CollapsiblePrimitiveContent.tsx`
around lines 47 - 65, The ResizeObserver usage in the useEffect can crash on
browsers without support; inside the useEffect (before creating new
ResizeObserver) add a guard that checks for runtime availability (e.g. ensure
typeof window !== "undefined" and 'ResizeObserver' in window or typeof
ResizeObserver !== "undefined") and simply return if it's not available; keep
the rest of the logic using ref, heightRef, setHeight, open and
transitionDuration unchanged so tests/mocks still work.

@github-actions
Copy link
Copy Markdown
Contributor

Coverage

This report compares the PR with the base branch. "Δ" shows how the PR affects each metric.

Metric PR Δ
Statements 84.08% -0.16%
Branches 61.85% -0.52%
Functions 68.98% +0.47%
Lines 83.27% -0.21%

Coverage decreased for at least one metric. Please add or update tests to improve coverage.

Run npm run coverage locally for detailed reports and target untested areas to raise these numbers.

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

Caution

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

⚠️ Outside diff range comments (1)
src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx (1)

78-113: ⚠️ Potential issue | 🟡 Minor

Prevent overflow from being true when viewport size is non‑measurable.

If viewportHeight/viewportWidth is <= 0 but content is larger, hasV/hasH becomes true, which can surface scrollbars while layout is still settling. Consider guarding overflow on a positive viewport size.

🔧 Suggested fix
-        const hasV = contentHeight > viewportHeight;
+        const hasV = viewportHeight > 0 && contentHeight > viewportHeight;
@@
-        const hasH = contentWidth > viewportWidth;
+        const hasH = viewportWidth > 0 && contentWidth > viewportWidth;
🤖 Fix all issues with AI agents
In `@src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx`:
- Around line 70-75: The early return in the ComboboxPrimitiveItem component
breaks the Rules of Hooks because it prevents subsequent hooks (useRef,
Floater.useListItem, useComboboxGroupContext, and the useEffect calls that call
bumpLabelsVersion) from being invoked on every render; replace that pattern by
creating a required-context helper (e.g., useRequiredComboboxGroupContext) that
throws if the context is missing and call it unconditionally at the top of
ComboboxPrimitiveItem, then remove the conditional early return so that
bumpLabelsVersion, useRef, Floater.useListItem, useComboboxGroupContext (via the
required hook), and all useEffect hooks are always executed in the same order;
ensure the helper references the same context used by useComboboxGroupContext so
existing logic continues to work.
🧹 Nitpick comments (3)
src/components/ui/Slider/fragments/SliderThumb.tsx (1)

113-116: Hard-coded 12px offset assumes fixed thumb dimensions.

The positioning calculation uses a fixed 12px offset for centering. This works for a default thumb size but may misalign if consumers customize thumb dimensions via CSS.

Consider using CSS custom properties or CSS transform: translateX(-50%) / translateY(-50%) for more flexible centering that adapts to any thumb size.

package.json (1)

285-285: Remove unused ts-node dependency.

No usage of ts-node was found in the codebase. The project already uses tsx for direct TypeScript file execution (e.g., generate-tokens script) and ts-jest for Jest test execution. Adding ts-node introduces a redundant dev dependency without a clear purpose. Additionally, the tsconfig.json uses "module": "ESNext" and "moduleResolution": "node" rather than the TypeScript-recommended "NodeNext"/"Node16" settings for proper ESM tooling compatibility.

src/core/primitives/Collapsible/fragments/CollapsiblePrimitiveContent.tsx (1)

68-85: Avoid duplicate ResizeObservers—consolidate to one effect.
There are now two near‑identical ResizeObserver effects (this one and the earlier block). That registers two observers on the same element and duplicates callbacks/state updates. Recommend removing the earlier effect and keeping this heightRef‑based version.

Comment on lines +70 to +75
React.useEffect(() => {
bumpLabelsVersion();
return () => {
bumpLabelsVersion();
};
}, [index, value, bumpLabelsVersion]);
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

React rules of hooks early return before hooks violation

💡 Result:

An early return before some Hooks run is a Rules of Hooks violation because React must see the same Hooks called in the same order on every render. If one render returns early, React may see “fewer hooks than expected” and can’t reliably match Hook state to the right call site. The official lint rule explicitly lists “Hooks after early returns” as a common violation. [1][2]

The violating pattern

function MyComp({ user }) {
  if (!user) return null;          // ❌ early return

  const [x, setX] = useState(0);   // ❌ hook after early return
  useEffect(() => {}, []);
  return <div />;
}

Fix patterns

1) Call Hooks first; return after

function MyComp({ user }) {
  const [x, setX] = useState(0);
  useEffect(() => {}, []);

  if (!user) return null;          // ✅ OK (after hooks)
  return <div />;
}

2) Split into a child component

function MyComp({ user }) {
  if (!user) return null;          // ✅ no hooks here
  return <Authed user={user} />;
}

function Authed({ user }) {
  const [x, setX] = useState(0);   // ✅ always runs when Authed renders
  return <div />;
}

3) Keep the Hook call unconditional; make its inputs/behavior conditional

function MyComp({ user }) {
  const enabled = !!user;
  useEffect(() => {
    if (!enabled) return;
    // do the effect
  }, [enabled]);

  if (!user) return null;
  return <div />;
}

Notes:

  • This applies to built-in Hooks and custom Hooks (anything that’s a Hook must obey the same rule). [2]
  • React’s special use API is an exception (it can be conditional), but normal Hooks like useState/useEffect cannot. [1]

Sources: React “Rules of Hooks” and the rules-of-hooks ESLint rule docs. [1][2]


🏁 Script executed:

find . -name "ComboboxPrimitiveItem.tsx" -type f

Repository: rad-ui/ui

Length of output: 119


🏁 Script executed:

cat -n ./src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx | head -100

Repository: rad-ui/ui

Length of output: 3946


Fix hook ordering: early return before hooks violates Rules of Hooks.

The conditional early return at line 23 prevents all subsequent hooks (useRef at line 44, Floater.useListItem at line 45, useComboboxGroupContext at line 54, and multiple useEffect hooks) from being called on every render, breaking the Rules of Hooks. Create a required-context hook to ensure all hooks are called unconditionally.

Suggested fix
+const useRequiredComboboxContext = () => {
+    const context = useContext(ComboboxPrimitiveContext);
+    if (!context) {
+        throw new Error('ComboboxPrimitiveItem must be used within a ComboboxPrimitive');
+    }
+    return context;
+};
+
 const ComboboxPrimitiveItem = React.forwardRef<
     React.ElementRef<typeof Primitive.div>,
     ComboboxPrimitiveItemProps & React.ComponentPropsWithoutRef<typeof Primitive.div>
 >(({ children, value, disabled, className, ...props }, forwardedRef) => {
-    const context = useContext(ComboboxPrimitiveContext);
-
-    if (!context) {
-        console.error('ComboboxPrimitiveItem must be used within a ComboboxPrimitive');
-        return null;
-    }
+    const context = useRequiredComboboxContext();
🧰 Tools
🪛 Biome (2.3.13)

[error] 70-70: 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)

🤖 Prompt for AI Agents
In `@src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx` around
lines 70 - 75, The early return in the ComboboxPrimitiveItem component breaks
the Rules of Hooks because it prevents subsequent hooks (useRef,
Floater.useListItem, useComboboxGroupContext, and the useEffect calls that call
bumpLabelsVersion) from being invoked on every render; replace that pattern by
creating a required-context helper (e.g., useRequiredComboboxGroupContext) that
throws if the context is missing and call it unconditionally at the top of
ComboboxPrimitiveItem, then remove the conditional early return so that
bumpLabelsVersion, useRef, Floater.useListItem, useComboboxGroupContext (via the
required hook), and all useEffect hooks are always executed in the same order;
ensure the helper references the same context used by useComboboxGroupContext so
existing logic continues to work.

@kotAPI kotAPI merged commit a7c3221 into main Jan 30, 2026
9 of 10 checks passed
@kotAPI kotAPI deleted the kotapi/13-jan-part2-improvements branch January 30, 2026 08:40
@coderabbitai coderabbitai Bot mentioned this pull request Apr 4, 2026
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