Skip to content

Comments

keyboard navigation#521

Merged
MrgSub merged 3 commits intostagingfrom
keyboard-navigation
Mar 26, 2025
Merged

keyboard navigation#521
MrgSub merged 3 commits intostagingfrom
keyboard-navigation

Conversation

@hiheyhello123
Copy link
Contributor

@hiheyhello123 hiheyhello123 commented Mar 26, 2025

  • Quick action buttons
  • Keyboard navigation

Summary by CodeRabbit

  • New Features

    • Enhanced email thread interactions with quick action buttons for archiving, marking as read, deleting, and replying directly from the mail list.
    • Improved navigation experience through refined mouse hover and keyboard controls for smoother, more intuitive thread selection.
    • Introduced a custom hook for managing keyboard navigation and quick actions within email threads.
  • Bug Fixes

    • Resolved issues related to context menu functionality by removing outdated components.
  • Localization

    • Added new entries for "Mark as read" in the localization file to enhance user interface clarity.

@vercel
Copy link

vercel bot commented Mar 26, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
0 ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 26, 2025 6:56pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 26, 2025

Walkthrough

The changes remove legacy context menu components by deleting the ThreadContextMenu and Radix UI–based context menu files. Enhancements in the mail list include additional state management for hover, keyboard focus, and quick actions. New functionality is introduced via a dedicated MailQuickActions component that provides quick action buttons for threads, and a custom hook useMailNavigation to manage keyboard navigation and quick action selection.

Changes

File(s) Change Summary
apps/mail/.../context/thread-context.tsx
apps/mail/.../ui/context-menu.tsx
Removed legacy context menu components and associated interfaces used for email thread actions.
apps/mail/.../mail/mail-list.tsx Updated Thread and MailList with new state properties (hover, keyboard focus, quick action mode), added handleNavigateToThread method, and integrated the useMailNavigation hook.
apps/mail/.../mail/mail-quick-actions.tsx Added new MailQuickActions component to handle thread actions such as archive, toggle read, delete, and quick reply with conditional rendering and error handling.
apps/mail/.../hooks/use-mail-navigation.ts Introduced a custom hook to manage keyboard navigation and quick action states for email threads, including handling mouse and keyboard events.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant ML as MailList
    participant NM as useMailNavigation
    participant T as Thread Component
    participant QA as MailQuickActions

    U->>ML: Hover/Click on a thread
    ML->>NM: Invoke handleMouseEnter
    NM->>T: Set focused & hovered state
    T->>QA: Render quick action buttons if active
    U->>QA: Trigger quick action (archive, delete, etc.)
    QA->>T: Execute thread action update
Loading

Possibly related PRs

Suggested reviewers

  • MrgSub
  • nizzyabi
  • ahmetskilinc

Poem

I'm a rabbit hopping through the code,
Changes sprout like carrots in a row.
Removed old paths to clear the way,
New quick actions now lead the play.
With hooks and lists, the maze is tamed,
My floppy ears twitch—code's never the same!
🥕 Hop on and code away!

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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 plan to trigger planning for file edits and PR creation.
  • @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: 1

🧹 Nitpick comments (7)
apps/mail/components/mail/mail-quick-actions.tsx (2)

60-87: Consider matching error message to action type.

The error handling in handleArchive uses a generic error message regardless of whether it's an archive or unarchive action.

- console.error('Error archiving thread', error);
+ console.error(`Error ${isArchiveFolder ? 'unarchiving' : 'archiving'} thread`, error);

162-167: Localize action labels for internationalization support.

The quickActions array uses string literals for labels instead of using the translation function t which is already imported and used elsewhere in the component.

const quickActions = [
-  { action: handleArchive, icon: Archive, label: isArchiveFolder || !isInbox ? 'Unarchive' : 'Archive' },
-  { action: handleToggleRead, icon: Mail, label: message.unread ? 'Mark as read' : 'Mark as unread' },
-  { action: handleDelete, icon: Trash, label: 'Delete' },
-  { action: handleQuickReply, icon: Reply, label: 'Reply' }
+  { action: handleArchive, icon: Archive, label: isArchiveFolder || !isInbox ? t('common.mail.unarchive') : t('common.mail.archive') },
+  { action: handleToggleRead, icon: Mail, label: message.unread ? t('common.mail.markAsRead') : t('common.mail.markAsUnread') },
+  { action: handleDelete, icon: Trash, label: t('common.mail.delete') },
+  { action: handleQuickReply, icon: Reply, label: t('common.mail.reply') }
];
apps/mail/hooks/use-mail-navigation.ts (3)

20-24: Consider handling keyboard focus persistence on item changes.

The effect resets focus when items change, but only if keyboard navigation is not active. This might lead to unexpected behavior if items are filtered or sorted while navigating.

Consider adding a more robust approach to maintaining focus position when items change, such as tracking the focused item's ID rather than just its index.


33-36: Fragile DOM element selection method.

The getThreadElement function relies on CSS selectors to find DOM elements, which can be brittle if the DOM structure changes.

Consider using React refs passed down to thread components instead of relying on DOM queries, or at least adding error handling for cases when elements aren't found.


38-52: Potential performance issue with scroll calculation.

The scrollIntoView function uses getBoundingClientRect() which can cause layout thrashing when scrolling quickly through a large list.

Consider using the IntersectionObserver API for better performance:

const createObserver = (index: number) => {
  const threadElement = getThreadElement(index);
  if (!threadElement || !containerRef.current) return;
  
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) {
          threadElement.scrollIntoView({
            block: 'nearest',
            behavior: 'smooth'
          });
          observer.disconnect();
        }
      });
    },
    { root: containerRef.current, threshold: 1.0 }
  );
  
  observer.observe(threadElement);
  return observer;
};
apps/mail/components/mail/mail-list.tsx (2)

67-92: Duplicate hover state tracking.

The component uses both isHovering.current (a ref) and isHovered (state) to track hovering, which could lead to inconsistencies.

Consider consolidating the hover tracking to use only the state variable, which would simplify the code and ensure consistent behavior:

const handleMouseEnter = () => {
  if (demo) return;
- isHovering.current = true;
  setIsHovered(true);

  // Prefetch only in single select mode
- if (selectMode === 'single' && sessionData?.userId && !hasPrefetched.current) {
+ if (selectMode === 'single' && sessionData?.userId && !hasPrefetched.current) {
    // Clear any existing timeout
    if (hoverTimeoutRef.current) {
      clearTimeout(hoverTimeoutRef.current);
    }

    // Set new timeout for prefetch
    hoverTimeoutRef.current = setTimeout(() => {
-     if (isHovering.current) {
+     if (isHovered) {
        const messageId = message.threadId ?? message.id;
        // Only prefetch if still hovering and hasn't been prefetched
        console.log(
          `🕒 Hover threshold reached for email ${messageId}, initiating prefetch...`,
        );
        void preloadThread(sessionData.userId, messageId, sessionData.connectionId ?? '');
        hasPrefetched.current = true;
      }
    }, HOVER_DELAY);
  }
};

373-376: Potential side effect in mail click handler.

Adding handleMouseEnter(message.id) to the click handler might cause unexpected side effects if the behavior of handleMouseEnter changes.

Consider separating the concerns by not mixing mouse enter handler behavior with click handling, or add a comment explaining the intentional behavior.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between de4231d and c3887fd.

📒 Files selected for processing (5)
  • apps/mail/components/context/thread-context.tsx (0 hunks)
  • apps/mail/components/mail/mail-list.tsx (11 hunks)
  • apps/mail/components/mail/mail-quick-actions.tsx (1 hunks)
  • apps/mail/components/ui/context-menu.tsx (0 hunks)
  • apps/mail/hooks/use-mail-navigation.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • apps/mail/components/context/thread-context.tsx
  • apps/mail/components/ui/context-menu.tsx
🔇 Additional comments (29)
apps/mail/components/mail/mail-quick-actions.tsx (8)

3-15: Appropriate imports and dependencies.

The component has all the necessary imports for its functionality, with a good organization of React core imports first, followed by UI components, utilities, and type definitions.


16-23: Well-structured props interface with good type definitions.

The MailQuickActionsProps interface provides clear typing for the component with proper optional properties, making it flexible and reusable.


25-44: Good component declaration with proper memoization and default values.

The component is memoized for performance optimization, and default values are provided for optional props. The parameter destructuring with defaults is a clean pattern.


45-58: Effective thread closing logic.

The closeThreadIfOpen function properly handles the cleanup by removing the threadId parameter from the URL and resetting navigation state when provided.


89-123: Comprehensive read/unread toggling with proper error handling.

The handleToggleRead function properly handles both marking as read and unread with appropriate user feedback and error handling.


125-148: Effective thread deletion implementation.

The handleDelete function correctly moves threads to trash with proper error handling and user feedback.


169-199: Well-structured conditional rendering with appropriate styling.

The component renders nothing when not hovered or in quick action mode, and otherwise displays a clean, properly styled container with the action buttons. The styling conditionally applies focus styles when in quick action mode.


202-202: Good practice setting displayName for debugging.

Setting displayName helps with component identification in React DevTools and error messages.

apps/mail/hooks/use-mail-navigation.ts (11)

1-11: Well-defined hook interface with appropriate imports.

The hook imports necessary React hooks and defines a clear interface for its props.


12-19: Good state management setup with appropriate refs.

The hook initializes state variables and refs for tracking focused index, quick action mode, hover state, and keyboard navigation state.


26-31: Clean navigation reset function.

The resetNavigation function properly clears all navigation state.


54-66: Effective thread navigation with appropriate mail state updates.

The navigateToThread function correctly handles thread navigation and mail state updates.


68-116: Well-implemented arrow key navigation with hover integration.

The arrow key navigation functions properly handle both keyboard and hover states, with good prevention of default browser behavior.


117-132: Robust Enter key handling for selection and action triggering.

The handleEnter function correctly handles both regular selection and quick action activation.


134-158: Well-implemented quick action mode toggling and navigation.

The Tab and arrow key handlers for quick action mode provide good navigation between quick actions.


160-170: Clean escape key handling for progressive state reset.

The handleEscape function properly handles different states, first exiting quick action mode and then resetting focus.


172-178: Comprehensive keyboard shortcut registration.

All necessary keyboard shortcuts are registered using the useHotKey hook.


180-187: Mouse interaction handling with keyboard state awareness.

The handleMouseEnter function properly updates the hover state and resets keyboard navigation when the mouse is used.


189-197: Clean hook return value with all necessary state and handlers.

The hook returns all the required state and handlers for integrating with components.

apps/mail/components/mail/mail-list.tsx (10)

5-5: Good import organization with type imports separated.

The import statement correctly uses type for TypeScript imports and separates component imports from React hook imports.


24-25: Appropriate import of new components and hooks.

The new MailQuickActions component and useMailNavigation hook are properly imported.


28-45: Updated Thread component props with keyboard navigation support.

The Thread component now accepts additional props to support keyboard navigation and quick actions.


54-54: Added state for hover tracking.

The isHovered state is added to track hover state for the thread.


94-100: Updated mouse leave handler with hover state.

The mouse leave handler now also updates the isHovered state.


116-144: Well-integrated MailQuickActions component.

The MailQuickActions component is properly integrated with appropriate props for hover state, quick action mode, and keyboard navigation.


244-248: Clean thread navigation handler implementation.

The handleNavigateToThread function correctly updates URL parameters when navigating to a thread.


250-261: Well-structured mail navigation hook integration.

The useMailNavigation hook is properly initialized with the necessary props and its return values are destructured for use in the component.


445-445: Updated dependencies array with new handler.

The dependencies array for handleMailClick now includes handleMouseEnter.


468-480: Properly updated Thread component props in Virtuoso.

The Thread component in the Virtuoso renderer now receives the necessary props for keyboard navigation and quick actions.

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 (5)
apps/mail/locales/en.json (1)

123-123: Inconsistent capitalization between similar entries.

I noticed the added markAsRead entries have different capitalizations:

  • "markAsRead": "Mark as read" in threadDisplay (line 123)
  • "markAsRead": "Mark as Read" in mail (line 212)

This follows the existing pattern where threadDisplay uses lowercase for the second word while mail uses title case, but it may cause confusion for future developers. Consider documenting this convention or standardizing capitalization across sections.

Also applies to: 212-212

apps/mail/components/mail/mail-quick-actions.tsx (4)

3-14: Add tests for the new component.

This new component handles critical mail actions like archiving, marking as read/unread, and deletion. Consider adding unit tests to ensure these features work correctly across different states and edge cases.


60-87: Consider refactoring shared logic in action handlers.

The action handlers (handleArchive, handleToggleRead, handleDelete) share similar patterns for error handling, state management, and thread ID extraction. Consider extracting common functionality into a higher-order function to reduce code duplication.

+ const performThreadAction = useCallback(
+   async (
+     e: React.MouseEvent | undefined,
+     actionFn: () => Promise<any>,
+     errorMessage: string
+   ) => {
+     e?.stopPropagation();
+     if (isProcessing || isLoading) return;
+     
+     setIsProcessing(true);
+     try {
+       await actionFn();
+     } catch (error) {
+       console.error(errorMessage, error);
+       toast.error(t('common.mail.errorMoving'));
+     } finally {
+       setIsProcessing(false);
+     }
+   },
+   [isProcessing, isLoading, t]
+ );

const handleArchive = useCallback(async (e?: React.MouseEvent) => {
-  e?.stopPropagation();
-  if (isProcessing || isLoading) return;
-  
-  setIsProcessing(true);
-  try {
+  return performThreadAction(e, async () => {
    const threadId = message.threadId ?? message.id;
    const destination = isArchiveFolder ? FOLDERS.INBOX : FOLDERS.ARCHIVE;

    await moveThreadsTo({
      threadIds: [`thread:${threadId}`],
      currentFolder: currentFolder,
      destination: destination as ThreadDestination
    }).then(async () => {
      await Promise.all([mutate(), mutateStats()]);
      
      const actionType = isArchiveFolder ? 'unarchive' : 'archive';
      toast.success(t(`common.mail.${actionType}`));
      
      closeThreadIfOpen();
    });
-  } catch (error) {
-    console.error('Error archiving thread', error);
-    toast.error(t('common.mail.errorMoving'));
-  } finally {
-    setIsProcessing(false);
-  }
+  }, 'Error archiving thread');
}, [message, currentFolder, isArchiveFolder, mutate, mutateStats, t, performThreadAction, closeThreadIfOpen]);

170-172: Consider showing button outlines for better discoverability.

Currently, quick actions are completely hidden until hover. Consider showing subtle button outlines even when not hovered to improve discoverability, especially for users who might not realize these actions exist.

if (!isHovered && !isInQuickActionMode) {
-  return null;
+  return (
+    <div className={cn(
+      'absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 bg-transparent rounded-md p-1 z-10 opacity-30',
+      className
+    )}>
+      {quickActions.map((quickAction, index) => (
+        <Button 
+          key={index}
+          variant="ghost"
+          size="icon" 
+          className="h-7 w-7 mail-quick-action-button opacity-0"
+          disabled={true}
+          aria-hidden={true}
+        >
+          <quickAction.icon className="h-4 w-4" />
+        </Button>
+      ))}
+    </div>
+  );
}

174-201: Consider keyboard focus management enhancements.

The component provides good visual feedback for keyboard navigation with the isInQuickActionMode and selectedQuickActionIndex props, but could be enhanced with explicit keyboard event handlers and focus management.

  return (
    <div 
      className={cn(
        'absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 bg-background/80 backdrop-blur-sm rounded-md p-1 z-10 overflow-visible',
        className,
        isInQuickActionMode && 'bg-background/95 ring-2 ring-primary/20'
      )}
+     role="toolbar"
+     aria-label="Quick actions"
    >
      {quickActions.map((quickAction, index) => (
        <Button 
          key={index}
          variant={isInQuickActionMode && selectedQuickActionIndex === index ? "secondary" : "ghost"}
          size="icon" 
          className={cn(
            "h-7 w-7 mail-quick-action-button", 
            isInQuickActionMode && selectedQuickActionIndex === index && 
            "border border-primary/60 shadow-sm"
          )}
          onClick={(e) => quickAction.action(e)}
          disabled={isLoading || isProcessing}
          aria-label={quickAction.label}
+         tabIndex={isInQuickActionMode && selectedQuickActionIndex === index ? 0 : -1}
        >
          <quickAction.icon className="h-4 w-4" />
        </Button>
      ))}
    </div>
  );
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c3887fd and e3d1f79.

📒 Files selected for processing (2)
  • apps/mail/components/mail/mail-quick-actions.tsx (1 hunks)
  • apps/mail/locales/en.json (2 hunks)
🔇 Additional comments (3)
apps/mail/components/mail/mail-quick-actions.tsx (3)

150-161: Incomplete reply functionality implementation.

The handleQuickReply function only shows a toast notification without implementing actual reply functionality.

Consider implementing the actual reply functionality or adding a TODO comment if this is intended to be a placeholder for future implementation:

const handleQuickReply = useCallback(async (e?: React.MouseEvent) => {
  e?.stopPropagation();
  if (isProcessing) return;
  
  setIsProcessing(true);
  try {
+   // TODO: Implement quick reply
    toast.info(t('common.mail.reply'));
  } finally {
    setIsProcessing(false);
  }
}, [t, isProcessing]);

32-59: LGTM: Good implementation of closeThreadIfOpen.

The closeThreadIfOpen function effectively manages the URL parameters and navigation state when a thread is closed, ensuring a smooth user experience when performing quick actions.


163-168: Well-organized quick actions array.

The quick actions array is well-structured, making it easy to maintain and extend with additional actions in the future.

@MrgSub MrgSub merged commit 62cc85c into staging Mar 26, 2025
5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Apr 1, 2025
@coderabbitai coderabbitai bot mentioned this pull request Apr 25, 2025
@coderabbitai coderabbitai bot mentioned this pull request Jul 9, 2025
10 tasks
@coderabbitai coderabbitai bot mentioned this pull request Aug 4, 2025
34 tasks
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