/** * Menu position calculation utility * Calculates optimal position for dropdown menus to avoid viewport clipping */ export interface MenuPosition { x: number; y: number; placement: 'bottom' | 'top'; } /** * Calculate the optimal position for a menu dropdown * @param triggerElement - The button/element that triggers the menu * @param menuWidth - Estimated or actual menu width (default: 160px) * @param menuHeight - Estimated or actual menu height (default: 120px) * @returns Position object with x, y coordinates and placement direction */ export function calculateMenuPosition( triggerElement: HTMLElement, menuWidth: number = 160, menuHeight: number = 120 ): MenuPosition { const rect = triggerElement.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; // Determine vertical placement (below or above trigger) const spaceBelow = viewportHeight - rect.bottom; const spaceAbove = rect.top; const fitsBelow = spaceBelow >= menuHeight + 8; // 8px margin const fitsAbove = spaceAbove >= menuHeight + 8; let y: number; let placement: 'bottom' | 'top'; if (fitsBelow) { // Prefer below if there's space y = rect.bottom + 4; // 4px gap placement = 'bottom'; } else if (fitsAbove) { // Show above if no space below y = rect.top - menuHeight - 4; // 4px gap placement = 'top'; } else { // Not enough space either way - prefer below and let it extend y = rect.bottom + 4; placement = 'bottom'; } // Horizontal positioning - align right edge of menu with right edge of button let x = rect.right - menuWidth; // Ensure menu doesn't overflow left edge of viewport if (x < 8) { x = 8; // 8px margin from left edge } // Ensure menu doesn't overflow right edge of viewport if (x + menuWidth > viewportWidth - 8) { x = viewportWidth - menuWidth - 8; // 8px margin from right edge } return { x, y, placement }; }