Skip to content

Conversation

@kotAPI
Copy link
Collaborator

@kotAPI kotAPI commented Feb 15, 2025

RovingFocusGroup - adding this component in collaboration with @GoldGroove06 @jindaljyoti

TODO

Summary by CodeRabbit

  • New Features

    • Launched an enhanced focus management system across interactive UI elements to improve keyboard navigation and accessibility.
    • Added interactive demos showcasing various navigation modes, including horizontal, vertical, and disabled item behavior.
  • Refactor

    • Updated tab navigation and color selection components to integrate the new focus management, streamlining user interactions and boosting overall usability.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This update introduces a comprehensive roving focus management system. New React components—namely RovingFocusRoot, RovingFocusGroup, and RovingFocusItem—are implemented to handle focus state and keyboard navigation across UI elements. Two contexts, RovingFocusRootContext and RovingFocusGroupContext, facilitate configuration sharing. The new system is integrated into tab components and the SandboxEditor, replacing previous navigation mechanisms with a refined focus-group approach.

Changes

File(s) Change Summary
src/core/.../fragments/RovingFocusRoot.tsx, src/core/.../fragments/RovingFocusGroup.tsx, src/core/.../fragments/RovingFocusItem.tsx Introduces new components for managing focus state, keyboard navigation, and registration of focusable items within groups.
src/core/.../context/RovingFocusRootContext.tsx, src/core/.../context/RovingFocusGroupContext.tsx Adds new contexts defining and providing focus-related configuration and state for the focus system.
src/core/.../index.tsx Exports a utility object encapsulating the new focus components for streamlined integration.
src/core/.../stories/RovingFocusGroup.stories.tsx Adds Storybook stories demonstrating the usage and behavior of the roving focus components, including horizontal, vertical, keyboard navigation, disabled items, and accessibility features.
src/components/ui/Tabs/.../TabList.tsx, src/components/ui/Tabs/.../TabRoot.tsx, src/components/ui/Tabs/.../TabTrigger.tsx Integrates the roving focus system into tab components by replacing div wrappers and removing manual navigation functions in favor of focus group items.
src/components/tools/SandboxEditor/SandboxEditor.tsx Refactors the color selection component to use roving focus items, improving keyboard navigation and interaction in the color-select interface.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant RFRoot as RovingFocusRoot (Provider)
    participant RFGroup as RovingFocusGroup (Manager)
    participant RFItem as RovingFocusItem (Focusable Element)
    
    User->>RFItem: Triggers focus (click/keydown)
    RFItem->>RFGroup: Registers/updating focus state
    RFGroup->>RFRoot: Propagates context values (direction, loop)
    RFRoot->>RFItem: Applies focus management properties
Loading

Possibly related PRs

Suggested labels

automerge

Poem

I'm just a rabbit, hopping with glee,
In a forest of components, wild and free.
I bound through focus with keyboard keys,
Skipping from item to item with effortless ease.
Through roving paths and context so bright,
I celebrate each change in the code light! 🐇✨

Tip

⚡🧪 Multi-step agentic review comment chat (experimental)
  • We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
    - To enable this feature, set early_access to true under in the settings.

📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 01d80d5 and 2e153e7.

📒 Files selected for processing (1)
  • src/components/ui/Tabs/fragments/TabList.tsx (2 hunks)

🪧 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.
    • Generate unit testing code for this file.
    • 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. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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

Documentation and Community

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

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ee73049 and 698dc9a.

📒 Files selected for processing (4)
  • src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/index.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx
  • src/core/utils/RovingFocusGroup/index.tsx
🔇 Additional comments (1)
src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1)

1-4: LGTM! Clean import structure.

The imports are well-organized and follow good practices with proper path aliasing.

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

♻️ Duplicate comments (2)
src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (2)

7-35: 🛠️ Refactor suggestion

Add accessibility documentation and keyboard interaction examples.

The story should demonstrate and document accessibility features:

  1. Keyboard navigation patterns
  2. ARIA attributes
  3. Focus management behavior

Here's a suggested implementation:

 export default {
     title: 'WIP/RovingFocusGroup',
     component: RovingFocusGroup,
+    parameters: {
+        docs: {
+            description: {
+                component: `
+                    RovingFocusGroup implements the roving tabindex pattern for keyboard navigation.
+                    
+                    Keyboard Interactions:
+                    - Arrow Left/Right: Navigate horizontally
+                    - Arrow Up/Down: Navigate vertically
+                    - Home: Move focus to first item
+                    - End: Move focus to last item
+                    
+                    ARIA Attributes:
+                    - role="group" on the root
+                    - role="menuitem" on items
+                    - aria-orientation on groups
+                `
+            }
+        }
+    },
     render: () => <>

37-42: 🛠️ Refactor suggestion

Expand story variants to demonstrate different navigation patterns.

The current All variant doesn't showcase different navigation patterns and configurations.

Here's a suggested implementation:

-export const All = {
+export const Horizontal = {
+    parameters: {
+        docs: {
+            description: {
+                story: 'Horizontal navigation using left/right arrow keys'
+            }
+        }
+    },
     args: {
-        className: ''
+        orientation: 'horizontal',
+        loop: true
     }
 };
+
+export const Vertical = {
+    parameters: {
+        docs: {
+            description: {
+                story: 'Vertical navigation using up/down arrow keys'
+            }
+        }
+    },
+    args: {
+        orientation: 'vertical',
+        loop: true
+    }
+};
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 698dc9a and 23d254e.

📒 Files selected for processing (5)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/index.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (1)
src/core/utils/RovingFocusGroup/index.tsx (1)

5-9: LGTM! Clean compound component pattern implementation.

The utility object follows the React compound component pattern, making the API clean and intuitive to use.

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

🧹 Nitpick comments (1)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1)

31-31: Remove console.log statement

Remove debug console.log statements before merging to production.

-            console.log('Tab');
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 23d254e and 650630c.

📒 Files selected for processing (7)
  • src/components/ui/Badge/Badge.tsx (0 hunks)
  • src/components/ui/Badge/fragments/BadgeRoot.tsx (0 hunks)
  • src/components/ui/Badge/stories/Badge.stories.tsx (2 hunks)
  • src/core/utils/RovingFocusGroup/context/RovingFocuGroupContext.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1 hunks)
💤 Files with no reviewable changes (2)
  • src/components/ui/Badge/fragments/BadgeRoot.tsx
  • src/components/ui/Badge/Badge.tsx
✅ Files skipped from review due to trivial changes (2)
  • src/core/utils/RovingFocusGroup/context/RovingFocuGroupContext.tsx
  • src/components/ui/Badge/stories/Badge.stories.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (1)
src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1)

6-16: Implement core focus management functionality.

As a roving focus group component, this implementation is missing essential features:

  1. Keyboard navigation handlers (arrow keys)
  2. Focus management logic
  3. ARIA attributes for accessibility
  4. TypeScript types for orientation and direction

