Skip to content

Conversation

@kotAPI
Copy link
Collaborator

@kotAPI kotAPI commented Jan 15, 2026

Note

Major component enhancements and test/styling updates.

  • ScrollArea: New type (auto|always|scroll|hover), both orientations supported, refined thumb sizing/dragging and scrollbar click paging; expanded context; new Corner; stories for all modes; updated styles for custom scrollbars
  • Slider: Supports single or range values (number | number[]); updated Root, Range, and Thumb to handle multiple thumbs and keyboard/pointer interactions; new range tests and snapshot updates
  • Combobox: Client-side search filtering with hidden indices and group visibility via new contexts; tests for filtering/group hiding; refreshed styles
  • Steps: Controllable value/defaultValue; Item exposes data-state (completed/active/inactive); tests added
  • Accordion: Header rendered as h3 and ref types fixed; new dynamic-content test
  • Dialog/AlertDialog/Tooltip: Dialog content now uses Floater.FocusManager; tests relaxed for jsdom focus flakiness and axe rule toggled; Tooltip wraps content in .rad-ui-tooltip-content-inner for animations; accessibility/stability tweaks
  • Infra/Styles: Global test polyfills (ResizeObserver, PointerEvent); updated styles for menubar, scroll-area, combobox, tooltip

Written by Cursor Bugbot for commit 6fc42a5. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Added search/filtering functionality to Combobox with dynamic item visibility.
    • Added dual-thumb range slider support alongside single-thumb mode.
    • Added configurable scrollbar visibility types (auto, always, scroll, hover) to ScrollArea.
    • Steps component now supports controlled state management.
  • Bug Fixes

    • Fixed focus trap edge cases in dialogs and alerts in test environments.
    • Improved dynamic content handling in Accordion and ScrollArea components.
  • Tests

    • Added comprehensive test coverage for Accordion, Combobox, Slider range mode, and Steps functionality.
  • Style

    • Refreshed Combobox, Menubar, ScrollArea, Steps, and Tooltip styling for improved consistency and user experience.

✏️ 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

changeset-bot bot commented Jan 15, 2026

⚠️ No Changeset found

Latest commit: 6fc42a5

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
Contributor

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

This PR introduces sweeping enhancements across multiple UI components and styling systems. Notable changes include: Accordion header element change from div to h3, comprehensive ScrollArea dual-axis refactor with expanded context and visibility handling, Slider multi-thumb (range) support with array values, Steps controllable state implementation, Combobox search-driven filtering with visibility tracking, Dialog content focus management refactor, AlertDialog test focus assertions disabled for stability, Tooltip inner wrapper structure, Collapsible dynamic height adjustment, and extensive styling rewrites for Combobox, Menubar, ScrollArea, Steps, and Tooltip components.

Changes

Cohort / File(s) Summary
Accordion Element & Tests
src/components/ui/Accordion/fragments/AccordionHeader.tsx, src/components/ui/Accordion/tests/Accordion.*.test.tsx
Header element type changed from div to h3 (forwardRef generic and JSX), headerRef type assertion updated; new dynamic content test validates rendering and state updates without crashing.
ScrollArea Comprehensive Refactor
src/components/ui/ScrollArea/context/ScrollAreaContext.tsx, src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx, src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx, src/components/ui/ScrollArea/fragments/ScrollAreaThumb.tsx, src/components/ui/ScrollArea/fragments/ScrollAreaCorner.tsx
Context expanded with refs (scrollYThumbRef, rootRef), type property ('auto'/'always'/'scroll'/'hover'), and orientation-aware handlers; Root introduces dual-axis thumb sizing, overflow tracking, and visibility state; Scrollbar adds visibility logic with auto-hide, ResizeObserver overflow detection, and orientation-based continuous scrolling; Thumb supports horizontal orientation and per-axis drag handling; Corner integrates rootClass styling.
ScrollArea Styling & Stories
src/components/ui/ScrollArea/stories/ScrollArea.stories.tsx, styles/themes/components/scroll-area.scss
Stories expanded with AutoType, AlwaysType, ScrollType, HoverType, BothOrientations, HorizontalOnly, InsideCard variants; SCSS restructured with orientation-based sizing, hover interactions, and refined thumb styling.
Slider Multi-Thumb Support
src/components/ui/Slider/Slider.tsx, src/components/ui/Slider/context/SliderContext.tsx, src/components/ui/Slider/fragments/SliderRoot.tsx, src/components/ui/Slider/fragments/SliderRange.tsx, src/components/ui/Slider/fragments/SliderThumb.tsx
Props expanded to accept number | number[] for range/dual-thumb mode; Context and fragments updated with array value handling, per-thumb tracking, index-based updates, and per-axis percent calculations; Range styling now computes start/end percentages for visual range display.
Slider Tests
src/components/ui/Slider/tests/Slider.interaction.test.tsx, src/components/ui/Slider/tests/Slider.range.test.tsx
Existing test updated with type cast for value handler; new range test validates dual-thumb rendering, nearest thumb selection on track click, and visual range positioning.
Steps Controllable State
src/components/ui/Steps/fragments/StepRoot.tsx, src/components/ui/Steps/fragments/StepItem.tsx, src/components/ui/Steps/tests/Steps.test.tsx
Root now accepts value, defaultValue, onValueChange via useControllableState and passes through context; Item renders data-state and data-value attributes, derives completion/activity flags; tests verify state-based rendering and customRootClass composition.
Combobox Search & Filtering
src/core/primitives/Combobox/contexts/ComboboxGroupContext.tsx, src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx, src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx, src/core/primitives/Combobox/fragments/ComboboxPrimitiveGroup.tsx, src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx, src/core/primitives/Combobox/fragments/ComboboxPrimitiveSearch.tsx
New ComboboxGroupContext provides item registration and visibility tracking; PrimitiveContext expanded with search, setSearch, hiddenIndices; Root computes hiddenIndices from search filtering; Group conditionally hides when all items filtered; Item registers visibility and renders hidden style; Search uses context-provided search state and simplified DOM handling.
Combobox Tests
src/components/ui/Combobox/tests/Combobox.simple.test.tsx, src/components/ui/Combobox/tests/Combobox.filtering.test.tsx
New simple test validates basic rendering; filtering test verifies search input filters items, hides empty groups, and reflects visibility state.
Dialog & AlertDialog Focus Management
src/components/ui/Dialog/tests/Dialog.portal.test.tsx, src/components/ui/AlertDialog/tests/AlertDialog.a11y.test.tsx, src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx
DialogPrimitiveContent refactored to use Floater.FocusManager for focus handling instead of manual control; props simplified via ComponentPropsWithoutRef; Dialog/AlertDialog tests disable flaky focus restoration assertions via FIXME comments; axe accessibility rule for aria-command-name disabled.
Collapsible Dynamic Height
src/core/primitives/Collapsible/fragments/CollapsiblePrimitiveContent.tsx
ResizeObserver effect added to dynamically recalculate height on content scrollHeight changes while open; opening height initialization tightened to avoid redundant resets.
Tooltip Structure & Styling
src/components/ui/Tooltip/fragments/TooltipContent.tsx, styles/themes/components/tooltip.scss, styles/themes/default.scss
Content wraps arrow and children in new inner div with "rad-ui-tooltip-content-inner" class; new tooltip.scss module introduced with floating-element and content-inner classes plus rad-ui-tooltip-slide-in keyframes animation; default.scss imports tooltip module and removes legacy inline tooltip CSS.
Combobox Styling Overhaul
styles/themes/components/combobox.scss
Root width increased to 240px; trigger refactored with flex layout, focus-visible states, and ::after arrow rotation on open; content redesigned with glassy panel, slide-down animation, and [data-state="closed"] hiding; search input compacted; items aligned flexbox with new indicator; added data-disabled state and rad-ui-slide-down keyframes.
Menubar Styling Rewrite
styles/themes/components/menubar.scss
Refactored to use data-state/data-disabled patterns, glassy content panel with rad-ui-slide-in animation, simplified typography and spacing, removed legacy aria-expanded/aria-disabled selectors, consolidated state handling.
Steps Styling Enhancement
styles/themes/components/steps.scss
Gap increased to 16px, step bubble enlarged to 32x32 with border and background, line width increased to 2px with rounded corners; introduced data-status="active" and data-status="complete" styling with color/shadow adjustments; typography refined, last-step cleanup rules added.
Test Environment Setup
src/setupTests.ts, src/test-utils/portal.ts
Global ResizeObserver mock and PointerEvent polyfill added to jsdom setup; focus-trap assertions in portal test utility disabled due to flakiness.

Sequence Diagrams

sequenceDiagram
    participant User
    participant ScrollArea as ScrollArea Root
    participant Scrollbar
    participant Thumb
    participant DOM as ResizeObserver/<br/>Viewport

    User->>ScrollArea: Mount with content
    ScrollArea->>DOM: Create ResizeObserver & MutationObserver
    DOM-->>ScrollArea: Monitor viewport/content changes
    
    ScrollArea->>ScrollArea: Calculate thumb sizes (dual-axis)
    ScrollArea->>Scrollbar: Pass context (type, refs, handlers)
    Scrollbar->>Scrollbar: Determine visibility based on type
    Scrollbar->>DOM: Show/hide scrollbar
    
    User->>Scrollbar: Hover/interact
    Scrollbar->>Scrollbar: Update visibility (show, set timeout)
    
    User->>Scrollbar: Click to scroll
    Scrollbar->>ScrollArea: Call handleScrollbarClick(orientation, coordinates)
    ScrollArea->>ScrollArea: Compute scroll position for orientation
    ScrollArea->>Thumb: Update thumb position
    Thumb->>DOM: Render at new position
    
    DOM->>ScrollArea: Content/viewport size changes
    ScrollArea->>ScrollArea: Recalculate overflow & thumb sizes
    ScrollArea->>Scrollbar: Update overflow/type context
    Scrollbar->>Scrollbar: Re-evaluate visibility rules
