LinkDesk/frontend/docs/shot-delete-event-bubbling-...

4.0 KiB

Shot Delete Event Bubbling Fix

Issue Description

When clicking the "Delete Shot" button in the ShotsDataTable, the shot detail panel was being triggered along with the delete action. This was happening because the delete button click event was bubbling up to the table row click handler, which opens the shot detail panel.

Root Cause

The issue was caused by event bubbling in the table row structure:

  1. The table row has a @click="handleRowClick" event handler that opens the shot detail panel
  2. The delete button (inside a dropdown menu) is within the table row
  3. Even though the dropdown menu items had e.stopPropagation(), the event was still bubbling up to the row click handler

Solution

1. Enhanced Event Handling in ShotsDataTable

Modified the handleRowClick function in ShotsDataTable.vue to check if the click target is within an interactive element:

const handleRowClick = (shot: Shot, event: MouseEvent) => {
  // Check if the click target is within a dropdown menu, button, or other interactive element
  const target = event.target as HTMLElement
  if (target) {
    // Check if the click is on a button, dropdown, or any interactive element
    const isInteractiveElement = target.closest('button, [role="menuitem"], [data-radix-collection-item]')
    if (isInteractiveElement) {
      // Don't emit row-click if clicking on interactive elements
      return
    }
  }
  
  emit('row-click', shot, event)
}

This approach:

  • Checks if the click target is within a button, dropdown menu item, or other interactive element
  • Uses closest() to traverse up the DOM tree to find interactive elements
  • Prevents the row-click event from being emitted when clicking on interactive elements

2. Improved Event Prevention in Columns

Enhanced the event handling in columns.ts for all dropdown menu items:

// Before
onClick: (e: Event) => { e.stopPropagation(); meta.onDelete(shot) }

// After
onClick: (e: Event) => { 
  e.stopPropagation()
  e.preventDefault()
  meta.onDelete(shot) 
}

Added e.preventDefault() in addition to e.stopPropagation() for more robust event handling.

3. Enhanced Dropdown Trigger

Improved the dropdown trigger button event handling:

h(
  DropdownMenuTrigger,
  { 
    asChild: true, 
    onClick: (e: Event) => {
      e.stopPropagation()
      e.preventDefault()
    }
  },
  {
    default: () =>
      h(
        Button,
        { 
          variant: 'ghost', 
          size: 'sm', 
          class: 'h-8 w-8 p-0',
          onClick: (e: Event) => {
            e.stopPropagation()
            e.preventDefault()
          }
        },
        // ...
      ),
  }
)

Testing

Created test-shot-delete-event-fix.html to verify the fix works correctly:

  1. Clicking the "Delete Shot" button should NOT trigger the row click event
  2. Clicking anywhere else on the row SHOULD trigger the row click event
  3. The event log shows which events are triggered and which are prevented

Files Modified

  1. frontend/src/components/shot/ShotsDataTable.vue

    • Enhanced handleRowClick function to detect interactive elements
  2. frontend/src/components/shot/columns.ts

    • Added e.preventDefault() to all dropdown menu item click handlers
    • Enhanced dropdown trigger event handling

Benefits

  • Prevents unwanted shot detail panel opening when deleting shots
  • Maintains proper row click functionality for non-interactive areas
  • Provides a robust solution that works with various UI components
  • Uses DOM traversal to detect interactive elements, making it future-proof

Prevention

To prevent similar issues in the future:

  1. Always use both e.stopPropagation() and e.preventDefault() for interactive elements within clickable containers
  2. Consider using DOM traversal (closest()) to detect interactive elements in container click handlers
  3. Test interactive elements within clickable containers to ensure proper event handling
  4. Use specific selectors for interactive elements (buttons, menu items, etc.)