Here's a suggested implementation:

-const RovingFocusGroup = ({ children, ...props }: { children: React.ReactNode }) => {
+interface RovingFocusGroupProps {
+  children: React.ReactNode;
+  orientation?: 'horizontal' | 'vertical';
+  loop?: boolean;
+  dir?: 'ltr' | 'rtl';
+}
+
+const RovingFocusGroup = ({
+  children,
+  orientation = 'horizontal',
+  loop = true,
+  dir = 'ltr',
+  ...props
+}: RovingFocusGroupProps) => {
+  const handleKeyDown = (event: React.KeyboardEvent) => {
+    // Implement keyboard navigation logic
+  };
+
     return <RovingFocusGroupContext.Provider value={{ focusedItemId, setFocusedItemId }}>
-        <Primitive.div {...props}>
+        <Primitive.div
+            role="group"
+            onKeyDown={handleKeyDown}
+            data-orientation={orientation}
+            dir={dir}
+            {...props}
+        >
             {children}
         </Primitive.div>
     </RovingFocusGroupContext.Provider>;
 };

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

🧹 Nitpick comments (2)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (2)

18-21: Remove or replace leftover debugging statements.

The repeated console logging seems to be for debugging and could clutter production logs.

-        console.log('focusedItemId', focusedItemId);
-        console.log('groupRef', groupRef);
-        console.log('groupRef.current', groupRef.current);
+        // Remove or replace with a proper debug utility or logger if needed.

74-77: Replace placeholder tab logic.

The Tab key handling is just logging to the console. Either remove it or implement real tab behavior if required by your design.

 case 'Tab':
-    console.log('Tab');
     break;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 650630c and 0a19f0e.

📒 Files selected for processing (3)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (4)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (2)

5-5: Fix typo in import path (confirmed from past feedback).

This import path is missing the 's' in Focus. The correct path should be '../context/RovingFocusGroupContext'.


83-86: Confirm necessity of event.preventDefault() on focus.

Preventing the default focus behavior could interfere with standard browser accessibility and tabbing behavior. Verify that this is intended.

src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (2)

4-4: Fix typo in import path (confirmed from past feedback).

As noted previously, ensure the import uses RovingFocusGroupContext (with an 's') for correctness.


12-37: Looks good overall!

The component properly initializes focus items and sets the first item as focused by default. The loop property is passed down to context, which is handled in RovingFocusItem. No immediate issues here.

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 (3)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (2)

11-30: Remove debug statements or wrap them behind a development flag.

Console logs (console.log) within the effect can create noise in production. Consider removing these logs or wrapping them in a conditional check to avoid cluttering the console.


32-97: Refactor duplicated navigation logic.

The keydown navigation code for ArrowUp/ArrowLeft/ArrowDown/ArrowRight is largely repetitive. Consider extracting common code (e.g., finding the previous/next index) into helper functions to reduce duplication and simplify maintenance.

src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1)

9-37: Combine more usage examples or document different scenarios.

The default story effectively demonstrates two groups, but adding additional scenarios (e.g., dynamic item counts, disabled items) or further documentation can help highlight the roving focus functionality more thoroughly.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0a19f0e and 174f531.

📒 Files selected for processing (2)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (5)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (3)

5-5: Fix the import path to include the missing 's' in "Focus".

It appears that there's a spelling mismatch in the import statement: '../context/RovingFocuGroupContext'. Based on previous discussions, this should likely be '../context/RovingFocusGroupContext'.


99-102: Verify usage of preventDefault() on focus event.

Calling event.preventDefault() in a focus event might interfere with default focus behavior. Ensure this aligns with your design intentions, particularly if you want the element to naturally receive focus.


104-107: Add ARIA attributes to enhance accessibility.

Consider configuring accessibility attributes (e.g., role="menuitem", aria-selected) on the Primitive.button to ensure proper screen reader and keyboard navigation support.

src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (2)

40-46: Horizontal story looks good.


48-54: Vertical story implementation is clear and consistent.

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

🔭 Outside diff range comments (1)
src/components/ui/Tabs/fragments/TabTrigger.tsx (1)

10-18: ⚠️ Potential issue

Fix type error in TabTriggerProps interface.

There's a type error in the TabTriggerProps interface - setActiveTab is defined with React.Dispatch<Tab> but Tab is not defined. It should likely be React.Dispatch<React.SetStateAction<string>> or similar based on how it's used.

export type TabTriggerProps = {
    tab: TabProps;
-   setActiveTab: React.Dispatch<Tab>;
+   setActiveTab: React.Dispatch<React.SetStateAction<string>>;
    activeTab: TabProps;
    className?: string;
    customRootClass?: string;
    index: number;
    props?: Record<string, any>[]
}
♻️ Duplicate comments (1)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1)

5-5: ⚠️ Potential issue

Fix the typo in the import path.
The file name in the import statement appears to be missing an “s.” This has been mentioned in a previous review.

Apply this diff to correct the import path:

-import { RovingFocusGroupContext } from '../context/RovingFocuGroupContext';
+import { RovingFocusGroupContext } from '../context/RovingFocusGroupContext';
🧹 Nitpick comments (4)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (3)

11-27: Include all referenced variables in the dependency array.
The effect references groupRef, id, and addFocusItem but excludes them from the dependency array. This can lead to stale or inconsistent behavior.

 useEffect(() => {
     if (!focusItems.includes(id)) {
         addFocusItem(id);
     }
     if (focusedItemId === id) {
         if (groupRef.current) {
             const sanitizedId = CSS.escape(id);
             const item = groupRef.current.querySelector(`#${sanitizedId}`);
             if (item) {
                 item.focus();
             }
         }
     }
-}, [focusItems, focusedItemId]);
+}, [focusItems, focusedItemId, groupRef, id, addFocusItem]);

29-94: Remove leftover debug statement for the Tab key.
The console.log('Tab') might be a placeholder. Consider removing or replacing it with any required Tab-handling logic to keep production code clean.

 case 'Tab':
-    console.log('Tab');
     break;

101-103: Add appropriate ARIA attributes for consistent accessibility.
Since the parent uses role="tablist", consider marking individual items with role="tab" and indicate selection state.

 return <Primitive.button 
     asChild 
     onFocus={handleFocus} 
     tabIndex={focusedItemId === id ? 0 : -1} 
     ref={ref} 
     id={id} 
     onKeyDown={handleKeyDown}
-    {...props}>
+    role="tab"
+    aria-selected={focusedItemId === id}
+    {...props}>
     {children}
 </Primitive.button>;
src/components/ui/Tabs/fragments/TabTrigger.tsx (1)

27-36: Consider refactoring handleFocus function.

The handleFocus function contains redundant code and could be simplified:

  1. The check if (ref.current) is unnecessary as it's immediately followed by ref.current?.focus() which already has the optional chaining operator
  2. The comments are verbose and could be more concise