Loading
sequenceDiagram
    participant User
    participant Combobox as Combobox Root
    participant Search as Search Input
    participant Group as ComboboxGroup
    participant Item as ComboboxItem
    participant Context as ComboboxContext

    User->>Combobox: Mount with items/groups
    Combobox->>Context: Initialize (search='', hiddenIndices=[])
    
    User->>Search: Type filter text
    Search->>Search: setSearch(text)
    Combobox->>Combobox: Compute hiddenIndices (non-matching labels)
    Context->>Context: Update (search, hiddenIndices)
    
    Group->>Context: Subscribe to search & hiddenIndices
    Group->>Group: Compute visibleItems (via registerItem)
    Group->>Group: Set shouldHide = search && !visibleItems.length
    
    Item->>Group: registerItem(id, isVisible)
    Item->>Item: Track visibility status
    Item->>Item: Render with style: hidden={isHidden}
    
    Note over Group,Item: Empty groups hidden during active search
    Note over Item: Only matching items visible
    
    User->>Search: Clear text
    Search->>Combobox: setSearch('')
    Combobox->>Context: Reset hiddenIndices=[]
    Group->>Group: shouldHide = false
    Item->>Item: All items visible again
Loading
sequenceDiagram
    participant User
    participant SliderRoot as Slider Root
    participant Thumb1 as Thumb[0]
    participant Thumb2 as Thumb[1]
    participant Context as SliderContext

    User->>SliderRoot: Mount with value=[20, 80]
    SliderRoot->>Context: Initialize (value=[20,80], activeThumbIndex=null)
    SliderRoot->>Thumb1: Render at 20%
    SliderRoot->>Thumb2: Render at 80%
    
    User->>SliderRoot: Click at position clientX=30
    SliderRoot->>SliderRoot: Identify nearest thumb (Thumb1)
    SliderRoot->>Context: Set activeThumbIndexRef=0
    SliderRoot->>SliderRoot: Compute newValue for position
    Context->>Context: setValue([30, 80])
    
    User->>Thumb1: Drag to 40
    SliderRoot->>SliderRoot: setFromPosition(40)
    SliderRoot->>Context: setValue([40, 80])
    Thumb1->>Thumb1: Update percent, aria-valuenow
    
    User->>SliderRoot: Release drag
    SliderRoot->>Context: Reset activeThumbIndexRef
    
    Note over Thumb1,Thumb2: Range slider tracks[min,max] with dual thumbs
    Note over SliderRoot: Identifies & updates nearest thumb on interaction
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related issues

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • GoldGroove06

🐰 Hops excitedly through the updates!

Sliders now dance with dual thumbs in hand,
Scrollbars whisper through each orient and strand,
Steps march forth with state under control,
While Combobox filters search toward its goal! ✨

🚥 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 overly broad, describing multiple categories of changes without highlighting the primary focus. Consider using a more specific title that highlights the main change, such as focusing on a primary component (e.g., 'Add range slider support and improve ScrollArea visibility') or the most significant feature addition.
✅ 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

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

}
position: relative;
display: inline-block;
width: var(--rad-combobox-width, 240px);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we dont have these tokens, remove

@@ -0,0 +1,33 @@
.rad-ui-tooltip-floating-element {
z-index: 50;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z-index isnt usually required since we do portals

this is probably only required when portals arent used - verify

@github-actions
Copy link
Contributor

Coverage

⚠️ Could not compare with base branch (base branch has failing tests or coverage issues). Showing PR coverage only.

Metric PR
Statements 84.27%
Branches 62.39%
Functions 68.51%
Lines 83.51%

✅ Coverage thresholds met! All tests passing.

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

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

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/stories/ScrollArea.stories.tsx (2)

18-25: Missing key prop on React Fragment.

When mapping to fragments, the fragment needs a key. Use <React.Fragment key={index}> instead of the shorthand <>.

Suggested 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 at least since the 1960s, when it was popularized by advertisements for Letraset transfer sheets. It is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin.Versions of the Lorem ipsum text have been used in typesetting at least since the 1960s, when it was popularized by advertisements for Letraset transfer sheets. It is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin.Versions of the Lorem ipsum text have been used in typesetting at least since the 1960s, when it was popularized by advertisements for Letraset transfer sheets. It is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin.
                                 </Text>
-                            </>
+                            </React.Fragment>
                         ))}

56-61: Missing key prop on React Fragment.

Same issue as above - use <React.Fragment key={index}> when mapping.

Suggested 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/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (1)

182-199: Props spread overwrites the inline style prop.

The {...props} spread at line 195 includes props.style, which will overwrite the computed style object at lines 188-191, potentially losing the display: 'none' logic.

🐛 Suggested fix

Move the style prop after the spread, or destructure style from props:

-const ScrollAreaScrollbar = forwardRef<ScrollAreaScrollbarElement, ScrollAreaScrollbarProps>(({ children, className = '', orientation = 'vertical', ...props }, ref) => {
+const ScrollAreaScrollbar = forwardRef<ScrollAreaScrollbarElement, ScrollAreaScrollbarProps>(({ children, className = '', orientation = 'vertical', style, ...props }, ref) => {

     // ... rest of component ...

     return (
         <div
             ref={ref}
             className={clsx(rootClass + '-scrollbar', className)}
             data-orientation={orientation}
             data-state={isVisible ? 'visible' : 'hidden'}
-            style={{ 
-                display: shouldKeepInDOM ? undefined : 'none', 
-                ...props.style 
-            }}
-            onMouseDown={startContinuousScroll}
-            onMouseUp={stopContinuousScroll}
-            onMouseLeave={stopContinuousScroll}
             {...props}
+            style={{ 
+                display: shouldKeepInDOM ? undefined : 'none', 
+                ...style 
+            }}
+            onMouseDown={startContinuousScroll}
+            onMouseUp={stopContinuousScroll}
+            onMouseLeave={stopContinuousScroll}
         >
src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx (1)

23-26: Hooks called conditionally after early return violates Rules of Hooks.

The early return on line 25 causes all subsequent hooks (lines 44-45, 54, 57, 64, 72, 98, 106) to be called conditionally. React requires hooks to be called in the same order on every render.

Move all hooks before the context validation, or throw an error instead of returning null.

🛠️ Recommended fix - throw instead of return
     if (!context) {
-        console.error('ComboboxPrimitiveItem must be used within a ComboboxPrimitive');
-        return null;
+        throw new Error('ComboboxPrimitiveItem must be used within a ComboboxPrimitive');
     }
src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx (1)

135-145: useLayoutEffect dependency on .current is unreliable.

selectedItemRef.current in the dependency array (line 145) won't trigger re-runs when the ref's current value changes, since React compares the ref object identity, not its contents.

Consider using state or a different triggering mechanism if you need to react to changes in the referenced element.

🤖 Fix all issues with AI agents
In `@src/components/ui/Accordion/fragments/AccordionHeader.tsx`:
- Around line 6-11: AccordionHeaderProps is incorrectly typed as
React.ComponentPropsWithoutRef<'div'> while AccordionHeader renders an <h3> and
uses React.ElementRef<'h3'> for the ref; change AccordionHeaderProps to
React.ComponentPropsWithoutRef<'h3'> so props and intrinsic element match,
update any related imports/types if needed, and ensure the forwardRef signature
in AccordionHeader remains React.forwardRef<React.ElementRef<'h3'>,
AccordionHeaderProps>.

In `@src/components/ui/Accordion/tests/Accordion.dynamic.test.tsx`:
- Around line 6-11: Remove the local ResizeObserver mock in
Accordion.dynamic.test.tsx because a global ResizeObserver mock is already
provided in setupTests.ts; locate and delete the global.ResizeObserver =
jest.fn().mockImplementation(...) block (the observe/unobserve/disconnect mocks)
so the test uses the shared implementation instead of redefining ResizeObserver
locally.

In `@src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx`:
- Around line 49-69: The mouseenter handler doesn't clear an existing hide
timeout, so a pending hide from handleMouseLeave can still fire after
re-entering; update handleMouseEnter (used in the useEffect) to clear
hideTimeoutRef.current before calling setVisible(true) (and ensure
hideTimeoutRef is the same ref used in handleMouseLeave), so re-entering cancels
any pending hide timer and prevents the race condition.
- Line 151: The variable shouldRender is declared with the same logic as
isVisible but never used; remove the unused declaration of shouldRender to avoid
dead code and keep only isVisible (used later). Locate the const shouldRender =
... line and delete it, ensuring no other references to shouldRender remain; if
any exist, replace them with isVisible.

In `@src/components/ui/Slider/fragments/SliderRange.tsx`:
- Around line 17-28: The code in SliderRange.tsx assumes Array.isArray(value)
implies non-empty; guard against an empty array before using sortedValues[0] and
sortedValues[sortedValues.length - 1] to avoid undefined/NaN: inside the
Array.isArray(value) branch, check if value.length === 0 and set startPercent
and endPercent to 0 (or appropriate defaults) and return/skip the percent
calculations; otherwise proceed to compute sortedValues, minVal, maxVal and the
existing logic (including the existing single-value handling for value.length
=== 1).

In `@src/components/ui/Slider/fragments/SliderRoot.tsx`:
- Line 50: The initial state passed to useControllableState can be a number (0)
even when valueProp is an array, causing a type mismatch; change the default
initializer to infer shape from valueProp so it matches number | number[] (e.g.,
replace defaultValue ?? 0 with defaultValue ?? (Array.isArray(valueProp) ?
valueProp : 0)); update the call in SliderRoot where value and setValue are
declared (useControllableState, valueProp, defaultValue, value, setValue) so the
initial state type aligns with the provided controlled value.
- Around line 93-99: The current logic uses nextValue.indexOf(newValue) which
breaks when duplicate values exist; instead preserve the original thumb index
through the sort: map the array to tuples like {value, origIndex} (use symbols
nextValue and indexToUpdate as entry points), update the tuple at origIndex to
newValue, sort the tuple array by value, then set activeThumbIndexRef.current to
the index where tuple.origIndex === indexToUpdate and finally extract the sorted
values back into the values array; update references around activeThumbIndexRef,
nextValue and indexToUpdate accordingly.

In `@src/components/ui/Slider/fragments/SliderThumb.tsx`:
- Around line 21-24: Array index access can return undefined; update the
currentValue calculation in SliderThumb to validate array bounds: if
Array.isArray(value) and index is within 0..value.length-1 use value[index],
otherwise fall back to a safe numeric value (e.g. if value is a number use that,
else use minValue). Ensure currentValue is a finite number (coerce or clamp and
guard against NaN) before computing percent so arithmetic and ARIA attributes
never operate on undefined. Apply this logic where currentValue and percent are
computed (referencing value, index, currentValue, minValue, maxValue).

In `@src/components/ui/Steps/fragments/StepItem.tsx`:
- Around line 13-17: The variable isInactive is computed in StepItem.tsx but
never used; remove that unused const and simplify the state calculation (keep
isCompleted and isActive and derive state). Also fix the attribute/style
mismatch by making the rendered attribute and SCSS consistent: either change the
component to set data-status (instead of data-state) and produce the status
string expected by styles (e.g., use 'complete' if SCSS expects that) or update
styles/themes/components/steps.scss to target data-state and accept
'completed'/'active'/'inactive'; ensure the value naming (completed vs complete)
matches between the state variable and the stylesheet.

In `@src/components/ui/Tooltip/fragments/TooltipContent.tsx`:
- Around line 41-44: The clsx call in TooltipContent.tsx passes a duplicated
class string ('rad-ui-arrow rad-ui-arrow') to FloatingArrow; update the
className argument so it only includes a single 'rad-ui-arrow' (e.g.,
clsx('rad-ui-arrow')) or combine unique classes if another modifier was
intended, leaving the rest of the props (ref={arrowRef}, context={context}) and
conditional rendering (showArrow) unchanged.

In `@src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx`:
- Around line 48-58: hiddenIndices is only memoized on search so it won't
recompute when labelsRef.current changes; add a small version counter (e.g.,
labelsVersion state/ref) that Floater.useListItem increments whenever an item's
label/value is added/updated/removed and include that version in the useMemo
dependency array for hiddenIndices (keep search and labelsVersion in the deps).
Locate where Floater.useListItem mutates labelsRef and increment the version
there, then update the hiddenIndices useMemo to depend on search and
labelsVersion so filtering recomputes when options change.

In `@src/core/primitives/Combobox/fragments/ComboboxPrimitiveSearch.tsx`:
- Around line 11-25: The component calls useContext(ComboboxPrimitiveContext)
and immediately destructures context (refs, handleSelect, labelsRef, etc.) which
will throw if the component is rendered outside ComboboxPrimitiveRoot; add a
null/undefined guard after const context = useContext(ComboboxPrimitiveContext)
and return null (or early return JSX) when context is falsy before destructuring
to match the pattern used in ComboboxPrimitiveItem and avoid runtime errors.

In `@src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx`:
- Around line 38-49: The inline style adding outline: 'none' is being
overwritten because props (which still contains style) is spread after the
inline style in Primitive.div; fix by removing style from the spread so the
merged style takes effect: destructure style from the component props (e.g.,
const { style, ...rest } = props or similar) and then pass the merged style as
style={{ outline: 'none', ...style }} while spreading rest (not props), or
alternatively spread {...props} before the inline style so the inline style
wins; update the Primitive.div usage to reference mergedRef, getFloatingProps(),
role, aria attrs, and spread the corrected props/rest accordingly.

In `@src/test-utils/portal.ts`:
- Around line 34-39: The function assertFocusTrap no longer asserts anything
because both focus assertions are commented out; either rename assertFocusTrap
to exerciseFocusTrap to reflect that it only performs tab navigation, or restore
validation by re-enabling the expectations (document.activeElement checks) and
guard them with a jsdom-detection branch that logs a console warning when
assertions are skipped; update all references to assertFocusTrap accordingly and
consider adding/linking a tracking issue if leaving assertions disabled.

In `@styles/themes/components/combobox.scss`:
- Line 1: Remove the unused SCSS module import by deleting the line containing
`@use` 'button'; in this file (the import is never referenced anywhere in
styles/themes/components/combobox.scss), and run the style linter/formatter
afterwards to ensure no leftover unused import warnings remain.

In `@styles/themes/components/steps.scss`:
- Line 1: This file already has a file-level "/* stylelint-disable
no-descending-specificity */" at the top, so remove the redundant inline disable
comment instance "/* stylelint-disable no-descending-specificity */" found later
in the file (around the previous line 58) to avoid the pipeline error; simply
delete that duplicate inline comment and keep the single top-of-file disable.
- Around line 42-62: The SCSS selectors use data-status="active"/"complete" but
the component StepItem.tsx renders data-state="active"/"completed", so update
the selectors in steps.scss to target data-state instead of data-status and use
"completed" (not "complete") — i.e., change &[data-status="active"] and
&[data-status="complete"] to &[data-state="active"] and
&[data-state="completed"] and also update the nested selector for
.rad-ui-steps-track .rad-ui-steps-line accordingly so active and completed steps
are styled.
🧹 Nitpick comments (22)
styles/themes/components/tooltip.scss (1)

17-17: Clean up incomplete comment.

The comment reads like unfinished developer notes rather than documentation. Consider removing it or clarifying the intent.

Proposed fix
-  position: relative; // Ensure z-index works if needed, and arrow positioning context? No arrow is sibling.
+  position: relative;
styles/themes/components/menubar.scss (4)

1-1: Unused import detected.

The button module is imported but doesn't appear to be used anywhere in this file. If it's not needed, consider removing it to reduce unnecessary dependencies.

🧹 Suggested fix
-@use 'button';
-

25-25: Consider specifying explicit transition properties.

Using transition: all can cause unexpected animations and minor performance overhead. Specify only the properties that should animate.

✨ Suggested fix
-    transition: all var(--rad-ui-transition-fast);
+    transition: background-color var(--rad-ui-transition-fast), color var(--rad-ui-transition-fast);

52-54: Exit animation won't play with display: none.

Setting display: none immediately when data-state="closed" prevents any exit animation from playing. If smooth closing transitions are desired, consider using opacity/visibility or animation-based hiding instead.

✨ Alternative approach for exit animation support
     &[data-state="closed"] {
-        display: none;
+        animation: rad-ui-slide-out 0.15s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+        pointer-events: none;
     }

You would also need to add a corresponding rad-ui-slide-out keyframe:

`@keyframes` rad-ui-slide-out {
    from {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
    to {
        opacity: 0;
        transform: translateY(2px) scale(0.96);
        visibility: hidden;
    }
}

70-70: Same transition: all concern.

For consistency and performance, consider specifying explicit properties here as well.

✨ Suggested fix
-    transition: all var(--rad-ui-transition-fast);
+    transition: background-color var(--rad-ui-transition-fast), color var(--rad-ui-transition-fast);
src/core/primitives/Collapsible/fragments/CollapsiblePrimitiveContent.tsx (1)

85-91: Minor: Redundant setHeight(0) when height is already 0.

The condition checks height === 0 || height === undefined, but then unconditionally calls setHeight(0). When height is already 0, this is a no-op. Consider simplifying:

Suggested simplification
         if (open) {
             // Opening animation
             // First set height to 0 to ensure proper animation start state
             // Only if we are not already open/opening
-            if (height === 0 || height === undefined) {
+            if (height === undefined) {
                 setHeight(0);
             }
src/components/ui/Steps/fragments/StepItem.tsx (1)

20-24: Props spreading after data attributes allows unintended overrides.

Spreading {...props} after data-state and data-value means consumers can accidentally override the component's computed state. Consider spreading props before the data attributes to preserve the internal state logic.

Proposed fix
         <div
             className={clsx(`${rootClass}-item`, className)}
+            {...props}
             data-state={state}
             data-value={value}
-            {...props}
         >
src/components/ui/Steps/fragments/StepRoot.tsx (1)

2-2: Remove unused useState import.

useState is no longer used after switching to useControllableState.

Proposed fix
-import React, { useState } from 'react';
+import React from 'react';
src/components/ui/Steps/tests/Steps.test.tsx (1)

6-41: Good test coverage for core functionality.

The tests effectively verify:

  • Data-state derivation logic (completed/active/inactive) based on step values
  • Uncontrolled mode via defaultValue
  • Controlled mode via value prop
  • Custom root class composition

Consider adding tests for:

  • onValueChange callback invocation when step changes
  • orientation prop and data-orientation attribute on the root element
src/components/ui/ScrollArea/context/ScrollAreaContext.tsx (1)

11-11: Avoid using any for coordinate parameters.

clientX and clientY should be typed as number instead of any for type safety.

Suggested 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/ScrollAreaThumb.tsx (1)

67-78: Consider attaching global listeners only during drag.

Global mousemove and mouseup listeners are always attached, which adds overhead when many ScrollArea instances exist. Attaching them only in startDrag and removing them in stopDrag would be more efficient.

Suggested optimization
+    React.useEffect(() => {
+        return () => {
+            // Cleanup on unmount in case drag is in progress
+            document.body.style.cursor = '';
+            document.body.style.userSelect = '';
+        };
+    }, []);

-    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);
-        };
-    }, [handleDrag, stopDrag]);

Then in startDrag:

const startDrag = useCallback((e: React.MouseEvent) => {
    // ... existing code ...
    
    const handleMouseMove = (e: MouseEvent) => handleDrag(e);
    const handleMouseUp = () => {
        stopDrag();
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
    };
    
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
}, [scrollAreaViewportRef, handleDrag, stopDrag]);
src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx (3)

32-36: Consider using a type-safe ref merging utility.

The ref merging logic works but uses any casts. A cleaner approach would be to use a utility function or a library like @radix-ui/react-compose-refs.

♻️ Suggested improvement
+import { useCallback } from 'react';
+
+const mergeRefs = <T,>(...refs: (React.Ref<T> | undefined)[]) => {
+    return (node: T | null) => {
+        refs.forEach((ref) => {
+            if (typeof ref === 'function') {
+                ref(node);
+            } else if (ref != null) {
+                (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);

38-41: Refs in dependency array are misleading.

Refs are stable and don't trigger re-renders. This effect will only run once on mount, which may be intentional. If so, use an empty dependency array to clarify intent.

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

191-211: Consider memoizing the context value to prevent unnecessary re-renders.

The context value object is recreated on every render, which can cause all consumers to re-render even when values haven't changed.

♻️ Suggested improvement
+    const contextValue = React.useMemo(() => ({
+        rootClass,
+        scrollYThumbRef,
+        scrollXThumbRef,
+        scrollAreaViewportRef,
+        handleScroll,
+        handleScrollbarClick,
+        type,
+        rootRef: internalRootRef
+    }), [rootClass, type, handleScroll, handleScrollbarClick]);
+
     return (
-        <ScrollAreaContext.Provider value={{ 
-            rootClass, 
-            scrollYThumbRef, 
-            scrollXThumbRef, 
-            scrollAreaViewportRef, 
-            handleScroll, 
-            handleScrollbarClick, 
-            type, 
-            rootRef: internalRootRef 
-        }}>
+        <ScrollAreaContext.Provider value={contextValue}>

Note: You'll also need to wrap handleScroll and handleScrollbarClick in useCallback for this optimization to be effective.

src/components/ui/AlertDialog/tests/AlertDialog.a11y.test.tsx (1)

112-113: Multiple focus trap assertions disabled - consider tracking resolution.

Disabling these focus-related assertions is understandable given jsdom limitations, and other critical a11y tests (axe violations, data-state attributes, some focus scenarios) remain active. However, focus trapping is a core accessibility requirement for alert dialogs.

Consider creating a tracking issue to:

  1. Document which assertions are disabled and why
  2. Explore alternatives (e.g., Playwright/Cypress for focus tests in a real browser)
  3. Re-enable once jsdom or the underlying library improves

This ensures these gaps aren't forgotten.

Would you like me to open an issue to track re-enabling these focus trap assertions?

Also applies to: 191-202

src/components/ui/Dialog/tests/Dialog.portal.test.tsx (1)

47-48: Consider tracking these FIXMEs for future resolution.

The disabled focus restoration assertions are reasonable given jsdom limitations. To ensure these don't get lost, consider opening an issue to investigate alternatives (e.g., testing with a more complete DOM implementation or mocking focus behavior).

Would you like me to open an issue to track the jsdom focus restoration flakiness?

Also applies to: 55-56

src/setupTests.ts (1)

46-61: Polyfills are appropriate for jsdom environment.

The ResizeObserver and PointerEvent polyfills address known jsdom limitations. The window existence check provides SSR safety.

Minor type safety improvement could replace @ts-ignore with explicit typing:

♻️ Optional type-safe alternative
     // PointerEvent
-    // `@ts-ignore`
-    if (!window.PointerEvent) {
-        // `@ts-ignore`
-        window.PointerEvent = MouseEvent;
-    }
+    if (!('PointerEvent' in window)) {
+        (window as unknown as { PointerEvent: typeof MouseEvent }).PointerEvent = MouseEvent;
+    }
src/components/ui/Accordion/tests/Accordion.dynamic.test.tsx (1)

45-55: Consider using waitFor or findBy for more robust async handling.

The manual setTimeout approach works but is less idiomatic than RTL's built-in async utilities, which handle timing more reliably:

Suggested improvement using waitFor
-        // Wait for items to update
-        await act(async () => {
-            await new Promise(resolve => setTimeout(resolve, 100));
-        });
-
-        // The content height should have updated or at least not be broken.
-        // Since we mocked ResizeObserver, we can't easily test the actual offsetHeight 
-        // in JSDOM, but we can verify that the component didn't crash and the new items are rendered.
-        expect(screen.getByText('Item 1')).toBeInTheDocument();
-        expect(screen.getByText('Item 2')).toBeInTheDocument();
-        expect(screen.getByText('Item 3')).toBeInTheDocument();
+        // Wait for dynamic items to render
+        expect(await screen.findByText('Item 2')).toBeInTheDocument();
+        expect(screen.getByText('Item 1')).toBeInTheDocument();
+        expect(screen.getByText('Item 3')).toBeInTheDocument();
src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx (1)

98-102: Potential stale closure in selectedItemRef effect.

The effect depends on isSelected and hasSearch, but itemRef.current may not be set when this effect runs on initial mount. Consider adding itemRef to dependencies or using a callback ref pattern.

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

147-190: Context value memoization looks correct.

The expanded dependency list properly includes the new search-related state values. However, note that refs, getFloatingProps, getReferenceProps, getItemProps, and floatingStyles are recreated on each render by Floating UI, which may cause unnecessary context updates.

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

51-51: Style spread order may unintentionally override hide behavior.

The current spread order { display: shouldHide ? 'none' : undefined, ...props.style } allows consumer-provided styles to override the display: none when the group should be hidden.

If this is unintentional, swap the order:

Suggested fix
-                style={{ display: shouldHide ? 'none' : undefined, ...props.style }}
+                style={{ ...props.style, display: shouldHide ? 'none' : undefined }}

Alternatively, if consumers should be able to override the hide behavior, this is fine as-is.

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

54-60: Prefer useCallback for ref callbacks.

While useMemo works, useCallback is the idiomatic choice for memoized callbacks and makes the intent clearer.

Suggested refactor
-    const mergedRef = React.useMemo(() => {
-        return (node: HTMLDivElement | null) => {
+    const mergedRef = React.useCallback((node: HTMLDivElement | null) => {
             (internalRef as any).current = node;
             if (typeof ref === 'function') ref(node);
             else if (ref) (ref as any).current = node;
-        };
-    }, [ref]);
+    }, [ref]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82d2ba3 and 6fc42a5.

⛔ Files ignored due to path filters (1)
  • src/components/ui/Slider/tests/__snapshots__/Slider.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (40)
  • src/components/ui/Accordion/fragments/AccordionHeader.tsx
  • src/components/ui/Accordion/tests/Accordion.dynamic.test.tsx
  • src/components/ui/Accordion/tests/Accordion.test.tsx
  • src/components/ui/AlertDialog/tests/AlertDialog.a11y.test.tsx
  • src/components/ui/Combobox/tests/Combobox.filtering.test.tsx
  • src/components/ui/Combobox/tests/Combobox.simple.test.tsx
  • src/components/ui/Dialog/tests/Dialog.portal.test.tsx
  • src/components/ui/ScrollArea/context/ScrollAreaContext.tsx
  • src/components/ui/ScrollArea/fragments/ScrollAreaCorner.tsx
  • src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx
  • src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx
  • src/components/ui/ScrollArea/fragments/ScrollAreaThumb.tsx
  • src/components/ui/ScrollArea/stories/ScrollArea.stories.tsx
  • src/components/ui/Slider/Slider.tsx
  • src/components/ui/Slider/context/SliderContext.tsx
  • src/components/ui/Slider/fragments/SliderRange.tsx
  • src/components/ui/Slider/fragments/SliderRoot.tsx
  • src/components/ui/Slider/fragments/SliderThumb.tsx
  • src/components/ui/Slider/tests/Slider.interaction.test.tsx
  • src/components/ui/Slider/tests/Slider.range.test.tsx
  • src/components/ui/Steps/fragments/StepItem.tsx
  • src/components/ui/Steps/fragments/StepRoot.tsx
  • src/components/ui/Steps/tests/Steps.test.tsx
  • src/components/ui/Tooltip/fragments/TooltipContent.tsx
  • src/core/primitives/Collapsible/fragments/CollapsiblePrimitiveContent.tsx
  • src/core/primitives/Combobox/contexts/ComboboxGroupContext.tsx
  • src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveGroup.tsx
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx
  • src/core/primitives/Combobox/fragments/ComboboxPrimitiveSearch.tsx
  • src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx
  • src/setupTests.ts
  • src/test-utils/portal.ts
  • styles/themes/components/combobox.scss
  • styles/themes/components/menubar.scss
  • styles/themes/components/scroll-area.scss
  • styles/themes/components/steps.scss
  • styles/themes/components/tooltip.scss
  • styles/themes/default.scss
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-04-07T04:38:34.864Z
Learnt from: kotAPI
Repo: rad-ui/ui PR: 1031
File: src/components/ui/Accordion/fragments/AccordionRoot.tsx:41-44
Timestamp: 2025-04-07T04:38:34.864Z
Learning: The Accordion component in rad-ui/ui supports both controlled and uncontrolled modes through props like `value`, `defaultValue`, and `onValueChange`. When implementing controlled components, remember to: 1) Initialize state from defaultValue, 2) Update internal state when value changes (controlled mode), 3) Call onValueChange callback, and 4) Prevent internal state updates when in controlled mode.

Applied to files:

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

Applied to files:

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

Applied to files:

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

Applied to files:

  • src/components/ui/Slider/fragments/SliderRoot.tsx
  • src/components/ui/Steps/fragments/StepRoot.tsx
🧬 Code graph analysis (15)
src/components/ui/Slider/fragments/SliderRange.tsx (1)
src/components/ui/Slider/context/SliderContext.tsx (1)
  • SliderContext (20-35)
src/components/ui/Accordion/tests/Accordion.dynamic.test.tsx (2)
docs/app/docs/components/accordion/docs/anatomy.tsx (1)
  • Accordion (3-14)
rollup.config.cjs (1)
  • resolve (1-1)
src/components/ui/ScrollArea/fragments/ScrollAreaThumb.tsx (1)
src/components/ui/ScrollArea/context/ScrollAreaContext.tsx (1)
  • ScrollAreaContext (16-19)
src/components/ui/ScrollArea/fragments/ScrollAreaCorner.tsx (1)
src/components/ui/ScrollArea/context/ScrollAreaContext.tsx (1)
  • ScrollAreaContext (16-19)
src/components/ui/Slider/fragments/SliderThumb.tsx (2)
src/components/ui/Slider/context/SliderContext.tsx (1)
  • SliderContext (20-35)
scripts/generate-exports.cjs (1)
  • name (74-74)
src/components/ui/Steps/fragments/StepItem.tsx (1)
src/components/ui/Steps/context/StepsContext.tsx (1)
  • useStepsContext (14-20)
src/components/ui/Dialog/tests/Dialog.portal.test.tsx (1)
test-utils/index.ts (1)
  • axe (56-62)
src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx (1)
src/components/ui/ScrollArea/context/ScrollAreaContext.tsx (1)
  • ScrollAreaContext (16-19)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveGroup.tsx (2)
src/core/primitives/Combobox/contexts/ComboboxPrimitiveContext.tsx (1)
  • ComboboxPrimitiveContext (37-37)
src/core/primitives/Combobox/contexts/ComboboxGroupContext.tsx (1)
  • ComboboxGroupContext (7-7)
src/components/ui/Accordion/fragments/AccordionHeader.tsx (2)
src/components/ui/Accordion/Accordion.tsx (1)
  • AccordionHeaderProps (17-17)
src/components/ui/Accordion/contexts/AccordionContext.tsx (1)
  • AccordionContext (13-21)
src/components/ui/Slider/fragments/SliderRoot.tsx (1)
src/core/hooks/useControllableState/index.tsx (1)
  • useControllableState (15-78)
src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (1)
src/components/ui/ScrollArea/context/ScrollAreaContext.tsx (1)
  • ScrollAreaContext (16-19)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx (1)
src/core/primitives/Combobox/contexts/ComboboxGroupContext.tsx (1)
  • useComboboxGroupContext (9-9)
src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx (3)
docs/components/Main/NavBar/index.js (1)
  • useContext (11-11)
src/core/primitives/Dialog/context/DialogPrimitiveContext.tsx (1)
  • DialogPrimitiveContext (19-33)
src/examples/FloatingUI/useInteractions/UseInteractionsExample.js (2)
  • isOpen (6-6)
  • role (16-18)
src/components/ui/Steps/fragments/StepRoot.tsx (2)
src/core/hooks/useControllableState/index.tsx (1)
  • useControllableState (15-78)
src/core/index.ts (1)
  • customClassSwitcher (4-4)
🪛 Biome (2.1.2)
src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx

[error] 44-44: 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] 45-45: 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] 54-54: 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] 57-57: 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] 64-64: 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] 72-72: 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] 98-98: 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] 106-106: 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)

