Building a Right-Click Context Menu That Knows What You Clicked
TLDR
Attaching individualcontextmenu listeners to every tab or accordion button increases memory usage and breaks when adding dynamic elements. Utilizing a single delegated contextmenu listener on the parent container alongside e.target.closest() dynamically dispatches action sets based on element types while clamping menu coordinates within viewport bounds.
| Event Listener Strategy | Memory Footprint | Dynamic Element Handling | Viewport Boundary Handling |
|---|---|---|---|
| Per-Element Binding | High (grows with tab count) | Requires re-binding on element add | Manual calculation per element |
| Delegated Container Binding | Minimal (Single parent listener) | 100% Automatic via closest() | Centralized clientX/Y Clamping |
Why individual context listeners cause event handler bloat
When UI bars support multiple interactive element types, such as individual sheet tabs (.sp-option) and expandable section headers (button.accordion), binding separate context menu listeners to each node creates unnecessary event overhead.
Elements added dynamically also require explicit listener attachment, increasing code complexity and edge-case bug risks.
Centralized event delegation and viewport clamping
We attached a single contextmenu listener to the tab bar container, using e.target.closest() to identify target element types and position a fixed floating menu:
// Delegated right-click context menu handler
export function bindTabContextMenu(containerEl, menuEl) {
containerEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
const tab = e.target.closest('.sp-option'); const accordion = e.target.closest('button.accordion'); const target = tab || accordion; if (!target) return;
const targetType = tab ? 'sheet' : 'section'; renderContextMenuItems(menuEl, target, targetType); positionFixedMenu(menuEl, e.clientX, e.clientY); }); }
function positionFixedMenu(menuEl, clientX, clientY) { menuEl.style.position = 'fixed'; menuEl.style.left = ${clientX}px; menuEl.style.top = ${clientY}px; menuEl.style.display = 'block';
// Clamp within viewport bounds to prevent off-screen overflow const rect = menuEl.getBoundingClientRect(); if (rect.right > window.innerWidth) menuEl.style.left = ${clientX - rect.width}px; if (rect.bottom > window.innerHeight) menuEl.style.top = ${clientY - rect.height}px; }
Dismissal listeners attached to click and scroll events ensure the floating menu closes cleanly when users interact elsewhere on the page.
Rule of thumb: Delegate context menu events to parent containers usinge.target.closest()and clamp positions usingclientX/clientY.