const handleFocus = (tab: TabProps) => {
-    if (ref.current) {
-        ref.current?.focus();
-    }
+    ref.current?.focus();
     setActiveTab(tab.value);
-
-    // This is a good way to manage flow, when a focus event is triggered, we can set the active tab to the tab that is being focused on
-    // This way, we dont need to keep track of the active tab in the parent component
-    // This should be the defacto pattern we should follow for all components
};
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 174f531 and ec0d4ad.

📒 Files selected for processing (4)
  • src/components/ui/Tabs/fragments/TabList.tsx (2 hunks)
  • src/components/ui/Tabs/fragments/TabRoot.tsx (2 hunks)
  • src/components/ui/Tabs/fragments/TabTrigger.tsx (3 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (9)
src/components/ui/Tabs/fragments/TabList.tsx (2)

7-8: Import statement looks good.
Using RovingFocusGroup here is consistent with the rest of the codebase. No issues detected.


21-23: Tab list container migration to RovingFocusGroup.Group
Replacing the <div> with <RovingFocusGroup.Group> is a good step towards better focus management. The role="tablist" and aria-orientation="horizontal" are correct for tab navigation.

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

8-8: New import for RovingFocusGroup is valid.
No concerns with the import path or usage here.


29-32: Adoption of RovingFocusGroup.Root
Wrapping the content with <RovingFocusGroup.Root> ensures cohesive focus handling for the tab structure. The usage here is appropriate, and the custom data attribute (data-accent-color) appears benign.

src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (2)

96-99: Revisit preventing default focus behavior.
Calling event.preventDefault() on a focus event can cause unexpected accessibility issues. Unless you have a specific reason, consider removing it to allow normal focus handling.

 const handleFocus = (event: React.FocusEvent<HTMLButtonElement>) => {
-    event.preventDefault();
     setFocusedItemId(id);
 };

106-108: Display name and export.
No issues with the displayName or default export.

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

22-22: Removed explicit keyboard navigation pattern.

The change simplifies the context usage by removing previousTab and nextTab functions, offloading this responsibility to the RovingFocusGroup component. This is a good architectural decision that centralizes keyboard navigation logic.


39-48:

❓ Verification inconclusive

Verify accessibility with RovingFocusGroup implementation.

The component now uses a composition pattern with RovingFocusGroup.Item and the asChild prop. This change removes the explicit keyboard event handling that was previously managing tab navigation, relying instead on the RovingFocusGroup utility.

Make sure the RovingFocusGroup implementation properly handles:

  1. Arrow key navigation between tabs
  2. Home/End key navigation
  3. Screen reader announcements
  4. Focus containment within the tab list

🏁 Script executed:

#!/bin/bash
# Check RovingFocusGroup.Item implementation for keyboard navigation
echo "Checking RovingFocusGroup.Item implementation:"
rg -A 20 "RovingFocusGroup.Item = " src/

# Check if there are any tests for RovingFocusGroup
echo -e "\nChecking for tests:"
fd "RovingFocusGroup.*test|test.*RovingFocusGroup" src/

Length of output: 258


Accessibility Verification: RovingFocusGroup Integration

The updated component now relies on the RovingFocusGroup’s built-in focus management via the composition pattern (with asChild), removing explicit keyboard event handling. Please ensure that the RovingFocusGroup implementation exhibits correct behavior regarding:

  • Arrow key navigation between tabs.
  • Home/End key navigation to jump to the first/last tab.
  • Screen reader announcements for state changes.
  • Focus containment within the tab list.

Note: Our search did not reveal any internal tests validating these accessibility features. It would be beneficial to manually verify these behaviors and consider adding tests if they are not already covered by the RovingFocusGroup utility.


8-9:

✅ Verification successful

Verify integration with RovingFocusGroup utility.

The new import of RovingFocusGroup represents a significant shift in how focus management is handled in the tabs component. This is a good architectural change that abstracts focus management logic.


🏁 Script executed:

#!/bin/bash
# Check how RovingFocusGroup is implemented and used in other components
echo "Checking RovingFocusGroup implementation:"
cat src/core/utils/RovingFocusGroup.tsx || cat src/core/utils/RovingFocusGroup/index.ts || fd "RovingFocusGroup" src/core/utils --exec cat {}

echo -e "\nChecking usage across the codebase:"
rg -t tsx "import .* RovingFocusGroup" src/ --stats

Length of output: 3981


I'll run an additional check to verify how the RovingFocusGroup is integrated within the Tabs component.


🏁 Script executed:

#!/bin/bash
# Inspect the content of TabTrigger to see RovingFocusGroup usage
echo "Inspecting RovingFocusGroup integration in TabTrigger.tsx:"
rg "RovingFocusGroup" src/components/ui/Tabs/fragments/TabTrigger.tsx -n

Length of output: 333


Integration with RovingFocusGroup verified successfully.

  • The import in src/components/ui/Tabs/fragments/TabTrigger.tsx (line 8) is correctly referencing the utility, and its usage (lines 39 and 48) aligns with the intended focus management abstraction.
  • The implementation of RovingFocusGroup within src/core/utils/RovingFocusGroup appears consistent with its usage in other parts of the codebase.

Overall, the architectural change to abstract focus management with RovingFocusGroup has been integrated properly.

@kotAPI kotAPI changed the title RovingFocusGroup [POC] RovingFocusGroup + Refactor Tabs component to use RovingFocusGroup Mar 8, 2025
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: 1

🧹 Nitpick comments (4)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (4)

7-7: Add extended props to support disabled/focusable states.

To align with best practices for roving focus items, consider adding props like disabled and focusable, as recommended in a past review.


34-99: Consider proactive handling of arrow key default behavior.

While you manage focus movement on arrow keys, you might also consider calling event.preventDefault() for arrow presses to prevent unintended native scrolling, especially for horizontally oriented groups.


93-95: Remove leftover debug statement.

Debug statements like console.log('Tab') can clutter logs in production.

-case 'Tab':
-    console.log('Tab');
-    break;
+case 'Tab':
+    // handle tab logic if needed
+    break;

106-108: Add accessibility attributes for better screen reader support.

AROA/ARIA attributes (e.g., role="menuitem" or aria-selected) should be added to help assistive technologies identify the focused element.

return <Primitive.button 
    asChild 
    onFocus={handleFocus} 
    tabIndex={focusedItemId === id ? 0 : -1} 
    ref={ref} 
    id={id} 
    onKeyDown={handleKeyDown}
+    role="menuitem"
+    aria-selected={focusedItemId === id}
    {...props}>
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ec0d4ad and 017d8f8.

📒 Files selected for processing (2)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (2)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (2)

5-5: Fix the typo in the import path.

The import path for RovingFocusGroupContext is missing an "s," which breaks the linkage.

-import { RovingFocusGroupContext } from '../context/RovingFocuGroupContext';
+import { RovingFocusGroupContext } from '../context/RovingFocusGroupContext';

101-104: Revisit the preventDefault() call on focus.

Preventing the default focus behavior can interfere with standard browser focus. Verify whether you need this or if you can rely on the default event handling.

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

♻️ Duplicate comments (1)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1)

13-22: 🛠️ Refactor suggestion

Add missing dependencies to the effect hook

addFocusItem and id are used inside the effect but aren’t listed in the dependencies array. This may lead to stale references or unexpected behavior.

 useEffect(() => {
     if (!focusItems.includes(id)) {
         addFocusItem(id);
     }
-}, [focusItems, focusedItemId]);
+}, [focusItems, focusedItemId, addFocusItem, id]);
🧹 Nitpick comments (5)
src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (1)

7-17: Consider adding an ARIA role or label to improve accessibility

Explicitly assigning a role (e.g., role="group") or relevant ARIA attributes to the container might help assistive technologies identify how to treat this interactive wrapper.

src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1)

95-97: Remove or replace console.log for production

Leaving a console log for the Tab key can clutter logs in production environments. Consider removing or using a more robust logging mechanism.

src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1)

16-18: Prevent duplicates in focusItems

The current approach appends all IDs to the list without checking for duplicates, which can cause repeated entries.

 const addFocusItem = (id: string) => {
-    setFocusItems((prev) => [...prev, id]);
+    setFocusItems((prev) => {
+       if (!prev.includes(id)) {
+         return [...prev, id];
+       }
+       return prev;
+    });
 };
src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (2)

1-52: Add usage instructions to demonstrate keyboard navigation

The story doesn't explicitly indicate to users how to interact with the component. Consider adding instructions about keyboard navigation, which is the core functionality of a RovingFocusGroup.

import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor';
import React from 'react';

import RovingFocusGroup from '../index';

import Button from '~/components/ui/Button/Button';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
    title: 'WIP/RovingFocusGroup',
    component: RovingFocusGroup,
+   parameters: {
+       docs: {
+           description: {
+               component: 'RovingFocusGroup manages keyboard navigation between focusable elements. Press Tab to enter a group, then use arrow keys to navigate between items within the group.'
+           }
+       }
+   },
    render: (args) => <>
        <SandboxEditor className="space-y-2">
+           <div className="mb-4 p-2 bg-gray-100 rounded">
+               <h3 className="font-bold">Keyboard Navigation:</h3>
+               <ul className="list-disc pl-5">
+                   <li>Press <kbd>Tab</kbd> to focus the first group</li>
+                   <li>Use <kbd>←</kbd> <kbd>→</kbd> or <kbd>↑</kbd> <kbd>↓</kbd> to navigate between items (depending on direction)</li>
+                   <li>Press <kbd>Tab</kbd> again to move to the next group</li>
+               </ul>
+           </div>
            <RovingFocusGroup.Root className="flex items-center gap-2" {...args}>

12-36: Consider adding customization controls for interactive testing

The story would be more useful if it allowed interactive testing of different properties. Consider exposing more controls via args.

export default {
    title: 'WIP/RovingFocusGroup',
    component: RovingFocusGroup,
+   argTypes: {
+       direction: {
+           control: 'radio',
+           options: ['horizontal', 'vertical'],
+           description: 'Navigation direction'
+       },
+       loop: {
+           control: 'boolean',
+           description: 'Whether focus should loop from last to first item'
+       }
+   },
    render: (args) => <>
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 017d8f8 and 29cb435.

📒 Files selected for processing (7)
  • src/components/ui/Tabs/fragments/TabContent.tsx (1 hunks)
  • src/components/ui/Tabs/fragments/TabRoot.tsx (2 hunks)
  • src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/ui/Tabs/fragments/TabRoot.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (11)
src/components/ui/Tabs/fragments/TabContent.tsx (2)

19-19: Clean removal of unused setActiveTab dependency.

The removal of setActiveTab from the context destructuring is a good cleanup that aligns with the refactoring to use RovingFocusGroup for focus management. The TabContent component only needs to read the current state (tabs and activeTab) without modifying it.


23-23: Potential type mismatch in activeTab comparison.

There appears to be a type inconsistency between how activeTab is typed and how it's used. The TabContentProps type defines activeTab as a TabProps object (line 11), but here it's compared against tab.value, suggesting activeTab might actually be a string/value identifier rather than a full tab object.

Consider adjusting either:

  1. The type definition to reflect that activeTab is a value identifier:
- activeTab: TabProps
+ activeTab: string | number
  1. Or the comparison logic if activeTab is indeed a full tab object:
- if (tab.value === activeTab) {
+ if (tab.value === activeTab.value) {
src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (1)

7-7: Looks good overall

No major issues spotted in the component logic or context usage.

src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (3)

5-5: Check for possible typo in import path

The import path is referencing '../context/RovingFocuGroupContext'. Verify if the file name is spelled “Focu” or “Focus.”

Do you want me to confirm the file name usage throughout the codebase?


103-106: Reevaluate preventing default on focus

Calling event.preventDefault() within the onFocus handler can block native focus behaviors. Ensure this is desired or remove it.


108-110: Add ARIA attributes to the button element for improved accessibility

src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1)

4-4: Check for possible typo in the import path

If the actual file is named RovingFocusGroupContext.ts, consider updating the path for consistency.

src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (4)

8-37: Add documentation parameters to the story

The story setup is functional but lacks documentation parameters that would explain the purpose and usage of the RovingFocusGroup component to other developers. Consider adding descriptive parameters to enhance documentation.

export default {
    title: 'WIP/RovingFocusGroup',
    component: RovingFocusGroup,
+   parameters: {
+       docs: {
+           description: {
+               component: 'RovingFocusGroup manages keyboard navigation between focusable elements, implementing the roving tabindex pattern for accessibility.'
+           }
+       }
+   },
    render: (args) => <>

14-34: Great implementation of example groups in RovingFocusGroup

The story effectively demonstrates the component with multiple groups and different types of focusable elements (buttons and links). The visual distinction between groups using colored borders is helpful.


39-45: Add documentation to story variants

While you've implemented the horizontal and vertical variants, they lack descriptive parameters explaining what each variant demonstrates and how to use them.

export const Horizontal = {
+   parameters: {
+       docs: {
+           description: {
+               story: 'Horizontal navigation using left/right arrow keys. Setting loop=true allows focus to wrap from last to first item.'
+           }
+       }
+   },
    args: {
        direction: 'horizontal',
        loop: true
    }
};

47-52: Add documentation to the Vertical variant

Similar to the Horizontal variant, the Vertical variant would benefit from documentation explaining its behavior.

export const Vertical = {
+   parameters: {
+       docs: {
+           description: {
+               story: 'Vertical navigation using up/down arrow keys. Setting loop=true allows focus to wrap from last to first item.'
+           }
+       }
+   },
    args: {
        direction: 'vertical',
        loop: true
    }
};

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

🧹 Nitpick comments (5)
src/components/tools/SandboxEditor/SandboxEditor.tsx (5)

25-32: Consider simplifying ColorSelect component

The ColorSelect component has been changed to return a <span> but it's later wrapped in a <button> when used within RovingFocusGroup.Item. This creates unnecessary nesting.

Consider having ColorSelect return null or just a color indicator and apply styling at the RovingFocusGroup.Item level:

const ColorSelect = ({ color, colorName, changeAccentColor }: ColorSelectProps) => {
    const dimensions = 32;
-    return <span
-        aria-label={`Change accent color to ${colorName}`}
-        className='cursor-pointer rounded-full inline-flex'
-        style={{ width: dimensions, height: dimensions, backgroundColor: color.light['900'] }}
-    ></span>;
+    return (
+        <div
+            style={{ width: dimensions, height: dimensions, backgroundColor: color.light['900'] }}
+        ></div>
+    );
};

71-90: Good implementation of RovingFocusGroup for color selection

The implementation of RovingFocusGroup enhances keyboard navigation and focus management for color selection. This improves accessibility by allowing users to navigate between color options using keyboard.

This is a good example of how RovingFocusGroup improves user interaction. Consider applying this pattern to other groups of interactive elements throughout the application that would benefit from improved keyboard navigation.


78-80: Enhance color selection feedback

The current implementation updates the color selection on both focus and click. For better user experience, consider adding some visual feedback to indicate which color is currently selected.

<RovingFocusGroup.Item
    asChild
    key={idx}
-   className='cursor-pointer rounded-full inline-block w-8 h-8 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
+   className={`cursor-pointer rounded-full inline-block w-8 h-8 focus:outline-none focus:ring-2 focus:ring-offset-2 ${colorName === color ? 'ring-2 ring-offset-2 ring-blue-500' : 'focus:ring-blue-500'}`}
    onFocus={() => setColorName(colorName)}
    onClick={() => setColorName(colorName)}
>

23-23: Unused parameter in ColorSelect props

The changeAccentColor parameter in ColorSelect props is defined but never used. It seems redundant since the color change is now handled by the RovingFocusGroup.Item's onClick and onFocus handlers.

-type ColorSelectProps = {color:typeof colors[keyof typeof colors], colorName: string, changeAccentColor: (colorName: string)=>void}
+type ColorSelectProps = {color:typeof colors[keyof typeof colors], colorName: string}

And update usage:

-<ColorSelect colorName={color} color={colors[colorName]}/>
+<ColorSelect colorName={color} color={colors[colorName]}/>

43-45: Remove empty useEffect

There's an empty useEffect hook that doesn't perform any actions. This should be removed if it's not needed.

-useEffect(() => {
-
-}, []);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 29cb435 and 0821d28.

📒 Files selected for processing (1)
  • src/components/tools/SandboxEditor/SandboxEditor.tsx (4 hunks)
🧰 Additional context used
🪛 GitHub Check: lint
src/components/tools/SandboxEditor/SandboxEditor.tsx

[warning] 1-1:
'forwardRef' is defined but never used

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
src/components/tools/SandboxEditor/SandboxEditor.tsx (2)

7-8: Good addition of RovingFocusGroup import

The import of RovingFocusGroup aligns with the PR objective to refactor components to use the new roving focus management utility.


59-59: Button variant change to 'solid'

The button variant has been changed from "outline" to "solid" for the dark mode toggle, which provides better visual prominence.

@@ -1,9 +1,11 @@
import React, { PropsWithChildren, useEffect, useState } from 'react';
import React, { forwardRef, PropsWithChildren, useEffect, useState } from 'react';
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

Remove unused import 'forwardRef'

The forwardRef import is defined but never used in this component.

-import React, { forwardRef, PropsWithChildren, useEffect, useState } from 'react';
+import React, { PropsWithChildren, useEffect, useState } from 'react';
📝 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
import React, { forwardRef, PropsWithChildren, useEffect, useState } from 'react';
import React, { PropsWithChildren, useEffect, useState } from 'react';
🧰 Tools
🪛 GitHub Check: lint

[warning] 1-1:
'forwardRef' is defined but never used

Comment on lines +82 to +84
<button>
<ColorSelect colorName={color} color={colors[colorName]}/>
</button>
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

Preserve aria-label for accessibility

The aria-label from ColorSelect is lost because it's wrapped in a button that doesn't forward the attributes.

<button
+    aria-label={`Change accent color to ${color}`}
>
    <ColorSelect colorName={color} color={colors[colorName]}/>
</button>
📝 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
<button>
<ColorSelect colorName={color} color={colors[colorName]}/>
</button>
<button aria-label={`Change accent color to ${color}`}>
<ColorSelect colorName={color} color={colors[colorName]}/>
</button>

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

🧹 Nitpick comments (5)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (4)

8-10: Consider extending the props for configurable interactions.

Defining props such as disabled or focusable can make this component more flexible, align with usage patterns across the codebase, and allow conditionally disabling focus logic.


28-39: Use optional chaining for clarity.

Instead of checking if (groupRef && groupRef.current), consider using optional chaining (groupRef?.current) to reduce nesting and improve readability.

- if (groupRef && groupRef.current) {
+ if (groupRef?.current) {
     setFocusedItemId(id);
     // ...
 }
🧰 Tools
🪛 Biome (1.9.4)

[error] 29-29: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


40-105: Refine handling of the Tab key.

Currently, pressing Tab logs a message without altering focus behavior. If you intend to allow default tabbing, remove the console statement; otherwise, implement the desired focus changes or constraints.

-case 'Tab':
-    console.log('Tab');
-    break;
+case 'Tab':
+    // Let the default browser tab behavior proceed
+    break;

112-115: Add accessibility attributes for better screen reader support.

Providing ARIA attributes like role, aria-selected, and others can improve the user experience for assistive technologies. Consider specifying the exact role and selection state to reflect the item’s focus or selection status.

<Primitive.button 
    asChild 
    onFocus={handleFocus} 
    tabIndex={focusedItemId === id ? 0 : -1} 
    ref={ref} 
    id={id} 
    onKeyDown={handleKeyDown}
+    role="menuitem"
+    aria-selected={focusedItemId === id}
    {...props}
>
    {children}
</Primitive.button>
src/core/utils/RovingFocusGroup/context/RovingFocusGroupContext.tsx (1)

14-21: Optional improvement: Throw an error in default context values to prevent silent misconfigurations.

Currently, the default context values (like setFocusedItemId: () => {}) silently no-op if the component isn’t wrapped by a provider. Consider throwing an error in these methods to alert maintainers that they must wrap usage in the provider.

 export const RovingFocusGroupContext = createContext<RovingFocusGroupContextTypes>({
     focusedItemId: null,
-    setFocusedItemId: () => {},
+    setFocusedItemId: () => {
+        throw new Error('RovingFocusGroupContext missing provider');
+    },
     // ...
 });
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0821d28 and 504fb74.

📒 Files selected for processing (5)
  • src/core/utils/RovingFocusGroup/context/RovingFocusGroupContext.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx
🧰 Additional context used
🧬 Code Definitions (1)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (2)
src/core/utils/RovingFocusGroup/context/RovingFocusGroupContext.tsx (2) (2)
  • RovingFocusGroupContextTypes (4-11)
  • RovingFocusGroupContext (14-21)
src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx (2) (2)
  • RovingFocusRootContextTypes (4-7)
  • RovingFocusRootContext (10-13)
🪛 Biome (1.9.4)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx

[error] 29-29: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (3)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (3)

107-110: Verify the usage of event.preventDefault() during focus.

Calling event.preventDefault() inside onFocus is unusual and may disrupt built-in focus handling. Double-check whether this is intended or if it should be removed for expected tab navigation behavior.


117-119: LGTM: The displayName and default export are clear.

Defining a displayName is helpful for debugging. This finalizes the component setup nicely.


1-6:

✅ Verification successful

Confirm React version support for useId.

Since useId is only available in React 18 or higher, please confirm that your project is running at least React 18 to avoid runtime errors.


🌐 Web query:

Does React 17 provide the `useId` hook or is it only available from React 18 onwards?

💡 Result:

The useId hook is only available in React 18 and later versions. React 17 does not include this hook. Here's the breakdown:

  • Introduced in React 18: The useId hook was added in React 18 to generate stable, unique IDs across client-server renders[1][3][11]. It addresses issues like duplicate IDs in reusable components and ensures hydration consistency[1][4].

  • React 17 Limitations: While React 17 included foundational updates like event system changes and JSX transform improvements[2][8][12], it lacked useId. Developers using React 17 had to rely on third-party solutions like @reach/auto-id for similar functionality[4].

  • Key Differences:

    React 17 React 18+
    No native unique ID generator useId built-in[1][11]
    Required manual ID management Automatic ID generation tied to component hierarchy[4][11]
    Risk of ID collisions in SSR Hydration-safe IDs with useId[1][4]

For projects using React 17, upgrading to React 18 is required to use useId natively[3][19], or alternatives like react-use-id-hook (via npm) can be used as a polyfill[7].

Citations:


Ensure Your Project Uses React 18 or Later for useId Support
Based on our verification, the useId hook is only available in React 18 or newer versions and is not present in React 17. Please confirm that your project's dependency is set to React 18+ (check your package.json) to avoid any runtime issues. If you are currently on React 17, consider upgrading or implementing a polyfill solution.

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

♻️ Duplicate comments (4)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (4)

17-26: ⚠️ Potential issue

Fix the dependency array in useEffect.

The useEffect hook is missing dependencies id and addFocusItem which are used inside the effect. This can lead to stale closures and missed updates.

useEffect(() => {
    // we check if the item is in the focusItems array, if not we add it
    if (!focusItems.includes(id)) {
        addFocusItem(id);
    }

    //

    //
-}, [focusItems, focusedItemId]);
+}, [focusItems, focusedItemId, id, addFocusItem]);

119-122: 🛠️ Refactor suggestion

Add disabled state handling in focus logic.

The component should respect a disabled state and not allow focus when disabled.

+interface RovingFocusItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+    children: React.ReactNode;
+    disabled?: boolean;
+}

// Then in the component implementation:

const handleFocus = (event: React.FocusEvent<HTMLButtonElement>) => {
    event.preventDefault();
-    setFocusedItemId(id);
+    if (!props.disabled) {
+        setFocusedItemId(id);
+    }
};

124-126: 🛠️ Refactor suggestion

Add ARIA attributes for better accessibility.

The button element is missing important ARIA attributes that help screen readers understand the role and state of the element.

-return <Primitive.button asChild onFocus={handleFocus} tabIndex={focusedItemId === id ? 0 : -1} ref={ref} id={id} onKeyDown={handleKeyDown} {...props}>
+return <Primitive.button 
+    asChild 
+    onFocus={handleFocus} 
+    tabIndex={focusedItemId === id ? 0 : -1} 
+    ref={ref} 
+    id={id} 
+    onKeyDown={handleKeyDown}
+    role="menuitem"
+    aria-selected={focusedItemId === id}
+    {...props}
+>
    {children}
</Primitive.button>;

8-10: 🛠️ Refactor suggestion

Enhance component props interface with additional configuration.

The current props interface is limited and doesn't include important properties for a proper roving focus implementation, like disabled state.

-type RovingFocusItemProps = {
-    children: React.ReactNode;
-} & React.ButtonHTMLAttributes<HTMLButtonElement>;
+interface RovingFocusItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+    children: React.ReactNode;
+    disabled?: boolean;
+    focusable?: boolean;
+}
🧹 Nitpick comments (8)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (3)

