Skip to content

Conversation

@kotAPI
Copy link
Collaborator

@kotAPI kotAPI commented Sep 5, 2025

Summary

  • refactor Tabs subcomponents to forward refs and align types
  • add tests for ref forwarding, accessibility, and warnings
  • enable ref support in RovingFocusGroup root

Testing

  • npx jest src/components/ui/Tabs/tests/Tabs.test.tsx --runInBand --silent
  • npm run build:rollup --silent

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

Summary by CodeRabbit

  • New Features

    • Tabs components (Root, List, Trigger, Content) now forward refs to their DOM nodes.
    • Improved composability: support passing native div/button props and optional asChild rendering.
    • Enhanced interactions: reliable click and focus activation for tab changes.
  • Accessibility

    • Maintains correct roles/orientation and improves aria-hidden toggling when switching tabs.
  • Refactor

    • Internal focus group root updated to forward refs for consistency across the Tabs system.
  • Tests

    • Added tests covering ref forwarding, accessibility behavior, and absence of console warnings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 5, 2025

Warning

Rate limit exceeded

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 9720c9a and 4add296.

📒 Files selected for processing (4)
  • src/components/ui/Tabs/fragments/TabContent.tsx (2 hunks)
  • src/components/ui/Tabs/fragments/TabList.tsx (1 hunks)
  • src/components/ui/Tabs/fragments/TabTrigger.tsx (2 hunks)
  • src/components/ui/Tabs/tests/Tabs.test.tsx (3 hunks)

Walkthrough

Ref-forwarding was added to Tabs components (Root, List, Trigger, Content) and RovingFocusRoot. Props were broadened to extend native element props and support asChild composition. Trigger now handles click and focus-driven activation explicitly. Tests were added for ref forwarding, accessibility state (aria-hidden), and warning-free rendering.

Changes

Cohort / File(s) Summary
Tabs ref-forwarding and prop broadening
src/components/ui/Tabs/fragments/TabRoot.tsx, src/components/ui/Tabs/fragments/TabList.tsx, src/components/ui/Tabs/fragments/TabTrigger.tsx, src/components/ui/Tabs/fragments/TabContent.tsx
Converted to React.forwardRef; props now extend native element props; added asChild support (List, Root); Trigger adds click handling, refined focus activation, data attributes; Content forwards ref and spreads extra props; Root forwards ref to RovingFocusGroup.Root and spreads props.
RovingFocusGroup ref-forwarding
src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx
Switched to forwardRef and attached ref to underlying div; displayName set; behavior otherwise unchanged.
Tests
src/components/ui/Tabs/tests/Tabs.test.tsx
Added tests for ref forwarding (Root/List/Trigger/Content), aria-hidden behavior with forceMount, and absence of console warnings/errors.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Trigger as Tabs.Trigger (button)
  participant Root as Tabs.Root (RovingFocusGroup.Root)
  participant List as Tabs.List (div or asChild)
  participant Content as Tabs.Content (div)
  participant RFG as RovingFocusGroup

  Note over Root,RFG: Refs forwarded to underlying elements

  User->>Trigger: Click / Focus
  alt Click
    Trigger->>Root: handleTabChange(value)
  else Focus (activationMode=automatic)
    Trigger->>Root: handleTabChange(value)
  end
  Root->>Content: Update active tab value
  Content-->>User: Render active content (aria-hidden=false)
  Content-->>User: Inactive content (aria-hidden=true, if forceMount)
  Trigger->>RFG: Participate in roving focus
  RFG-->>Trigger: Manage focus movement
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Tabs [forwardRef refactor] #1395 — Implements the forwardRef refactor for TabContent, TabList, TabRoot, and TabTrigger, matching the components and changes in this PR.

Possibly related PRs

Suggested reviewers

  • GoldGroove06

Poem

A nibble of refs, a hop through props,
I forward my thoughts where focus stops.
Clicks and tabs in tidy rows,
aria whispers where content shows.
With gentle paws I test and cheer—
The UI garden feels quite clear. 🐰✨

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

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

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

Caution

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

⚠️ Outside diff range comments (2)
src/components/ui/Tabs/fragments/TabList.tsx (1)

3-3: Fix clsx import (named vs default export)

clsx exports a default function; the named import will be undefined at runtime.

-import { clsx } from 'clsx';
+import clsx from 'clsx';
src/components/ui/Tabs/fragments/TabRoot.tsx (1)

75-86: Forward asChild and move ...props before core props on RovingFocusGroup.Root

-            <RovingFocusGroup.Root
-                ref={ref}
-                orientation={orientation}
-                loop
-                dir={dir}
-                className={clsx(rootClass, className)}
-                data-rad-ui-accent-color={color}
-                {...dataAttributes}
-                {...props}
-            >
+            <RovingFocusGroup.Root
+                ref={ref}
+                asChild={asChild}
+                {...props}
+                {...dataAttributes}
+                orientation={orientation}
+                loop
+                dir={dir}
+                className={clsx(rootClass, className)}
+                data-rad-ui-accent-color={color}
+            >
🧹 Nitpick comments (6)
src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (2)

