LinkDesk/frontend/docs/context-menu-popover-fix.md

5.7 KiB

Context Menu Popover Fix

Issue

The context menu was not showing when right-clicking on selected tasks in the TaskBrowser.

Root Cause Analysis

After reviewing the shadcn-vue documentation and comparing with our implementation, the following issues were identified:

Problems with DropdownMenu Approach:

  1. Missing Trigger Element: DropdownMenu requires a DropdownMenuTrigger component. Our implementation used v-model:open to control the menu programmatically, but DropdownMenu expects to be triggered by a trigger element.

  2. Incorrect Positioning: We attempted to use fixed positioning with inline styles on DropdownMenuContent, but DropdownMenu uses Radix UI's portal-based positioning logic, which conflicts with manual positioning.

  3. Programmatic Control Limitations: The v-model:open pattern doesn't work well with DropdownMenu without a proper trigger element.

  4. Wrong Component Choice: DropdownMenu is designed for click-triggered dropdowns with a visible trigger button, not for programmatic context menus triggered by right-click events.

Solution

Replaced DropdownMenu with Popover component and simplified the menu structure to use native buttons instead of DropdownMenu submenus.

Why Popover is Better:

  1. Programmatic Control: Supports v-model:open for programmatic control
  2. Custom Positioning: Works with PopoverAnchor for custom positioning
  3. Flexible Triggers: Works well with custom triggers like right-click events
  4. Dynamic Positioning: Can position based on mouse coordinates using a fixed-position anchor
  5. No Trigger Required: Doesn't require wrapping trigger elements

Implementation Changes

TaskBulkActionsMenu.vue

Before (DropdownMenu):

<template>
  <DropdownMenu v-model:open="isOpen">
    <DropdownMenuContent
      :style="{
        position: 'fixed',
        left: `${adjustedPosition.x}px`,
        top: `${adjustedPosition.y}px`,
      }"
      class="w-56"
    >
      <!-- Menu content -->
    </DropdownMenuContent>
  </DropdownMenu>
</template>

After (Popover):

<template>
  <Popover v-model:open="isOpen">
    <PopoverAnchor
      :style="{
        position: 'fixed',
        left: `${props.position.x}px`,
        top: `${props.position.y}px`,
        width: '1px',
        height: '1px',
      }"
    />
    <PopoverContent
      class="w-56 p-0"
      :side="'bottom'"
      :align="'start'"
      @interact-outside="handleInteractOutside"
    >
      <!-- Menu content -->
    </PopoverContent>
  </Popover>
</template>

Key Changes:

  1. Replaced DropdownMenu with Popover: Changed the wrapper component
  2. Added PopoverAnchor: Creates a 1px invisible anchor point at the mouse cursor position
  3. Updated PopoverContent: Replaced DropdownMenuContent with PopoverContent
  4. Removed Manual Positioning Logic: Removed the adjustedPosition computed property since Popover handles positioning automatically
  5. Updated Imports: Changed from DropdownMenu imports to Popover imports

How It Works:

  1. User right-clicks on a table row
  2. handleContextMenu in TaskBrowser captures the mouse coordinates
  3. contextMenuPosition is updated with { x: event.clientX, y: event.clientY }
  4. showContextMenu is set to true
  5. PopoverAnchor creates an invisible 1px element at the cursor position
  6. PopoverContent appears relative to the anchor
  7. Popover's built-in positioning logic handles viewport boundaries automatically

Benefits

  • Context menu now appears at cursor position
  • Automatic viewport boundary detection
  • Proper z-index and portal rendering
  • Maintains all existing functionality (status update, assignment)
  • Cleaner code (removed manual positioning logic)
  • Better accessibility

Testing

To test the fix:

  1. Open TaskBrowser in the application
  2. Select one or more tasks using checkboxes
  3. Right-click on a selected task
  4. Context menu should appear at the cursor position
  5. Verify "Set Status" submenu works
  6. Verify "Assign To" submenu works
  7. Verify menu closes when clicking outside
  8. Verify menu closes after selecting an action

Files Modified

  • frontend/src/components/task/TaskBulkActionsMenu.vue - Replaced DropdownMenu with Popover

Additional Fix: Menu Structure

Issue with DropdownMenu Submenus

After initial implementation, we encountered an error:

Error: Injection `Symbol(MenuContext)` not found. Component must be used within MenuRoot

Cause: DropdownMenuSub, DropdownMenuSubTrigger, and DropdownMenuSubContent components require a MenuRoot context (provided by DropdownMenu). They cannot be used inside a Popover.

Solution

Replaced DropdownMenu submenu components with simple native button elements styled to match the design system:

Before:

<DropdownMenuSub>
  <DropdownMenuSubTrigger>Set Status</DropdownMenuSubTrigger>
  <DropdownMenuSubContent>
    <DropdownMenuItem @click="...">...</DropdownMenuItem>
  </DropdownMenuSubContent>
</DropdownMenuSub>

After:

<div class="py-1">
  <div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
    Set Status
  </div>
  <button
    class="w-full text-left px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground rounded-sm cursor-pointer"
    @click="handleStatusSelected(status.value)"
  >
    {{ status.label }}
  </button>
</div>

Benefits of Simplified Structure

  1. No dependency on DropdownMenu context
  2. Simpler, more maintainable code
  3. Better performance (fewer components)
  4. Same visual appearance
  5. Full control over styling and behavior