23-25: Remove empty comment lines.

These empty comment lines don't provide any value and should be removed.

    if (!focusItems.includes(id)) {
        addFocusItem(id);
    }

-    //
-
-    //
}, [focusItems, focusedItemId]);

29-30: Use optional chaining for cleaner code.

The current null check can be simplified using optional chaining for better readability.

-    const focusItemWithId = (id: string) => {
-        if (groupRef && groupRef.current) {
-            setFocusedItemId(id);
+    const focusItemWithId = (id: string) => {
+        setFocusedItemId(id);
+        if (groupRef?.current) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 29-29: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


110-113: Remove console.log statement.

There's a console.log statement that appears to be leftover debugging code and should be removed. Consider implementing proper Tab key handling instead.

case 'Tab':
-    // Logic to handle tab key if needed
-    console.log('Tab');
    break;
src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (5)

9-12: Type property mismatch: direction vs orientation

The StoryArgs type defines direction but the previous review comments mentioned orientation. Ensure consistency in the API naming throughout the component.

 type StoryArgs = {
-    direction?: 'horizontal' | 'vertical';
+    orientation?: 'horizontal' | 'vertical';
     loop?: boolean;
 };

 // In Horizontal component
 args: {
-    direction: 'horizontal',
+    orientation: 'horizontal',
     loop: true
 }

Also applies to: 48-49


16-17: Add documentation parameters to improve component understanding

The story lacks documentation describing what the RovingFocusGroup component does. Consider adding parameters with a description explaining the component's purpose and accessibility benefits.

 export default {
     title: 'WIP/RovingFocusGroup',
     component: RovingFocusGroup,
+    parameters: {
+        docs: {
+            description: {
+                component: 'RovingFocusGroup implements the roving tabindex pattern for keyboard navigation between elements. It prevents default browser scrolling when using arrow keys and manages focus order.'
+            }
+        }
+    },
     render: (args: StoryArgs) => <>

29-29: Add aria-label to link for accessibility

Links should have descriptive labels for screen readers. Since this is a demonstration of an accessibility pattern, it's important to model best practices.

-<a href="#" className="border border-green-500">Link 1 Group 1</a>
+<a href="#" className="border border-green-500" aria-label="Link 1 Group 1">Link 1 Group 1</a>

18-42: Consider consistency in story render implementation

The default export uses a function component syntax while KeyboardNavigation uses an object with a render property. For consistency, consider using the same pattern throughout.

export default {
    title: 'WIP/RovingFocusGroup',
    component: RovingFocusGroup,
-    render: (args: StoryArgs) => <>
+    render: (args: StoryArgs) => (
         <SandboxEditor className="space-y-2">
             {/* ... */}
         </SandboxEditor>
-    </>
+    )
};

Also applies to: 62-153


1-7: Consider adding missing import documentation

Since this is a new component that others will reference, add a brief comment explaining what RovingFocusGroup is and how it relates to this story file.

 import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor';
 import React from 'react';

+/**
+ * RovingFocusGroup is a utility that implements the roving tabindex pattern
+ * for keyboard navigation. It allows users to navigate between elements
+ * using arrow keys without triggering page scrolling.
+ */
 import RovingFocusGroup from '../index';

 import Button from '~/components/ui/Button/Button';
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 504fb74 and 69903c5.

📒 Files selected for processing (2)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (2)
src/core/utils/RovingFocusGroup/context/RovingFocusGroupContext.tsx (2) (2)
  • RovingFocusGroupContextTypes (4-11)
  • RovingFocusGroupContext (14-21)
src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx (2) (2)
  • RovingFocusRootContextTypes (4-7)
  • RovingFocusRootContext (10-13)
🪛 Biome (1.9.4)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx

[error] 29-29: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (2)
src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (2)

46-58: Expand documentation for the orientation variants

The current story variants don't provide documentation to explain their purpose. Consider adding parameter descriptions for both stories.

export const Horizontal = {
+    parameters: {
+        docs: {
+            description: {
+                story: 'Demonstrates horizontal navigation with left/right arrow keys. The loop parameter ensures focus wraps from last to first item and vice versa.'
+            }
+        }
+    },
    args: {
        direction: 'horizontal',
        loop: true
    }
};

export const Vertical = {
+    parameters: {
+        docs: {
+            description: {
+                story: 'Demonstrates vertical navigation with up/down arrow keys. The loop parameter ensures focus wraps from last to first item and vice versa.'
+            }
+        }
+    },
    args: {
        direction: 'vertical',
        loop: true
    }
};

60-154: Good implementation of comprehensive keyboard navigation example

The KeyboardNavigation story provides an excellent demonstration of the component's functionality with clear explanations and multiple scenarios. The descriptions and visual indicators make it easy to understand how the component prevents default scrolling behavior while enabling keyboard navigation.

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 (6)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1)

67-77: Consider using optional chaining for cleaner code

You can simplify the null check by using optional chaining.

-    const focusItemWithId = (id: string) => {
-        if (groupRef && groupRef.current) {
-            setFocusedItemId(id);
-            // Sanitize the id to ensure it's a valid CSS selector
-            const sanitizedId = CSS.escape(id);
-            const item = groupRef.current.querySelector(`#${sanitizedId}`);
-            if (item) {
-                (item as HTMLElement).focus();
-            }
-        }
-    };
+    const focusItemWithId = (id: string) => {
+        if (groupRef?.current) {
+            setFocusedItemId(id);
+            // Sanitize the id to ensure it's a valid CSS selector
+            const sanitizedId = CSS.escape(id);
+            const item = groupRef.current.querySelector(`#${sanitizedId}`);
+            if (item) {
+                (item as HTMLElement).focus();
+            }
+        }
+    };
🧰 Tools
🪛 Biome (1.9.4)

[error] 68-68: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (5)

9-12: Type property name doesn't match component props.

The StoryArgs type uses direction but the component likely uses orientation based on the Storybook variants. This inconsistency could cause confusion.

type StoryArgs = {
-    direction?: 'horizontal' | 'vertical';
+    orientation?: 'horizontal' | 'vertical';
    loop?: boolean;
};

29-30: Replace empty href with more accessible alternative.

Using href="#" can cause page jumps and isn't ideal for accessibility. Consider using a button styled as a link or a more meaningful href.

-                        <a href="#" className="border border-green-500">Link 1 Group 1</a>
+                        <a href="javascript:void(0)" className="border border-green-500">Link 1 Group 1</a>

Alternatively, for a more semantic approach:

-                        <a href="#" className="border border-green-500">Link 1 Group 1</a>
+                        <button className="border border-green-500 text-blue-600 underline">Link 1 Group 1</button>

147-151: Consider a more accessible approach for demonstrating scroll prevention.

While adding empty space to make the page scrollable works for demonstration, it's not very screen-reader friendly. Consider using actual content or a more descriptive approach.

- {/* Add empty space to make the page scrollable */}
- <div className="h-[600px] bg-gray-100" />
+ {/* Add content to make the page scrollable */}
+ <div className="space-y-4 bg-gray-100 p-4">
+   <h3 className="text-md font-medium">Scrollable Content Area</h3>
+   <p>This area contains content to make the page scrollable for demonstration purposes.</p>
+   <div className="h-[500px] flex items-end">
+     <p className="text-sm text-gray-600">Scroll to bottom content...</p>
+   </div>
+ </div>

177-178: Use a consistent pattern for disabled items.

You're using both inline styles and the Button component's built-in disabled state. Consider adopting a consistent approach across all examples.

If the Button component handles disabled styling internally:

- <Button disabled className="opacity-50 cursor-not-allowed">Disabled Item</Button>
+ <Button disabled>Disabled Item</Button>

If not, ensure all disabled buttons use the same pattern:

- <Button disabled>Admin (Disabled)</Button>
+ <Button disabled className="opacity-50 cursor-not-allowed">Admin (Disabled)</Button>

Also applies to: 183-184, 205-206, 211-212, 230-231, 236-237, 292-293


19-41: Consider adding a basic description to the default story.

The default render function shows the component structure well, but lacks a description explaining what's being demonstrated (multiple groups within a single focus root).

export default {
    title: 'WIP/RovingFocusGroup',
    component: RovingFocusGroup,
+   parameters: {
+       docs: {
+           description: {
+               component: 'RovingFocusGroup manages keyboard navigation within and between groups of focusable elements, implementing the roving tabindex pattern.'
+           }
+       }
+   },
    render: (args: StoryArgs) => <>
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 69903c5 and aea1bc2.

📒 Files selected for processing (7)
  • src/core/utils/RovingFocusGroup/context/RovingFocusGroupContext.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/index.tsx (1 hunks)
  • src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx
  • src/core/utils/RovingFocusGroup/context/RovingFocusGroupContext.tsx
🧰 Additional context used
🧬 Code Definitions (3)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (2)
src/core/utils/RovingFocusGroup/context/RovingFocusGroupContext.tsx (1) (1)
  • RovingFocusGroupContext (26-33)
src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx (1) (1)
  • RovingFocusRootContext (17-20)
src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (1)
src/core/utils/RovingFocusGroup/context/RovingFocusRootContext.tsx (1) (1)
  • RovingFocusRootContext (17-20)
src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (1)
src/core/utils/RovingFocusGroup/context/RovingFocusGroupContext.tsx (1) (1)
  • RovingFocusGroupContext (26-33)
🪛 Biome (1.9.4)
src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx

[error] 68-68: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (11)
src/core/utils/RovingFocusGroup/index.tsx (2)

5-44: Well-documented component with comprehensive JSDoc

The JSDoc documentation for this component is excellent. It clearly explains the purpose, key features, and accessibility benefits of the RovingFocusGroup utility. The code example is particularly helpful for understanding how to use the component.


45-49: Clean and consistent component composition pattern

Good job using the compound component pattern here. This approach of exposing Root, Group, and Item as properties of a single exported object provides a clean and intuitive API for consumers.

src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx (2)

15-21: Well-defined props interface with thorough documentation

The props interface is clearly defined with good JSDoc comments. The optional props have sensible defaults, and the component extends HTMLDivElement attributes to ensure full compatibility with standard HTML attributes.


50-60: Proper context implementation and ARIA attributes

The component correctly uses the RovingFocusRootContext provider to pass configuration values down to child components. The ARIA attributes are properly applied for accessibility, matching the WAI-ARIA best practices mentioned in the documentation.

src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx (3)

85-117: Robust implementation of enabled item finding logic

The findEnabledItemIndex function is well-implemented with thorough handling of edge cases:

  1. Properly respects the loop configuration
  2. Avoids infinite loops with a boundary check
  3. Correctly identifies disabled items
  4. Uses CSS.escape for secure DOM querying

This is a solid implementation that ensures reliable keyboard navigation.


175-226: Comprehensive keyboard navigation implementation

The keyboard event handling is excellent, with support for:

  1. All directional arrow keys
  2. Home and End keys for jumping to first/last items
  3. Direction-aware navigation (horizontal/vertical)
  4. Preventing default scrolling behavior
  5. Properly skipping disabled items

This implementation follows accessibility best practices and provides a great user experience.


238-254: Complete ARIA attributes for accessibility

The component correctly implements all necessary ARIA attributes:

  • role="option"
  • aria-selected
  • aria-disabled
  • aria-label and aria-labelledby support
  • Proper tabIndex management

This ensures the component is fully accessible to screen readers and keyboard users.

src/core/utils/RovingFocusGroup/fragments/RovingFocusGroup.tsx (2)

38-55: Well-implemented focus state management

The state management for focus items is well-implemented:

  1. The focusItems array tracks all available items
  2. The addFocusItem function correctly adds new items
  3. The effect to set initial focus works properly

This ensures that focus management is reliable and predictable.


66-77: Proper context provider and ARIA attributes

The component correctly provides the focus management context to child components and applies appropriate ARIA attributes for accessibility. The use of a ref to access the DOM node is also a good practice for complex interactions.

src/core/utils/RovingFocusGroup/stories/RovingFocusGroup.stories.tsx (2)

16-17: Add documentation parameters to story variants.

The story components lack descriptive documentation. Adding parameters with descriptions would help users understand the purpose of each variant.

export const Horizontal = {
+    parameters: {
+        docs: {
+            description: {
+                story: 'Horizontal navigation using left/right arrow keys'
+            }
+        }
+    },
    args: {
        direction: 'horizontal',
        loop: true
    }
};

export const Vertical = {
+    parameters: {
+        docs: {
+            description: {
+                story: 'Vertical navigation using up/down arrow keys'
+            }
+        }
+    },
    args: {
        direction: 'vertical',
        loop: true
    }
};

Also applies to: 46-58


15-43: Excellent demonstration of RovingFocusGroup capabilities.

The stories provide comprehensive examples of:

  • Different orientations (horizontal/vertical)
  • Keyboard navigation behavior
  • Handling of disabled items
  • Accessibility features with ARIA attributes

This is a significant improvement over previous versions and will be very helpful for developers using this component.

Also applies to: 61-154, 157-256, 259-330

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)
src/components/ui/Tabs/fragments/TabTrigger.tsx (1)

39-47: Add explicit button type for clarity.

Wrapping the tab trigger in <RovingFocusGroup.Item> is a solid approach for roving focus. However, adding a type="button" to the <button> element ensures it won’t inadvertently submit forms in nested contexts.

 <RovingFocusGroup.Item
   onFocus={() => handleFocus(tab)}
 >
-  <button
+  <button type="button"
     ref={ref}
     className={clsx(`${rootClass}-trigger`, `${isActive ? 'active' : ''}`, className)}
     role="tab" {...props}
  >
    {tab.label}
  </button>
</RovingFocusGroup.Item>
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between aea1bc2 and 01d80d5.

📒 Files selected for processing (2)
  • src/components/tools/SandboxEditor/SandboxEditor.tsx (4 hunks)
  • src/components/ui/Tabs/fragments/TabTrigger.tsx (3 hunks)
🔇 Additional comments (7)
src/components/ui/Tabs/fragments/TabTrigger.tsx (2)

8-8: Good addition of the RovingFocusGroup import.

Importing RovingFocusGroup aligns with the new focus management approach in this pull request. This import is used effectively below, so no issues here.


22-22: Confirm removal of keyboard navigation logic.

Previously, tab navigation might have relied on previousTab or nextTab from TabsRootContext. Verify that removing these does not regress the keyboard navigation experience. If this logic has been superseded by RovingFocusGroup, no further action is required.

src/components/tools/SandboxEditor/SandboxEditor.tsx (5)

1-1: Remove unused ‘forwardRef’ import.

As noted in a previous review, this import is still unused.

-import React, { forwardRef, PropsWithChildren, useEffect, useState } from 'react';
+import React, { PropsWithChildren, useEffect, useState } from 'react';

7-7: No issues with importing RovingFocusGroup.

This supports the new roving focus management.


27-31: Ensure semantic clarity for the color swatch.

Turning ColorSelect into a <span> is acceptable since the focusable wrapper is provided externally. Just confirm the aria-label is effectively read out when tabbing onto the outer <button> that now contains ColorSelect.


59-59: Confirm design intent for changing the variant to "solid".

Switching to a "solid" variant can affect the UI. Ensure this aligns with the dark-mode toggle design.


71-89: RovingFocusGroup usage looks correct.

Wrapping color swatches in RovingFocusGroup.Group and using RovingFocusGroup.Item is a solid approach for keyboard navigation. The onFocus and onClick both set the color, which should improve accessibility.

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