7-14: Update JSDoc to match the actual props (orientation 'both', dir, mode, disableTabIndexing)

Docs are out of sync with the type surface.

Apply:

 /**
  * Props for the RovingFocusRoot component
  * @property {React.ReactNode} children - Child components (should include RovingFocusGroup)
- * @property {'horizontal'|'vertical'} [orientation='horizontal'] - Orientation of keyboard navigation
- * @property {boolean} [loop=true] - Whether focus should loop from last to first item and vice versa
- * @property {string} [aria-label] - Accessible label for the roving focus group
- * @property {string} [aria-labelledby] - ID of an element that labels the roving focus group
+ * @property {'horizontal'|'vertical'|'both'} [orientation='horizontal'] - Orientation of keyboard navigation
+ * @property {'ltr'|'rtl'} [dir='ltr'] - Text direction, used for arrow-key handling
+ * @property {'default'|'tree'} [mode='default'] - Navigation mode
+ * @property {boolean} [loop=true] - Whether focus should loop from last to first item and vice versa
+ * @property {boolean} [disableTabIndexing=false] - Disable tabIndex management on items
+ * @property {string} [aria-label] - Accessible label for the roving focus group
+ * @property {string} [aria-labelledby] - ID of an element that labels the roving focus group
  */

60-66: Preserve controlled ARIA and add dir to DOM

Avoid consumers overriding required ARIA accidentally; also reflect dir on the element for CSS/AT.

-        <Primitive.div
-            ref={ref}
-            aria-orientation={orientation === 'both' ? undefined : orientation}
-            aria-label={ariaLabel}
-            aria-labelledby={ariaLabelledBy}
-            {...props}
-        >
+        <Primitive.div
+            ref={ref}
+            {...props}
+            dir={dir}
+            aria-orientation={orientation === 'both' ? undefined : orientation}
+            aria-label={ariaLabel}
+            aria-labelledby={ariaLabelledBy}
+        >
src/components/ui/Tabs/tests/Tabs.test.tsx (1)

555-575: Always restore console spies even on failure

Use try/finally to avoid cross-test leakage.

