type ClampDropdownToViewportOptions = { anchorLeft: number; anchorWidth: number; viewportWidth: number; viewportPadding?: number; }; export type ClampedDropdownHorizontalPosition = { left: number; width: number; }; export function clampDropdownToViewport({ anchorLeft, anchorWidth, viewportWidth, viewportPadding = 8, }: ClampDropdownToViewportOptions): ClampedDropdownHorizontalPosition { const availableWidth = Math.max(0, viewportWidth - viewportPadding * 2); const width = Math.min(Math.max(0, anchorWidth), availableWidth); const left = Math.min( Math.max(viewportPadding, anchorLeft), Math.max(viewportPadding, viewportWidth - width - viewportPadding), ); return { left, width }; } type FloatingMenuPositionOptions = { anchorTop: number; menuHeight: number; menuLeft: number; menuTop: number; menuWidth: number; viewportHeight: number; viewportPadding?: number; viewportWidth: number; verticalGap?: number; }; export type FloatingMenuPosition = { maxHeight: number | null; maxWidth: number; translateX: number; translateY: number; }; export function constrainFloatingMenuToViewport({ anchorTop, menuHeight, menuLeft, menuTop, menuWidth, viewportHeight, viewportPadding = 12, viewportWidth, verticalGap = 8, }: FloatingMenuPositionOptions): FloatingMenuPosition { const maxWidth = Math.max(0, viewportWidth - viewportPadding * 2); const renderedWidth = Math.min(menuWidth, maxWidth); const { left } = clampDropdownToViewport({ anchorLeft: menuLeft, anchorWidth: renderedWidth, viewportPadding, viewportWidth, }); const translateX = left - menuLeft; const viewportBottom = viewportHeight - viewportPadding; if (menuTop + menuHeight <= viewportBottom) { return { maxHeight: null, maxWidth, translateX, translateY: 0 }; } const spaceBelow = Math.max(0, viewportBottom - menuTop); const spaceAbove = Math.max(0, anchorTop - viewportPadding - verticalGap); if (spaceAbove > spaceBelow) { const maxHeight = Math.min(menuHeight, spaceAbove); return { maxHeight, maxWidth, translateX, translateY: anchorTop - verticalGap - maxHeight - menuTop, }; } return { maxHeight: Math.min(menuHeight, spaceBelow), maxWidth, translateX, translateY: 0, }; }