🪛 GitHub Actions: Lint
styles/themes/components/steps.scss

[error] 58-58: stylelint: 'no-descending-specificity' has already been disabled. CssSyntaxError

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: coverage
🔇 Additional comments (49)
styles/themes/components/tooltip.scss (1)

1-33: LGTM on the tooltip styling module.

The separation of concerns between the floating element (positioning) and inner content (visual styling) is well-structured. The animation is smooth with appropriate will-change hints.

styles/themes/default.scss (1)

49-50: LGTM on the tooltip module import.

The import correctly integrates the new dedicated tooltip styling module, replacing the legacy inline styles.

styles/themes/components/menubar.scss (3)

3-11: LGTM!

The root container styling is clean and follows good practices with CSS custom properties for theming.


84-88: LGTM!

Simple and clean separator styling.


90-99: LGTM!

The slide-in animation is well-crafted with appropriate values for a smooth, subtle entrance effect.

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

20-35: Controllable state implementation looks correct.

The use of useControllableState follows the established pattern from learnings for supporting both controlled and uncontrolled modes. The hook handles the internal/external state synchronization, warnings for mode switching, and onChange callbacks appropriately. The data-orientation attribute is a good addition for styling and accessibility.

src/components/ui/ScrollArea/fragments/ScrollAreaCorner.tsx (1)

3-12: LGTM!

Clean implementation using context for consistent class naming. The clsx composition and ref forwarding are correctly implemented.

styles/themes/components/scroll-area.scss (1)

20-71: LGTM! Well-structured orientation-aware scrollbar styling.

The implementation correctly handles both vertical and horizontal orientations with appropriate positioning. The 44px minimum hit area on the thumb pseudo-element aligns with touch accessibility guidelines.

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

5-19: Context type and defaults are well-defined.

The expanded context type with orientation support and new refs enables the dual-axis scrollbar functionality cleanly.

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

75-247: Good story coverage for the new ScrollArea features.

The new stories comprehensively demonstrate all type variations (auto, always, scroll, hover), both orientations, and composition with other components like Card.

src/components/ui/ScrollArea/fragments/ScrollAreaThumb.tsx (2)

35-59: Drag handling logic is well-implemented.

The orientation-aware drag calculation correctly computes scroll ratios and clamps values within bounds for both vertical and horizontal axes.


80-90: Type assertion for ref assignment is acceptable but worth noting.

The cast from RefObject to MutableRefObject is a common pattern when combining context-provided refs with forwardRef. This works but relies on the context actually providing mutable refs.

src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx (3)

74-101: LGTM!

The thumb sizing logic correctly handles both axes with appropriate minimum size constraints and safely updates the overflow state.


130-160: LGTM!

The animated scrolling implementation with cubic easing is clean and handles both axes appropriately.


162-189: LGTM!

The click-based scrolling logic correctly handles both orientations with appropriate bounds checking.

src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (5)

12-23: LGTM!

The component signature and state initialization correctly set up the visibility management based on the type prop.


71-88: LGTM!

The overflow detection correctly uses ResizeObserver and observes both the viewport and its children for dynamic content changes.


90-101: LGTM!

The scroll continuation logic correctly determines whether the mouse position is still outside the thumb bounds based on orientation.


103-137: LGTM!

The continuous scrolling implementation with initial click and delayed interval is well-structured with proper orientation handling.


139-149: LGTM!

The cleanup function properly resets all scrolling state and clears the interval.

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

6-11: Minimal smoke test - consider expanding coverage.

This test validates basic rendering, which is a reasonable starting point. Given the AI summary mentions other Combobox tests (filtering, dynamic) provide broader coverage, this simple test is acceptable as a quick sanity check.

Consider adding at least one interaction test (e.g., opening the dropdown) if not covered elsewhere to ensure the component integrates correctly.

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

32-34: LGTM!

The new context properties (search, setSearch, hiddenIndices) are well-typed and align with the search-driven filtering behavior described in the PR. The simpler setSearch signature (vs React.Dispatch) provides flexibility for controlled search implementations.

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

1-9: LGTM!

Clean and well-typed context implementation. The null default value correctly signals that the context is optional, aligning with the hook returning null when used outside a provider.

src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx (1)

32-37: Verify initialFocus={0} behavior with Floater.FocusManager.

initialFocus={0} sets focus to the first tabbable element. Confirm this is the intended behavior for all dialog use cases, as some dialogs may need focus on a specific element (e.g., a close button or primary action).

src/components/ui/Dialog/tests/Dialog.portal.test.tsx (1)

103-109: Well-documented axe rule exclusion.

The comment clearly explains why aria-command-name is disabled. Since this is an upstream Floating UI implementation detail (focus guards with role="button" but no accessible name), this is a reasonable workaround.

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

54-75: Good accessibility and animation patterns.

The content styling properly handles visibility with data-state, includes focus rings, and uses a smooth animation. The glass background effect with backdrop-filter provides a modern look.

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

47-47: LGTM! Type updates align with AccordionHeader rendering change.

The ref type and assertion correctly reflect that AccordionHeader now renders an <h3> element instead of a <div>, improving semantic HTML structure for accessibility.

Also applies to: 68-68

src/components/ui/Slider/tests/Slider.interaction.test.tsx (1)

67-67: Acceptable type narrowing for single-value test scenario.

The cast v as number is appropriate here since this test explicitly uses a single-thumb slider. For production code, a runtime check would be preferable, but for test files this pattern is acceptable.

src/components/ui/Combobox/tests/Combobox.filtering.test.tsx (2)

6-44: Good coverage of core filtering behavior.

The test validates the primary filtering flow: initial visibility, search-based filtering, and group hiding. Consider adding tests for edge cases in a follow-up (e.g., clearing search, empty search string, case sensitivity).


37-42: The test assertions are correct and will not fail.

The filtering implementation uses CSS display: none to hide items rather than removing them from the DOM (ComboboxPrimitiveItem.tsx line 110 and ComboboxPrimitiveGroup.tsx line 51). Because elements remain in the DOM when hidden, screen.queryByText() will successfully find them, and not.toBeVisible() will correctly assert they are not visible. The test is safe as written.

src/components/ui/Slider/context/SliderContext.tsx (1)

5-6: LGTM! Clean API expansion for range slider support.

The type widening from number to number | number[] enables multi-thumb (range) sliders while maintaining backward compatibility with single-value usage. Default context values remain valid.

src/components/ui/Accordion/tests/Accordion.dynamic.test.tsx (1)

24-35: Test structure looks good for validating dynamic content handling.

The test properly exercises the accordion with dynamically growing content, verifies initial rendering from defaultValue, and confirms the component doesn't crash when content updates. The inline comment acknowledging JSDOM limitations is helpful documentation.

src/components/ui/Slider/tests/Slider.range.test.tsx (4)

6-8: LGTM on the PointerEvent polyfill.

Standard pattern for enabling pointer events in jsdom test environments.


11-27: LGTM on the dual-thumb rendering test.

Good coverage for verifying that both thumbs render with correct aria-valuenow attributes.


29-61: Second assertion may fail due to stale component state.

The test fires two pointerDown events sequentially without re-rendering. After the first click at clientX: 10, the component's internal state updates to [10, 80]. However, the second assertion at line 60 expects [10, 90], which assumes the first update persisted. Since defaultValue is used (uncontrolled mode) and onValueChange is mocked, the component's internal state does update, but verify that the mock captures independent calls rather than cumulative state.

If the intent is to test cumulative updates, consider using controlled mode with state, or use toHaveBeenLastCalledWith for the second assertion.


63-77: LGTM on the range rendering test.

Correctly validates the visual positioning of the range element between two thumbs.

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

30-39: LGTM on the lifecycle management.

Properly sets hasSearch on mount, resets activeIndex, and cleans up both hasSearch and search on unmount.


44-65: LGTM on the controlled input binding.

The input is correctly bound to context-provided search state with proper event handling.

src/components/ui/Slider/fragments/SliderThumb.tsx (2)

64-72: LGTM on the array value update logic.

The approach of updating the specific index without sorting is a valid design choice to prevent thumb hopping during keyboard navigation. The comment documents the intent well.


119-124: LGTM on the hidden input handling.

The naming convention name[index] aligns with standard form array field patterns for multi-thumb values.

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

72-95: Disabled indices effect has inverted early-return logic.

Lines 75-76 return prev unchanged when the item is disabled and already in the list, or when enabled and not in the list. This is correct for avoiding no-op state updates.

However, verify this doesn't cause issues when items re-render with the same disabled state - the cleanup function (lines 87-94) will always try to remove the index on unmount regardless of disabled state, which is correct.

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

60-63: LGTM on totalDisabledIndices computation.

Correctly merges disabled and hidden indices into a sorted, deduplicated array for navigation exclusion.

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

36-55: LGTM!

The orientation-aware styling correctly positions the range indicator using percentage-based values for both horizontal and vertical modes.

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

12-27: LGTM!

The type definitions correctly support both single-value and array-based (range) modes. The Omit on line 27 properly prevents conflicts between the custom value props and the native div element props.


52-72: Default implementation only supports single-value mode.

The default render structure creates a single Slider.Thumb, but the props now accept number[] for range mode. If a user passes defaultValue={[20, 80]} to this component, only one thumb will render, causing unexpected behavior.

Consider either:

  1. Dynamically rendering thumbs based on the value array length, or
  2. Adding a runtime warning when array values are passed to the simple Slider component

Is the expectation that users must use Slider.Root with manually composed Slider.Thumb elements for range mode? If so, documenting this distinction would help prevent confusion.

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

155-170: LGTM!

The context values correctly expose all necessary state and callbacks for child components. The value/setValue pair properly supports both single and array modes as defined in the context type.


172-188: LGTM!

The root element correctly uses the merged ref, applies orientation and disabled data attributes, and spreads remaining props for extensibility.


112-120: The data-index attribute is already properly set by SliderThumb on line 103 of SliderThumb.tsx as data-index={index}, which correctly supports both single-value and multi-thumb scenarios. No changes needed.

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

17-38: The cleanup safeguard prevents the race condition described.

The registerItem function does call on every visibility change (via the isHidden dependency in ComboboxPrimitiveItem's useEffect), but the cleanup function at line 32 includes a safeguard check: if (!prev.has(id)) return prev;. This prevents the stale removal scenario you described—if cleanup runs at any point, it safely returns without removing the item if it's no longer in the Set.

React's effect cleanup semantics ensure the old cleanup runs before the new effect executes, and even if ordering were different, the safeguard makes the operation idempotent. The code is safe as written.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 6 to +11
export type AccordionHeaderProps = React.ComponentPropsWithoutRef<'div'>;

const AccordionHeader = React.forwardRef<React.ElementRef<'div'>, AccordionHeaderProps>(({ children, className = '', ...props }, ref) => {
const AccordionHeader = React.forwardRef<React.ElementRef<'h3'>, AccordionHeaderProps>(({ children, className = '', ...props }, ref) => {
const { rootClass } = useContext(AccordionContext);
return (
<div ref={ref} className={clsx(`${rootClass}-header`, className)} {...props}>
<h3 ref={ref} className={clsx(`${rootClass}-header`, className)} {...props}>
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:

# First, locate the AccordionHeader.tsx file
find . -name "AccordionHeader.tsx" -type f

Repository: rad-ui/ui

Length of output: 112


🏁 Script executed:

# Search for usages of AccordionHeaderProps
rg "AccordionHeaderProps" --type=ts --type=tsx -n

Repository: rad-ui/ui

Length of output: 80


🏁 Script executed:

# Check the Accordion module structure and exports
fd "Accordion" --type f -e ts -e tsx | grep -E "(index|Accordion\.tsx)" | head -20

Repository: rad-ui/ui

Length of output: 94


🏁 Script executed:

# Read the AccordionHeader.tsx file
cat -n ./src/components/ui/Accordion/fragments/AccordionHeader.tsx

Repository: rad-ui/ui

Length of output: 831


🏁 Script executed:

# Search for AccordionHeaderProps without type filter
rg "AccordionHeaderProps" -n

Repository: rad-ui/ui

Length of output: 502


🏁 Script executed:

# Check the main Accordion.tsx exports
cat -n ./src/components/ui/Accordion/Accordion.tsx

Repository: rad-ui/ui

Length of output: 1615


🏁 Script executed:

# Search for AccordionHeaderProps usage in stories and tests
rg "AccordionHeaderProps" -A3 -B3 | grep -v "fragments/AccordionHeader.tsx" | grep -v "Accordion.tsx"

Repository: rad-ui/ui

Length of output: 55


🏁 Script executed:

# Check if AccordionHeader is used in Storybook stories
find . -name "*.stories.*" -type f | xargs grep -l "AccordionHeader" 2>/dev/null

Repository: rad-ui/ui

Length of output: 37


🏁 Script executed:

# Find all story files
fd "\.stories\." -type f

Repository: rad-ui/ui

Length of output: 223


🏁 Script executed:

# Search for Accordion usage in story files
rg "Accordion\.Header" -l

Repository: rad-ui/ui

Length of output: 486


🏁 Script executed:

# Check how AccordionHeader is used in stories
cat -n ./src/components/ui/Accordion/stories/Accordion.stories.tsx

Repository: rad-ui/ui

Length of output: 5945


🏁 Script executed:

# Check the test files to understand usage
head -100 ./src/components/ui/Accordion/tests/Accordion.test.tsx

Repository: rad-ui/ui

Length of output: 4108


Fix the props type to match the rendered heading element.

AccordionHeaderProps is typed as ComponentPropsWithoutRef<'div'> but the component renders an <h3>. The ref type is already correctly set to React.ElementRef<'h3'>, so the props type should match.

🔧 Proposed fix
-export type AccordionHeaderProps = React.ComponentPropsWithoutRef<'div'>;
+export type AccordionHeaderProps = React.ComponentPropsWithoutRef<'h3'>;
🤖 Prompt for AI Agents
In `@src/components/ui/Accordion/fragments/AccordionHeader.tsx` around lines 6 -
11, AccordionHeaderProps is incorrectly typed as
React.ComponentPropsWithoutRef<'div'> while AccordionHeader renders an <h3> and
uses React.ElementRef<'h3'> for the ref; change AccordionHeaderProps to
React.ComponentPropsWithoutRef<'h3'> so props and intrinsic element match,
update any related imports/types if needed, and ensure the forwardRef signature
in AccordionHeader remains React.forwardRef<React.ElementRef<'h3'>,
AccordionHeaderProps>.

Comment on lines +6 to +11
// Mock ResizeObserver
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
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:

rg -n "ResizeObserver" --type=ts --type=tsx -B2 -A5

Repository: rad-ui/ui

Length of output: 80


🏁 Script executed:

rg -n "ResizeObserver" -B2 -A5

Repository: rad-ui/ui

Length of output: 7105


Remove the duplicate ResizeObserver mock from this test file.

The global mock in setupTests.ts already provides a ResizeObserver implementation that applies to all tests, making the local mock at lines 6-11 redundant. This local mock redefines the same global object, which can cause maintenance issues and divergence if implementations differ across test files.

🤖 Prompt for AI Agents
In `@src/components/ui/Accordion/tests/Accordion.dynamic.test.tsx` around lines 6
- 11, Remove the local ResizeObserver mock in Accordion.dynamic.test.tsx because
a global ResizeObserver mock is already provided in setupTests.ts; locate and
delete the global.ResizeObserver = jest.fn().mockImplementation(...) block (the
observe/unobserve/disconnect mocks) so the test uses the shared implementation
instead of redefining ResizeObserver locally.

Comment on lines 103 to 128
const handleScroll = () => {
// The full height of all the content inside the scrollable element [Visible Area Height]
const scrollAreaContainerHeight = scrollAreaViewportRef.current?.clientHeight || 0;

// The full height of all the content inside the scrollable element [Total Content Height]
const scrollAreaHeight = scrollAreaViewportRef.current?.scrollHeight || 0;

// The current scroll position of the scrollable element [Scroll Position]
const scrollTopPosition = scrollAreaViewportRef.current?.scrollTop || 0;
if (!scrollAreaViewportRef.current) return;
const viewport = scrollAreaViewportRef.current;

// The height of the scroll thumb [Scroll Thumb Height]
const scrollThumbHeight = scrollXThumbRef.current?.clientHeight || 0;
// Vertical
if (scrollYThumbRef.current) {
const viewportHeight = viewport.clientHeight;
const contentHeight = viewport.scrollHeight;
const scrollTop = viewport.scrollTop;
const thumbHeight = scrollYThumbRef.current.clientHeight;

const scrollThumbPosition = (scrollTopPosition / (scrollAreaHeight - scrollAreaContainerHeight)) * (scrollAreaContainerHeight - scrollThumbHeight);
const thumbPosition = (scrollTop / (contentHeight - viewportHeight)) * (viewportHeight - thumbHeight);
scrollYThumbRef.current.style.top = `${thumbPosition}px`;
}

// Horizontal
if (scrollXThumbRef.current) {
scrollXThumbRef.current.style.top = `${scrollThumbPosition}px`;
const viewportWidth = viewport.clientWidth;
const contentWidth = viewport.scrollWidth;
const scrollLeft = viewport.scrollLeft;
const thumbWidth = scrollXThumbRef.current.clientWidth;

const thumbPosition = (scrollLeft / (contentWidth - viewportWidth)) * (viewportWidth - thumbWidth);
scrollXThumbRef.current.style.left = `${thumbPosition}px`;
}
};
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

Guard against division by zero when content fits exactly.

If contentHeight === viewportHeight or contentWidth === viewportWidth, the division results in NaN, setting invalid CSS values like NaNpx.

🐛 Suggested fix
     const handleScroll = () => {
         if (!scrollAreaViewportRef.current) return;
         const viewport = scrollAreaViewportRef.current;

         // Vertical
         if (scrollYThumbRef.current) {
             const viewportHeight = viewport.clientHeight;
             const contentHeight = viewport.scrollHeight;
             const scrollTop = viewport.scrollTop;
             const thumbHeight = scrollYThumbRef.current.clientHeight;
+            const scrollableHeight = contentHeight - viewportHeight;
 
-            const thumbPosition = (scrollTop / (contentHeight - viewportHeight)) * (viewportHeight - thumbHeight);
+            const thumbPosition = scrollableHeight > 0
+                ? (scrollTop / scrollableHeight) * (viewportHeight - thumbHeight)
+                : 0;
             scrollYThumbRef.current.style.top = `${thumbPosition}px`;
         }

         // Horizontal
         if (scrollXThumbRef.current) {
             const viewportWidth = viewport.clientWidth;
             const contentWidth = viewport.scrollWidth;
             const scrollLeft = viewport.scrollLeft;
             const thumbWidth = scrollXThumbRef.current.clientWidth;
+            const scrollableWidth = contentWidth - viewportWidth;

-            const thumbPosition = (scrollLeft / (contentWidth - viewportWidth)) * (viewportWidth - thumbWidth);
+            const thumbPosition = scrollableWidth > 0
+                ? (scrollLeft / scrollableWidth) * (viewportWidth - thumbWidth)
+                : 0;
             scrollXThumbRef.current.style.left = `${thumbPosition}px`;
         }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleScroll = () => {
// The full height of all the content inside the scrollable element [Visible Area Height]
const scrollAreaContainerHeight = scrollAreaViewportRef.current?.clientHeight || 0;
// The full height of all the content inside the scrollable element [Total Content Height]
const scrollAreaHeight = scrollAreaViewportRef.current?.scrollHeight || 0;
// The current scroll position of the scrollable element [Scroll Position]
const scrollTopPosition = scrollAreaViewportRef.current?.scrollTop || 0;
if (!scrollAreaViewportRef.current) return;
const viewport = scrollAreaViewportRef.current;
// The height of the scroll thumb [Scroll Thumb Height]
const scrollThumbHeight = scrollXThumbRef.current?.clientHeight || 0;
// Vertical
if (scrollYThumbRef.current) {
const viewportHeight = viewport.clientHeight;
const contentHeight = viewport.scrollHeight;
const scrollTop = viewport.scrollTop;
const thumbHeight = scrollYThumbRef.current.clientHeight;
const scrollThumbPosition = (scrollTopPosition / (scrollAreaHeight - scrollAreaContainerHeight)) * (scrollAreaContainerHeight - scrollThumbHeight);
const thumbPosition = (scrollTop / (contentHeight - viewportHeight)) * (viewportHeight - thumbHeight);
scrollYThumbRef.current.style.top = `${thumbPosition}px`;
}
// Horizontal
if (scrollXThumbRef.current) {
scrollXThumbRef.current.style.top = `${scrollThumbPosition}px`;
const viewportWidth = viewport.clientWidth;
const contentWidth = viewport.scrollWidth;
const scrollLeft = viewport.scrollLeft;
const thumbWidth = scrollXThumbRef.current.clientWidth;
const thumbPosition = (scrollLeft / (contentWidth - viewportWidth)) * (viewportWidth - thumbWidth);
scrollXThumbRef.current.style.left = `${thumbPosition}px`;
}
};
const handleScroll = () => {
if (!scrollAreaViewportRef.current) return;
const viewport = scrollAreaViewportRef.current;
// Vertical
if (scrollYThumbRef.current) {
const viewportHeight = viewport.clientHeight;
const contentHeight = viewport.scrollHeight;
const scrollTop = viewport.scrollTop;
const thumbHeight = scrollYThumbRef.current.clientHeight;
const scrollableHeight = contentHeight - viewportHeight;
const thumbPosition = scrollableHeight > 0
? (scrollTop / scrollableHeight) * (viewportHeight - thumbHeight)
: 0;
scrollYThumbRef.current.style.top = `${thumbPosition}px`;
}
// Horizontal
if (scrollXThumbRef.current) {
const viewportWidth = viewport.clientWidth;
const contentWidth = viewport.scrollWidth;
const scrollLeft = viewport.scrollLeft;
const thumbWidth = scrollXThumbRef.current.clientWidth;
const scrollableWidth = contentWidth - viewportWidth;
const thumbPosition = scrollableWidth > 0
? (scrollLeft / scrollableWidth) * (viewportWidth - thumbWidth)
: 0;
scrollXThumbRef.current.style.left = `${thumbPosition}px`;
}
};

Comment on lines +49 to +69
// Handle hover visibility
React.useEffect(() => {
if (type !== 'hover') return;
const root = rootRef?.current;
if (!root) return;

const handleMouseEnter = () => setVisible(true);
const handleMouseLeave = () => {
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = setTimeout(() => {
setVisible(false);
}, 500);
};

root.addEventListener('mouseenter', handleMouseEnter);
root.addEventListener('mouseleave', handleMouseLeave);
return () => {
root.removeEventListener('mouseenter', handleMouseEnter);
root.removeEventListener('mouseleave', handleMouseLeave);
};
}, [type, rootRef]);
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

Race condition: mouse enter doesn't cancel pending hide timeout.

If the user quickly leaves and re-enters the scroll area, the pending hide timeout from mouseleave will still fire and hide the scrollbar, even though the mouse is now inside.

🐛 Suggested fix
         const handleMouseEnter = () => {
+            if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
             setVisible(true);
         };
📝 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
// Handle hover visibility
React.useEffect(() => {
if (type !== 'hover') return;
const root = rootRef?.current;
if (!root) return;
const handleMouseEnter = () => setVisible(true);
const handleMouseLeave = () => {
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = setTimeout(() => {
setVisible(false);
}, 500);
};
root.addEventListener('mouseenter', handleMouseEnter);
root.addEventListener('mouseleave', handleMouseLeave);
return () => {
root.removeEventListener('mouseenter', handleMouseEnter);
root.removeEventListener('mouseleave', handleMouseLeave);
};
}, [type, rootRef]);
// Handle hover visibility
React.useEffect(() => {
if (type !== 'hover') return;
const root = rootRef?.current;
if (!root) return;
const handleMouseEnter = () => {
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
setVisible(true);
};
const handleMouseLeave = () => {
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = setTimeout(() => {
setVisible(false);
}, 500);
};
root.addEventListener('mouseenter', handleMouseEnter);
root.addEventListener('mouseleave', handleMouseLeave);
return () => {
root.removeEventListener('mouseenter', handleMouseEnter);
root.removeEventListener('mouseleave', handleMouseLeave);
};
}, [type, rootRef]);
🤖 Prompt for AI Agents
In `@src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx` around lines
49 - 69, The mouseenter handler doesn't clear an existing hide timeout, so a
pending hide from handleMouseLeave can still fire after re-entering; update
handleMouseEnter (used in the useEffect) to clear hideTimeoutRef.current before
calling setVisible(true) (and ensure hideTimeoutRef is the same ref used in
handleMouseLeave), so re-entering cancels any pending hide timer and prevents
the race condition.

}, []);

// Attach global mouse listeners only while actively scrolling
const shouldRender = type === 'always' || (type === 'auto' && isOverflowing) || (isOverflowing && visible);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused variable shouldRender.

This variable is defined but never used. Line 179 defines isVisible with identical logic, which is the one actually used.

🧹 Suggested fix
-    const shouldRender = type === 'always' || (type === 'auto' && isOverflowing) || (isOverflowing && visible);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const shouldRender = type === 'always' || (type === 'auto' && isOverflowing) || (isOverflowing && visible);
🤖 Prompt for AI Agents
In `@src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx` at line 151,
The variable shouldRender is declared with the same logic as isVisible but never
used; remove the unused declaration of shouldRender to avoid dead code and keep
only isVisible (used later). Locate the const shouldRender = ... line and delete
it, ensuring no other references to shouldRender remain; if any exist, replace
them with isVisible.

Comment on lines +38 to +49
<Primitive.div
ref={mergedRef}
asChild={asChild}
{...getFloatingProps()}
style={{ outline: 'none', ...props.style }}
role={role}
aria-labelledby={isOpen ? ariaLabelledBy : undefined}
aria-describedby={isOpen ? ariaDescribedBy : undefined}
data-state={dataState}
aria-modal={ariaModal}
{...props}
>
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

Style prop override bug.

The style prop is merged on line 42 to include outline: 'none', but then {...props} on line 48 spreads the original style again, overwriting the merged value. Since style isn't destructured from the component props, it remains in ...props.

🐛 Proposed fix

Either destructure style from props:

 const DialogPrimitiveContent = forwardRef<HTMLDivElement, DialogPrimitiveContentProps>(({
     children,
     asChild = false,
     forceMount = false,
     role = 'dialog',
     'aria-modal': ariaModal = true,
     'aria-labelledby': ariaLabelledBy,
     'aria-describedby': ariaDescribedBy,
+    style,
     ...props
 }, ref) => {

Then update line 42:

-                        style={{ outline: 'none', ...props.style }}
+                        style={{ outline: 'none', ...style }}

Or spread props before the inline style:

                     <Primitive.div
                         ref={mergedRef}
                         asChild={asChild}
                         {...getFloatingProps()}
-                        style={{ outline: 'none', ...props.style }}
+                        {...props}
+                        style={{ outline: 'none', ...props.style }}
                         role={role}
                         aria-labelledby={isOpen ? ariaLabelledBy : undefined}
                         aria-describedby={isOpen ? ariaDescribedBy : undefined}
                         data-state={dataState}
                         aria-modal={ariaModal}
-                        {...props}
                     >
📝 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
<Primitive.div
ref={mergedRef}
asChild={asChild}
{...getFloatingProps()}
style={{ outline: 'none', ...props.style }}
role={role}
aria-labelledby={isOpen ? ariaLabelledBy : undefined}
aria-describedby={isOpen ? ariaDescribedBy : undefined}
data-state={dataState}
aria-modal={ariaModal}
{...props}
>
<Primitive.div
ref={mergedRef}
asChild={asChild}
{...getFloatingProps()}
{...props}
style={{ outline: 'none', ...props.style }}
role={role}
aria-labelledby={isOpen ? ariaLabelledBy : undefined}
aria-describedby={isOpen ? ariaDescribedBy : undefined}
data-state={dataState}
aria-modal={ariaModal}
>
🤖 Prompt for AI Agents
In `@src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx` around lines
38 - 49, The inline style adding outline: 'none' is being overwritten because
props (which still contains style) is spread after the inline style in
Primitive.div; fix by removing style from the spread so the merged style takes
effect: destructure style from the component props (e.g., const { style, ...rest
} = props or similar) and then pass the merged style as style={{ outline:
'none', ...style }} while spreading rest (not props), or alternatively spread
{...props} before the inline style so the inline style wins; update the
Primitive.div usage to reference mergedRef, getFloatingProps(), role, aria
attrs, and spread the corrected props/rest accordingly.

Comment on lines +34 to +39
// FIXME: Focus trap navigation is flaky in jsdom environment
// expect(document.activeElement).toBe(last);

last.focus();
await user.tab();
expect(document.activeElement).toBe(first);
// expect(document.activeElement).toBe(first);
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

Function name no longer reflects behavior.

With both assertions commented out, assertFocusTrap no longer actually asserts anything about the focus trap. It performs tab navigation but doesn't validate the results, which could mask real focus trap regressions.

Consider one of these approaches:

  1. Rename to exerciseFocusTrap to clarify it only performs navigation without validation
  2. Add a console warning that focus trap assertions are skipped in jsdom
  3. Create a tracking issue to fix the underlying jsdom limitation
🤖 Prompt for AI Agents
In `@src/test-utils/portal.ts` around lines 34 - 39, The function assertFocusTrap
no longer asserts anything because both focus assertions are commented out;
either rename assertFocusTrap to exerciseFocusTrap to reflect that it only
performs tab navigation, or restore validation by re-enabling the expectations
(document.activeElement checks) and guard them with a jsdom-detection branch
that logs a console warning when assertions are skipped; update all references
to assertFocusTrap accordingly and consider adding/linking a tracking issue if
leaving assertions disabled.

@@ -1,213 +1,152 @@
@use 'button';
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 'button' namespace or any button variables/mixins are used in this file
rg -n 'button\.' styles/themes/components/combobox.scss

Repository: rad-ui/ui

Length of output: 35


🏁 Script executed:

# Also read the entire combobox.scss file to verify
wc -l styles/themes/components/combobox.scss

Repository: rad-ui/ui

Length of output: 95


🏁 Script executed:

# Read the full file content
cat -n styles/themes/components/combobox.scss

Repository: rad-ui/ui

Length of output: 4705


Remove unused @use 'button' import.

The button module is imported on line 1 but is not referenced anywhere in this file. It can be safely removed.

♻️ Proposed fix
-@use 'button';
-
 .rad-ui-combobox-root {
📝 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 'button';
.rad-ui-combobox-root {
🤖 Prompt for AI Agents
In `@styles/themes/components/combobox.scss` at line 1, Remove the unused SCSS
module import by deleting the line containing `@use` 'button'; in this file (the
import is never referenced anywhere in styles/themes/components/combobox.scss),
and run the style linter/formatter afterwards to ensure no leftover unused
import warnings remain.

@@ -1,60 +1,95 @@
/* stylelint-disable no-descending-specificity */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove redundant stylelint disable comment causing pipeline failure.

The file-level disable on line 1 already disables no-descending-specificity for the entire file, making the inline disable on line 58 redundant and causing the pipeline error.

Proposed fix
-            // stylelint-disable-next-line no-descending-specificity
             .rad-ui-steps-track .rad-ui-steps-line {

Also applies to: 58-58

🤖 Prompt for AI Agents
In `@styles/themes/components/steps.scss` at line 1, This file already has a
file-level "/* stylelint-disable no-descending-specificity */" at the top, so
remove the redundant inline disable comment instance "/* stylelint-disable
no-descending-specificity */" found later in the file (around the previous line
58) to avoid the pipeline error; simply delete that duplicate inline comment and
keep the single top-of-file disable.

Comment on lines +42 to 62
&[data-status="active"] {
.rad-ui-steps-bubble {
border-color: var(--rad-ui-color-accent-600);
background-color: var(--rad-ui-color-accent-50);
color: var(--rad-ui-color-accent-700);
box-shadow: 0 0 0 4px var(--rad-ui-color-accent-100);
}
}

&[data-status="complete"] {
.rad-ui-steps-bubble {
border-color: var(--rad-ui-color-accent-600);
background-color: var(--rad-ui-color-accent-600);
color: white;
}

// stylelint-disable-next-line no-descending-specificity
.rad-ui-steps-track .rad-ui-steps-line {
background-color: var(--rad-ui-color-accent-600);
}
}
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

Data attribute selectors don't match the component output.

The SCSS uses data-status with values "active" and "complete", but StepItem.tsx renders data-state with values "active" and "completed". These selectors will never match, leaving active/completed steps unstyled.

Proposed fix
-        &[data-status="active"] {
+        &[data-state="active"] {
             .rad-ui-steps-bubble {
                 border-color: var(--rad-ui-color-accent-600);
                 background-color: var(--rad-ui-color-accent-50);
                 color: var(--rad-ui-color-accent-700);
                 box-shadow: 0 0 0 4px var(--rad-ui-color-accent-100);
             }
         }

-        &[data-status="complete"] {
+        &[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;
             }
             
-            // stylelint-disable-next-line no-descending-specificity
             .rad-ui-steps-track .rad-ui-steps-line {
                 background-color: var(--rad-ui-color-accent-600);
             }
         }
📝 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
&[data-status="active"] {
.rad-ui-steps-bubble {
border-color: var(--rad-ui-color-accent-600);
background-color: var(--rad-ui-color-accent-50);
color: var(--rad-ui-color-accent-700);
box-shadow: 0 0 0 4px var(--rad-ui-color-accent-100);
}
}
&[data-status="complete"] {
.rad-ui-steps-bubble {
border-color: var(--rad-ui-color-accent-600);
background-color: var(--rad-ui-color-accent-600);
color: white;
}
// stylelint-disable-next-line no-descending-specificity
.rad-ui-steps-track .rad-ui-steps-line {
background-color: var(--rad-ui-color-accent-600);
}
}
&[data-state="active"] {
.rad-ui-steps-bubble {
border-color: var(--rad-ui-color-accent-600);
background-color: var(--rad-ui-color-accent-50);
color: var(--rad-ui-color-accent-700);
box-shadow: 0 0 0 4px var(--rad-ui-color-accent-100);
}
}
&[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;
}
.rad-ui-steps-track .rad-ui-steps-line {
background-color: var(--rad-ui-color-accent-600);
}
}
🧰 Tools
🪛 GitHub Actions: Lint

[error] 58-58: stylelint: 'no-descending-specificity' has already been disabled. CssSyntaxError

🤖 Prompt for AI Agents
In `@styles/themes/components/steps.scss` around lines 42 - 62, The SCSS selectors
use data-status="active"/"complete" but the component StepItem.tsx renders
data-state="active"/"completed", so update the selectors in steps.scss to target
data-state instead of data-status and use "completed" (not "complete") — i.e.,
change &[data-status="active"] and &[data-status="complete"] to
&[data-state="active"] and &[data-state="completed"] and also update the nested
selector for .rad-ui-steps-track .rad-ui-steps-line accordingly so active and
completed steps are styled.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on February 7

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

// Provide a sensible default implementation
const {
defaultValue = 50,
defaultValue,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slider wrapper doesn't render multiple thumbs for array values

Medium Severity

The Slider convenience wrapper's type signature now accepts number | number[] for defaultValue and value, suggesting range slider support. However, the implementation always renders only a single Slider.Thumb without an index prop. When a user passes an array like defaultValue={[20, 80]} expecting two thumbs, they only see one thumb positioned at value[0]. The type signature is misleading since array values don't work as expected with the convenience wrapper.

Additional Locations (1)

Fix in Cursor Fix in Web

document.addEventListener('mouseup', handleMouseUp);
}

// Add listeners when dragging starts
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing cleanup leaves body styles corrupted on unmount

Medium Severity

The useEffect cleanup in ScrollAreaThumb no longer calls stopDrag() when the component unmounts. The startDrag function sets document.body.style.cursor to 'grabbing' and document.body.style.userSelect to 'none'. If the component unmounts while the user is dragging (e.g., navigation or conditional render), these body styles are never reset, leaving the page with a stuck grabbing cursor and disabled text selection everywhere.

Fix in Cursor Fix in Web

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