-        test('renders without console warnings', () => {
-            const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
-            const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
-            render(
+        test('renders without console warnings', () => {
+            const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+            const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+            try {
+              render(
                 <Tabs.Root defaultValue="tab1">
                   <Tabs.List>
                     <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
                     <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
                   </Tabs.List>
                   <Tabs.Content value="tab1">Content 1</Tabs.Content>
                   <Tabs.Content value="tab2">Content 2</Tabs.Content>
                 </Tabs.Root>
-            );
-            expect(warnSpy).not.toHaveBeenCalled();
-            expect(errorSpy).not.toHaveBeenCalled();
-            warnSpy.mockRestore();
-            errorSpy.mockRestore();
+              );
+              expect(warnSpy).not.toHaveBeenCalled();
+              expect(errorSpy).not.toHaveBeenCalled();
+            } finally {
+              warnSpy.mockRestore();
+              errorSpy.mockRestore();
+            }
         });
src/components/ui/Tabs/fragments/TabList.tsx (1)

31-37: Preserve child ref when asChild=true

Cloning overrides the child’s existing ref. Compose refs to avoid breaking consumers.

-            ? React.cloneElement(children as React.ReactElement<any>, {
-                ...childProps,
-                className: clsx((children as React.ReactElement<any>).props.className, childProps.className)
-            })
+            ? React.cloneElement(children as React.ReactElement<any>, {
+                ...childProps,
+                // compose child's ref with forwarded ref
+                ref: (node: TabListElement) => {
+                  // @ts-ignore - accessing possible ref callback/object
+                  const childRef = (children as any).ref;
+                  if (typeof childRef === 'function') childRef(node);
+                  else if (childRef && 'current' in childRef) childRef.current = node;
+                  if (typeof ref === 'function') ref(node);
+                  else if (ref && 'current' in (ref as any)) (ref as any).current = node;
+                },
+                className: clsx((children as React.ReactElement<any>).props.className, childProps.className)
+            })

Alternatively, use a small helper or @radix-ui/react-compose-refs.

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

17-17: asChild is declared but unused.

Either wire it up (e.g., via a Slot) or drop it to avoid a dead prop.

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

11-13: asChild is unused.

If asChild is part of the public API, implement it (e.g., Slot-based) or remove to reduce surface area.

Also applies to: 21-21

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3ff826c and 9720c9a.

📒 Files selected for processing (6)
  • src/components/ui/Tabs/fragments/TabContent.tsx (2 hunks)
  • src/components/ui/Tabs/fragments/TabList.tsx (1 hunks)
  • src/components/ui/Tabs/fragments/TabRoot.tsx (4 hunks)
  • src/components/ui/Tabs/fragments/TabTrigger.tsx (2 hunks)
  • src/components/ui/Tabs/tests/Tabs.test.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (5 hunks)
⏰ 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 (8)
src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (2)

40-50: Ref forwarding implementation looks good

Forwarded ref is correctly attached to the DOM node and displayName is set.

Also applies to: 61-71


17-24: Confirm 'both' orientation is supported in downstream roving logic

If items rely on horizontal/vertical branching, passing 'both' into context may break key handling.

Would you like a quick repo scan to locate all consumers of orientation in RovingFocus* and verify handling for 'both'?

src/components/ui/Tabs/tests/Tabs.test.tsx (2)

504-526: Solid ref-forwarding assertions

Good coverage on Root/List/Trigger/Content ref targets and element types.


529-551: A11y toggle test is accurate

forceMount + aria-hidden checks validate the new behavior well.

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

39-40: LGTM: grouping with RovingFocus is clear

Returning the list element inside RovingFocusGroup.Group keeps concerns separated.

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

9-15: Types + forwardRef look solid.

Element/props aliases are correct; children comes via div props.

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

13-24: Type layering looks good.

Omitting orientation from inherited props and reintroducing it explicitly is correct.


26-39: Ref-forwarding and controllable state wiring look good.

The forwardRef target and controlled/uncontrolled bridge are implemented cleanly.

Comment on lines 15 to 22
const TabTrigger = React.forwardRef<TabTriggerElement, TabTriggerProps>(({
value,
children,
className = '',
disabled,
asChild = false,
...props
}, forwardedRef) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Compose onClick, don’t override it; also set type="button" and lock attrs after spreads.

Currently, a consumer-provided onClick in {...props} will replace internal activation. Compose handlers and place {...props} before component-defined props so ARIA/disabled can’t be overridden. Add type="button" to avoid unintended form submits.

-const TabTrigger = React.forwardRef<TabTriggerElement, TabTriggerProps>({
-    value,
-    children,
-    className = '',
-    disabled,
-    asChild = false,
-    ...props
-}, forwardedRef) => {
+const TabTrigger = React.forwardRef<TabTriggerElement, TabTriggerProps>({
+    value,
+    children,
+    className = '',
+    disabled,
+    asChild = false,
+    onClick: userOnClick,
+    ...props
+}, forwardedRef) => {
@@
-            <button
-                ref={ref}
-                onClick={handleClick}
+            <button
+                ref={ref}
+                {...props}
+                {...dataAttributes}
+                type="button"
+                onClick={(e) => {
+                    userOnClick?.(e);
+                    if (!e.defaultPrevented) handleClick(e);
+                }}
                 className={clsx(
                     `${rootClass}-trigger`,
                     isActive ? 'active' : '',
                     disabled ? 'disabled' : '',
                     className
                 )}
                 role="tab"
                 aria-selected={isActive}
                 aria-disabled={disabled}
                 disabled={disabled}
-                {...dataAttributes}
-                {...props}
             >

Please run: npx jest src/components/ui/Tabs/tests/Tabs.test.tsx --runInBand --silent

Also applies to: 66-81


🏁 Script executed:

#!/bin/bash
set -e
# Locate the TabTrigger component file
file=$(find src -type f -path '*/Tabs/fragments/TabTrigger.tsx' | head -n1)
echo "Inspecting $file"
# Show the component signature and prop destructuring
sed -n '1,50p' "$file"
# List all occurrences of onClick in the file
echo "------ onClick occurrences ------"
grep -n "onClick" "$file" || true
# Show the Button render snippet where spreads and attributes are applied
echo "------ Button render snippet ------"
sed -n '60,120p' "$file"

Length of output: 3045


Extract onClick, compose with internal handler, reorder spreads before locked attrs, and add type="button
Destructure onClick from props to call the consumer’s handler before handleClick, move {...props} ahead of explicit props (disabled, role, ARIA, data-attributes) so they can’t be overridden, and add type="button" to the <button> to prevent implicit submits.

🤖 Prompt for AI Agents
In src/components/ui/Tabs/fragments/TabTrigger.tsx around lines 15 to 22,
destructure onClick from props, call the consumer onClick before calling the
component's internal handleClick, move the spread {...props} so it appears
before locked attributes (disabled, role, aria-*, data-*) to prevent consumers
from accidentally overriding them, and ensure the rendered button includes
type="button" to avoid implicit form submissions.

@kotAPI
Copy link
Collaborator Author

kotAPI commented Sep 5, 2025

Closing as stale

@kotAPI kotAPI closed this Sep 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants