Skip to content

Conversation

@kotAPI
Copy link
Collaborator

@kotAPI kotAPI commented Aug 13, 2025

Summary by CodeRabbit

  • New Features

    • Theme now defaults to following the system (light/dark) preference; UI reacts to OS color-scheme changes.
    • Continuous auto-scroll added to scroll areas for smoother thumb-driven scrolling.
  • Behavior Changes

    • Hover card timing and open/close handling improved (more reliable delayed open/close behavior).
    • Tabs/tab navigation now respect controlled vs uncontrolled usage when applying default selection.
  • Documentation

    • Fixed spacing in README around the Documentation link.
  • Tests

    • Added tests covering system theme behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

Walkthrough

Multiple UI component updates: formatting fixes in README and SVG markup, behavior and lifecycle improvements for HoverCard and ScrollAreaScrollbar, controlled/uncontrolled handling fixes in Tab/TabNav, Theme default and system-color handling with tests, a Link props type change, test refactors, and minor cleanup in Splitter.

Changes

Cohort / File(s) Summary
Docs formatting
README.md
Removed an extra space before the Documentation link.
SVG JSX formatting
src/components/ui/Checkbox/fragments/CheckboxIndicator.tsx, src/components/ui/Menubar/stories/Menubar.stories.tsx, src/components/ui/VisuallyHidden/stories/VisuallyHidden.stories.tsx
Reformatted inline SVGs to multiline JSX and converted kebab-case SVG attributes (e.g., fill-rule, clip-rule) to React camelCase (fillRule, clipRule). No path or behavior changes.
HoverCard behavior
src/components/ui/HoverCard/fragments/HoverCardRoot.tsx, src/components/ui/HoverCard/stories/HoverCard.stories.tsx
Replaced uncontrolled state with useControllableState, added cancelable delayed open/close via timeout refs, exposed delay helpers through context, added cleanup; story updated to use explicit click-to-open flow.
ScrollArea continuous scroll
src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx
Added continuous auto-scroll lifecycle: interval and mouse-position refs, start/stop handlers, guarded interval scheduling, global listeners attached while scrolling, and cleanup on unmount. Public API unchanged.
Link props type
src/components/ui/Link/Link.tsx
Changed LinkProps.props from Record<string, any>[] to Record<string, any> (array → single object).
Tab / TabNav default handling
src/components/ui/TabNav/fragments/TabNavRoot.tsx, src/components/ui/Tabs/fragments/TabRoot.tsx
Apply defaultValue only when uncontrolled (value === undefined) and add value to effect deps so behavior adapts when toggling between controlled/uncontrolled. Comments added.
Splitter cleanup
src/components/ui/Splitter/fragments/SplitterRoot.tsx
Added useEffect cleanup to clear debouncedOnSizesChange timeout on unmount.
Theme behavior, stories, tests
src/components/ui/Theme/Theme.tsx, src/components/ui/Theme/stories/Theme.stories.tsx, src/components/ui/Theme/tests/Theme.test.tsx
Default appearance changed from 'light' to 'system'; added prefers-color-scheme media query handling with change listener and cleanup; story adjusted to use system appearance; new test added to assert media listener registration, theme updates, and cleanup.
Table tests refactor
src/components/ui/Table/tests/Table.test.tsx
Rewrote tests to use @testing-library/react role-based queries and within scoping instead of container DOM traversal.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Trigger as HoverCard.Trigger
    participant HoverRoot as HoverCard.Root
    participant Context as HoverCardContext

    User->>Trigger: mouseenter / click
    Trigger->>HoverRoot: notify open request
    HoverRoot->>Context: call openWithDelay()
    Context->>HoverRoot: schedule timeout (openTimeoutRef)
    HoverRoot-->>Context: setOpen(true) via useControllableState (onOpenChange handled by hook)
    User->>Trigger: mouseleave
    HoverRoot->>Context: call closeWithDelay()
    Context->>HoverRoot: schedule/clear closeTimeoutRef
    HoverRoot-->>Context: setOpen(false)
Loading
sequenceDiagram
    participant User
    participant Thumb as ScrollArea.Thumb
    participant Scrollbar as ScrollAreaScrollbar

    User->>Thumb: mousedown & hold (mouseY)
    Thumb->>Scrollbar: startContinuousScroll(e)
    Scrollbar->>Scrollbar: immediate scroll step (mousePositionRef)
    Scrollbar->>Scrollbar: after 300ms start interval (50ms)
    loop while shouldContinueScrolling(mouseY)
        Scrollbar->>Scrollbar: repeated scroll steps
    end
    User->>Scrollbar: mouseup / mouseleave
    Scrollbar->>Scrollbar: stopContinuousScroll() -> clear interval, cleanup listeners
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Suggested reviewers

  • GoldGroove06

Poem

A rabbit hops and trims the code,
Tiny fixes in every node.
Timeouts tidy, themes align,
SVGs shine in camel fine.
Hooray — the UI hops along! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kotapi/13-aug-2025-improvements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

Copy link
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: 0

🧹 Nitpick comments (1)
README.md (1)

15-15: Nit: Smooth the link phrasing and casing.

Consider lowercasing the link text and adding “the” to read more naturally.

Apply this diff:

-For full documentation, visit [Documentation](https://www.rad-ui.com/docs/first-steps/introduction).
+For full documentation, visit the [documentation](https://www.rad-ui.com/docs/first-steps/introduction).
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc29f88 and 87aab3c.

📒 Files selected for processing (1)
  • README.md (1 hunks)
🔇 Additional comments (1)
README.md (1)

15-15: LGTM: Whitespace fix is correct.

Removing the extra space improves readability and keeps the docs tidy.

@kotAPI kotAPI merged commit b1658bc into main Aug 13, 2025
7 checks passed
@kotAPI kotAPI deleted the kotapi/13-aug-2025-improvements branch August 13, 2025 15:44
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: 4

🔭 Outside diff range comments (4)
src/components/ui/Link/Link.tsx (2)

22-26: Rename rest param and drop ts-ignore; ensure native attrs pass through

After retyping LinkProps, forward native anchor attributes via a clearly named rest param and remove the ts-ignore.

Apply this diff:

-const Link = ({ children, href = '#', customRootClass, className, size = '', ...props }: LinkProps) => {
+const Link = ({ children, href = '#', customRootClass, className, size = '', ...rest }: LinkProps) => {
     const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
     const dataAttributes = useCreateDataAttribute('link', { size });
-    // @ts-ignore
-    return <Primitive.a href={href} className={clsx(rootClass, className)} {...dataAttributes()} {...props}>{children}</Primitive.a>;
+    return (
+        <Primitive.a
+            href={href}
+            className={clsx(rootClass, className)}
+            {...dataAttributes()}
+            {...rest}
+        >
+            {children}
+        </Primitive.a>
+    );
 };

If TS still complains about spreading dataAttributes(), consider giving the hook a precise return type or locally asserting:

{...(dataAttributes() as React.HTMLAttributes<HTMLAnchorElement>)}

10-17: Fix LinkProps to Extend Anchor Attributes and Rename Rest Parameter

To eliminate the props collision and properly type all native <a> attributes:

  • Remove the custom props?: Record<string, any> field from LinkProps.
  • Extend React.ComponentPropsWithoutRef<'a'> so consumers can pass any valid anchor props (e.g. target, rel, etc.).
  • Rename the destructured rest parameter from ...props to ...rest.
  • Update the JSX spread to {...rest}.
  • No usages of <Link props={…}> were found in the codebase, so no migration of call sites is required.

Apply this diff in src/components/ui/Link/Link.tsx:

-export type LinkProps = {
-    children: React.ReactNode;
-    href: string;
-    customRootClass?: string;
-    size?: string;
-    className?: string;
-    props?: Record<string, any>;
-}
+export type LinkProps = React.ComponentPropsWithoutRef<'a'> & {
+    customRootClass?: string;
+    size?: string;
+};

-const Link = ({ children, href = '#', customRootClass, className, size = '', ...props }: LinkProps) => {
+const Link = ({
+  children,
+  href = '#',
+  className,
+  customRootClass,
+  size = '',
+  ...rest
+}: LinkProps) => {
     const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
     const dataAttributes = useCreateDataAttribute('link', { size });
-    // @ts-ignore
-    return <Primitive.a href={href} className={clsx(rootClass, className)} {...dataAttributes()} {...props}>{children}</Primitive.a>;
+    return (
+      // @ts-ignore
+      <Primitive.a
+        href={href}
+        className={clsx(rootClass, className)}
+        {...dataAttributes()}
+        {...rest}
+      >
+        {children}
+      </Primitive.a>
+    );
 };
src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (2)

41-66: Untracked startup delay timer; potential leak and late firing after stop/unmount

setTimeout is used to defer the start of continuous scrolling but its ID isn’t stored/cleared. If scrolling is stopped or the component unmounts before the delay elapses, the callback may run later than intended.

+    // delay before the continuous scroll starts
+    const delayTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
-        // Start continuous scrolling after a brief delay
-        setTimeout(() => {
+        // Start continuous scrolling after a brief delay
+        delayTimeoutRef.current = window.setTimeout(() => {
             if (isScrollingRef.current) {
                 intervalRef.current = setInterval(() => {
                     if (isScrollingRef.current && shouldContinueScrolling(mousePositionRef.current)) {
                         handleScrollbarClick({ clientY: mousePositionRef.current });
                     } else {
                         // Stop scrolling if thumb reached mouse position
                         stopContinuousScroll();
                     }
                 }, 50); // Scroll every 50ms
             }
-        }, 300); // 300ms delay before continuous scrolling starts
+        }, 300); // 300ms delay before continuous scrolling starts

And clear it when stopping:

 const stopContinuousScroll = useCallback(() => {
     isScrollingRef.current = false;
     removeListenersRef.current?.();
     removeListenersRef.current = null;
+    if (delayTimeoutRef.current) {
+        clearTimeout(delayTimeoutRef.current);
+        delayTimeoutRef.current = null;
+    }
     if (intervalRef.current) {
         clearInterval(intervalRef.current);
         intervalRef.current = null;
     }
     setIsScrolling(false);
 }, []);

24-38: Ensure Orientation-aware Scroll Logic

The ScrollAreaScrollbar currently ignores the orientation prop and always uses a single scrollXThumbRef with clientY/top–bottom checks—there’s no support for horizontal scrolling (clientX/left–right), and the context doesn’t expose a scrollYThumbRef. We should either remove the unused orientation prop (and rename scrollXThumbRef to scrollThumbRef if only vertical mode is needed) or fully implement orientation support by:

  • Adding scrollYThumbRef?: RefObject<HTMLDivElement> to ScrollAreaContextType
  • Initializing and providing scrollYThumbRef alongside scrollXThumbRef in ScrollAreaRoot.tsx
  • Updating ScrollAreaScrollbar.tsx to destructure scrollYThumbRef and switch axis/ref based on orientation

Locations to update:

  • src/components/ui/ScrollArea/context/ScrollAreaContext.tsx
  • src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx
  • src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (lines 12–38)

Proposed diff for shouldContinueScrolling in ScrollAreaScrollbar.tsx:

-    const { rootClass, handleScrollbarClick, scrollXThumbRef } = useContext(ScrollAreaContext);
+    const { rootClass, handleScrollbarClick, scrollXThumbRef, scrollYThumbRef } = useContext(ScrollAreaContext);

-    const shouldContinueScrolling = useCallback((mouseY: number): boolean => {
-        if (!scrollXThumbRef?.current) return false;
-
-        const thumbRect = scrollXThumbRef.current.getBoundingClientRect();
-        const thumbStart = thumbRect.top;
-        const thumbEnd = thumbRect.bottom;
-
-        // Stop if mouse is within thumb bounds
-        if (mouseY >= thumbStart && mouseY <= thumbEnd) {
-            return false;
-        }
-
-        return true;
-    }, [scrollXThumbRef]);
+    const shouldContinueScrolling = useCallback((mousePos: number): boolean => {
+        // Choose the correct thumb and axis per orientation
+        const thumbRef = orientation === 'horizontal' ? scrollXThumbRef : scrollYThumbRef;
+        if (!thumbRef?.current) return false;
+
+        const rect = thumbRef.current.getBoundingClientRect();
+        const [start, end] =
+            orientation === 'horizontal' ? [rect.left, rect.right] : [rect.top, rect.bottom];
+
+        // Stop when pointer enters the thumb bounds
+        return !(mousePos >= start && mousePos <= end);
+    }, [orientation, scrollXThumbRef, scrollYThumbRef]);
🧹 Nitpick comments (7)
src/components/ui/Menubar/stories/Menubar.stories.tsx (1)

14-20: Mark decorative icon as aria-hidden to avoid SR noise

ChevronRight is purely decorative in SubTrigger. Hide it from assistive tech and prevent accidental focus.

Apply this diff:

     <svg
         width="15"
         height="15"
         viewBox="0 0 15 15"
         fill="none"
         xmlns="http://www.w3.org/2000/svg"
+        aria-hidden="true"
+        focusable="false"
     >
src/components/ui/VisuallyHidden/stories/VisuallyHidden.stories.tsx (1)

8-22: SVG camelCase conversion looks good; optionally allow prop overrides on icons

  • The JSX-friendly attributes and multi-line formatting are correct. Icons are wrapped with aria-hidden at usage sites, which is ideal.
  • Optional: expose standard SVG props to make these icons reusable (e.g., size/className overrides in stories).

Apply this diff for both icons:

-const EmailIcon = () => {
+const EmailIcon = (props: React.SVGProps<SVGSVGElement>) => {
     return (
         <svg
             width="15"
             height="15"
             viewBox="0 0 15 15"
             fill="none"
             xmlns="http://www.w3.org/2000/svg"
+            {...props}
         >
-const SearchIcon = () => {
+const SearchIcon = (props: React.SVGProps<SVGSVGElement>) => {
     return (
         <svg
             width="15"
             height="15"
             viewBox="0 0 15 15"
             fill="none"
             xmlns="http://www.w3.org/2000/svg"
+            {...props}
         >

Also applies to: 26-41

src/components/ui/Checkbox/fragments/CheckboxIndicator.tsx (1)

14-21: Hide indicator SVG from assistive tech

The checkbox state should be conveyed by the control semantics; the indicator SVG is decorative. Mark it aria-hidden and not focusable to avoid duplicate announcements.

Apply this diff:

         <svg
             width="15"
             height="15"
             viewBox="0 0 15 15"
             className={clsx(`${rootClass}-indicator`, className)}
             fill="none"
             xmlns="http://www.w3.org/2000/svg"
+            aria-hidden="true"
+            focusable="false"
         >
src/components/ui/Link/Link.tsx (1)

19-21: Remove stale comment about 'alt' on anchors

This comment refers to previous code and adds noise. Safe to remove.

Apply this diff:

-// TODO: in the previous return value
-// return <a href={href} alt={alt} className={clsx(rootClass, className)} {...props}>{children}</a>;
-// 'alt' prop does not exist on an anchor element
src/components/ui/Theme/stories/Theme.stories.tsx (1)

25-27: Story toggle no longer affects Theme; consider aligning UI/wording or exposing a control

With appearance="system", the “Toggle Theme” button now only changes the CardComponent text, not the Theme. This can confuse readers of the story.

  • Option A: Wire the toggle back to Theme appearance for a light/dark demo.
  • Option B: Keep system mode and adjust the button text or add a Storybook control (args) for appearance.

Minor nit: prefer appearance="system" over appearance={'system'}.

-                    <Theme appearance={'system'}>
+                    <Theme appearance="system">
src/components/ui/Theme/tests/Theme.test.tsx (1)

13-47: Solid coverage of system color-scheme lifecycle (init, update, cleanup)

The test accurately validates initial light mode, listener registration, dark-mode transition, and cleanup via removeEventListener. Nicely scoped and readable.

Two minor type/robustness improvements:

  • Avoid as unknown as MediaQueryList by providing the minimal shape for MediaQueryList in the stub.
  • Optionally include addListener/removeListener no-ops for older APIs if your component supports them.
-        const mediaQueryList = {
-            matches: false,
-            addEventListener: jest.fn((_event, listener) => {
-                listeners.push(listener);
-            }),
-            removeEventListener: jest.fn((_event, listener) => {
-                const index = listeners.indexOf(listener);
-                if (index !== -1) {
-                    listeners.splice(index, 1);
-                }
-            })
-        } as unknown as MediaQueryList;
+        const mediaQueryList: MediaQueryList = {
+            media: '(prefers-color-scheme: dark)',
+            matches: false,
+            onchange: null,
+            addEventListener: jest.fn((_event, listener: EventListenerOrEventListenerObject) => {
+                listeners.push(listener as (e: MediaQueryListEvent) => void);
+            }),
+            removeEventListener: jest.fn((_event, listener: EventListenerOrEventListenerObject) => {
+                const index = listeners.indexOf(listener as (e: MediaQueryListEvent) => void);
+                if (index !== -1) listeners.splice(index, 1);
+            }),
+            // Legacy APIs (optional)
+            addListener: jest.fn(),
+            removeListener: jest.fn(),
+            dispatchEvent: jest.fn(() => true),
+        };
src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (1)

41-66: Optional: track live mouse position for smoother UX

Comments suggest tracking the “latest mouse Y position”, but the code only captures the initial down position. If live tracking is desired, add a mousemove listener while scrolling to update mousePositionRef.current.

         document.addEventListener('mouseup', handleMouseUp);
         document.addEventListener('mouseleave', handleMouseLeave);
+        const handleMouseMove = (ev: MouseEvent) => {
+            mousePositionRef.current = orientation === 'vertical' ? ev.clientY : ev.clientX;
+        };
+        document.addEventListener('mousemove', handleMouseMove);
@@
         removeListenersRef.current = () => {
             document.removeEventListener('mouseup', handleMouseUp);
             document.removeEventListener('mouseleave', handleMouseLeave);
+            document.removeEventListener('mousemove', handleMouseMove);
         };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87aab3c and 015239a.

📒 Files selected for processing (14)
  • src/components/ui/Checkbox/fragments/CheckboxIndicator.tsx (1 hunks)
  • src/components/ui/HoverCard/fragments/HoverCardRoot.tsx (3 hunks)
  • src/components/ui/HoverCard/stories/HoverCard.stories.tsx (1 hunks)
  • src/components/ui/Link/Link.tsx (1 hunks)
  • src/components/ui/Menubar/stories/Menubar.stories.tsx (1 hunks)
  • src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx (3 hunks)
  • src/components/ui/Splitter/fragments/SplitterRoot.tsx (2 hunks)
  • src/components/ui/TabNav/fragments/TabNavRoot.tsx (1 hunks)
  • src/components/ui/Table/tests/Table.test.tsx (3 hunks)
  • src/components/ui/Tabs/fragments/TabRoot.tsx (1 hunks)
  • src/components/ui/Theme/Theme.tsx (1 hunks)
  • src/components/ui/Theme/stories/Theme.stories.tsx (1 hunks)
  • src/components/ui/Theme/tests/Theme.test.tsx (1 hunks)
  • src/components/ui/VisuallyHidden/stories/VisuallyHidden.stories.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-04-07T04:38:34.864Z
Learnt from: kotAPI
PR: rad-ui/ui#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/TabNav/fragments/TabNavRoot.tsx
  • src/components/ui/Tabs/fragments/TabRoot.tsx
📚 Learning: 2025-07-18T16:43:26.175Z
Learnt from: GoldGroove06
PR: rad-ui/ui#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/TabNav/fragments/TabNavRoot.tsx
  • src/components/ui/Splitter/fragments/SplitterRoot.tsx
  • src/components/ui/Tabs/fragments/TabRoot.tsx
🧬 Code Graph Analysis (1)
src/components/ui/HoverCard/fragments/HoverCardRoot.tsx (1)
src/core/hooks/useControllableState/index.tsx (1)
  • useControllableState (15-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (14)
src/components/ui/Menubar/stories/Menubar.stories.tsx (1)

24-25: SVG attribute camelCase is correct

Good conversion from fill-rule/clip-rule to fillRule/clipRule for React JSX compatibility.

src/components/ui/Checkbox/fragments/CheckboxIndicator.tsx (1)

25-26: CamelCase attributes are correct

fillRule/clipRule usage is correct for JSX; no issues spotted.

src/components/ui/Splitter/fragments/SplitterRoot.tsx (2)

2-2: LGTM! Proper useEffect import addition.

The useEffect import is correctly added to support the cleanup functionality being introduced.


55-62: Excellent memory leak prevention with proper timeout cleanup.

The cleanup useEffect correctly clears the debounced timeout on component unmount, preventing potential memory leaks and callbacks executing after the component is destroyed. The implementation properly checks for the timeout existence before clearing it and sets it to null afterwards.

src/components/ui/TabNav/fragments/TabNavRoot.tsx (1)

39-47: Excellent controlled/uncontrolled state handling improvement.

The changes correctly implement the controlled vs uncontrolled component pattern by:

  1. Only setting the default value when value === undefined (uncontrolled mode)
  2. Including value in the dependency array to handle mode transitions
  3. Adding clear comments explaining the behavior

This aligns with React best practices and the retrieved learning about controlled component implementation.

src/components/ui/Tabs/fragments/TabRoot.tsx (1)

53-63: Perfect controlled/uncontrolled pattern implementation with clear documentation.

The implementation correctly follows the controlled component pattern established in the learning from AccordionRoot:

  1. Only applies defaultValue in uncontrolled mode (value === undefined)
  2. Includes value in dependencies to handle control state transitions
  3. Provides comprehensive comments explaining the behavior

This change maintains consistency with the TabNavRoot implementation and aligns with React best practices.

src/components/ui/Table/tests/Table.test.tsx (4)

2-2: Good addition of the within utility.

The within import enables more semantic and accessible testing patterns by scoping queries to specific DOM subtrees.


51-54: Improved accessibility-focused testing approach.

The change from DOM querying to getAllByRole('table') provides better semantic testing that aligns with how screen readers and users interact with the component.


57-68: Excellent semantic testing with proper DOM structure validation.

The new approach using within() and role-based queries:

  1. Properly validates the table structure (rowgroup → row → columnheader hierarchy)
  2. Uses semantic ARIA roles instead of generic DOM queries
  3. Maintains the same test coverage with better accessibility validation

This is a significant improvement in test quality.


71-88: Robust data validation with semantic queries.

The updated test approach:

  1. Validates proper table structure with 2 rowgroups (header + body)
  2. Uses semantic role queries throughout
  3. Maintains comprehensive data validation
  4. Follows accessibility-first testing patterns

This provides better confidence in the component's accessibility and structure.

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

13-13: Excellent default change to system-aware theming.

Changing the default appearance to 'system' provides a better user experience by respecting the user's OS-level dark/light mode preference by default.


20-35: Perfect system color-scheme handling with proper cleanup.

The implementation correctly:

  1. Creates a MediaQueryList to detect system color scheme preference
  2. Only updates theme when in 'system' mode to avoid overriding explicit user choices
  3. Sets initial theme based on current media query state
  4. Adds event listener for dynamic color scheme changes
  5. Properly cleans up the event listener to prevent memory leaks

The logic handles both initial state and ongoing changes while respecting controlled behavior.

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

67-69: Click-to-open UX looks good

Switching the trigger to an explicit click aligns with the “Controlled” story intent and helps disambiguate hover vs. click behaviors.

src/components/ui/HoverCard/fragments/HoverCardRoot.tsx (1)

41-41: Good adoption of useControllableState

Routing all open state changes through useControllableState simplifies the controlled/uncontrolled logic and centralizes the onOpenChange callback.

Comment on lines +46 to +47
const openTimeoutRef = useRef<number | null>(null);
const closeTimeoutRef = useRef<number | null>(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Timer ref typing/casts: use ReturnType and drop double casts

Avoid as unknown as number by typing refs to ReturnType<typeof setTimeout> | null. This is portable across DOM/Node typings and removes unsafe casts.

-    const openTimeoutRef = useRef<number | null>(null);
-    const closeTimeoutRef = useRef<number | null>(null);
+    const openTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+    const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
-        openTimeoutRef.current = setTimeout(() => {
+        openTimeoutRef.current = setTimeout(() => {
             handleOpenChange(true);
-        }, openDelay) as unknown as number;
+        }, openDelay);
@@
-        closeTimeoutRef.current = setTimeout(() => {
+        closeTimeoutRef.current = setTimeout(() => {
             setMouseIsExiting(prevState => {
                 if (prevState) {
                     handleOpenChange(false);
                 }
                 return prevState;
             });
-        }, closeDelay) as unknown as number;
+        }, closeDelay);
@@
     useEffect(() => {
         return () => {
             if (openTimeoutRef.current) {
                 clearTimeout(openTimeoutRef.current);
             }
             if (closeTimeoutRef.current) {
                 clearTimeout(closeTimeoutRef.current);
             }
         };
     }, []);

Also applies to: 79-82, 86-97, 103-113

🤖 Prompt for AI Agents
In src/components/ui/HoverCard/fragments/HoverCardRoot.tsx around lines 46-47
(and also update usages at 79-82, 86-97, 103-113), the timer refs are currently
typed/used with unsafe number casts; change their types to use
useRef<ReturnType<typeof setTimeout> | null> (initialize to null) for
openTimeoutRef and closeTimeoutRef and any other timeout refs, remove the "as
unknown as number" casts, and adjust setTimeout/clearTimeout calls to accept
these typed refs (check for null before clearTimeout and assign setTimeout
result directly to the ref). Ensure all references and null checks are updated
accordingly.

Comment on lines +61 to 64
onOpenChange={() => {}}
openDelay={100}
closeDelay={200}
customRootClass=""
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Controlled pattern: propagate onOpenChange to keep external state in sync

Using open={open} with onOpenChange={() => {}} prevents internal interactions (hover, dismiss) from updating your controlled state, which can lead to confusing demos. For a controlled example, pass setOpen to onOpenChange so both internal requests and your buttons remain in sync.

-            <HoverCard.Root
-                open={open}
-                onOpenChange={() => {}}
+            <HoverCard.Root
+                open={open}
+                onOpenChange={setOpen}
                 openDelay={100}
                 closeDelay={200}
                 customRootClass=""
             >
📝 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
onOpenChange={() => {}}
openDelay={100}
closeDelay={200}
customRootClass=""
<HoverCard.Root
open={open}
onOpenChange={setOpen}
openDelay={100}
closeDelay={200}
customRootClass=""
>
🤖 Prompt for AI Agents
In src/components/ui/HoverCard/stories/HoverCard.stories.tsx around lines 61 to
64, the story uses a controlled open={open} but supplies a no-op onOpenChange
handler, which prevents internal interactions from updating the external state;
replace the empty handler with the state setter (e.g., onOpenChange={setOpen})
so internal hover/dismiss events propagate to the story's open state and keep
the UI and buttons in sync.

Comment on lines 14 to 16
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// read/write ref to know if continuous scrolling is currently active
const isScrollingRef = useRef(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Timer type mismatch in the browser

This file runs on the client; using NodeJS.Timeout can be incorrect and cause TS friction in DOM builds. Prefer number | null or ReturnType<typeof setInterval>.

-    const intervalRef = useRef<NodeJS.Timeout | null>(null);
+    const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
📝 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 intervalRef = useRef<NodeJS.Timeout | null>(null);
// read/write ref to know if continuous scrolling is currently active
const isScrollingRef = useRef(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// read/write ref to know if continuous scrolling is currently active
const isScrollingRef = useRef(false);
🤖 Prompt for AI Agents
In src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx around lines
14 to 16, the intervalRef is typed as NodeJS.Timeout which is wrong for browser
builds; change its type to number | null (or ReturnType<typeof setInterval>) and
update places that call setInterval/clearInterval to treat the ref value as a
number (use window.setInterval if needed) and pass it to clearInterval without
Node-specific typing so the code compiles cleanly in DOM builds.

Comment on lines +80 to 106
// Attach global mouse listeners only while actively scrolling
React.useEffect(() => {
if (!isScrollingRef.current) {
return () => {
removeListenersRef.current?.();
stopContinuousScroll();
};
}

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

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

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

return () => {
removeListenersRef.current?.();
removeListenersRef.current = null;
stopContinuousScroll(); // Cleanup on unmount
};
}, [stopContinuousScroll]);
}, [isScrolling, stopContinuousScroll]);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Effect cleanup calls setState via stopContinuousScroll when not scrolling; risks state thrash

The effect returns a cleanup that invokes stopContinuousScroll() even when no listeners were attached. This can trigger state flips during re-renders and complicate the lifecycle.

Refactor to a standard “attach when active” pattern: only attach when isScrolling is true, and cleanup just removes those listeners without calling stopContinuousScroll.

-    React.useEffect(() => {
-        if (!isScrollingRef.current) {
-            return () => {
-                removeListenersRef.current?.();
-                stopContinuousScroll();
-            };
-        }
+    React.useEffect(() => {
+        if (!isScrolling) return;
 
         const handleMouseUp = () => stopContinuousScroll();
         const handleMouseLeave = () => stopContinuousScroll();
 
         document.addEventListener('mouseup', handleMouseUp);
         document.addEventListener('mouseleave', handleMouseLeave);
 
         removeListenersRef.current = () => {
             document.removeEventListener('mouseup', handleMouseUp);
             document.removeEventListener('mouseleave', handleMouseLeave);
         };
 
-        return () => {
-            removeListenersRef.current?.();
-            removeListenersRef.current = null;
-            stopContinuousScroll(); // Cleanup on unmount
-        };
-    }, [isScrolling, stopContinuousScroll]);
+        return () => {
+            removeListenersRef.current?.();
+            removeListenersRef.current = null;
+        };
+    }, [isScrolling, stopContinuousScroll]);
📝 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
// Attach global mouse listeners only while actively scrolling
React.useEffect(() => {
if (!isScrollingRef.current) {
return () => {
removeListenersRef.current?.();
stopContinuousScroll();
};
}
const handleMouseUp = () => stopContinuousScroll();
const handleMouseLeave = () => stopContinuousScroll();
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mouseleave', handleMouseLeave);
return () => {
removeListenersRef.current = () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mouseleave', handleMouseLeave);
};
return () => {
removeListenersRef.current?.();
removeListenersRef.current = null;
stopContinuousScroll(); // Cleanup on unmount
};
}, [stopContinuousScroll]);
}, [isScrolling, stopContinuousScroll]);
// Attach global mouse listeners only while actively scrolling
React.useEffect(() => {
if (!isScrolling) return;
const handleMouseUp = () => stopContinuousScroll();
const handleMouseLeave = () => stopContinuousScroll();
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mouseleave', handleMouseLeave);
removeListenersRef.current = () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mouseleave', handleMouseLeave);
};
return () => {
removeListenersRef.current?.();
removeListenersRef.current = null;
};
}, [isScrolling, stopContinuousScroll]);
🤖 Prompt for AI Agents
In src/components/ui/ScrollArea/fragments/ScrollAreaScrollbar.tsx around lines
80 to 106, the effect currently calls stopContinuousScroll() in its cleanup even
when no listeners were attached, which can cause unnecessary state updates;
change the effect to only run and attach document listeners when isScrolling (or
isScrollingRef.current) is true, register mouseup/mouseleave handlers, store a
removal function, and in the cleanup simply remove those listeners and clear the
removal ref without calling stopContinuousScroll(); ensure stopContinuousScroll
is invoked only from the actual handlers (or where scrolling truly ends), and
adjust the effect dependencies accordingly.

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