UNPKG

@redocly/theme

Version:

Shared UI components lib

142 lines (124 loc) 3.91 kB
import type { TooltipPlacement, TooltipProps } from '@redocly/theme/core/types'; const PLACEMENT_MARGIN = 10; const COUNTER_CLOCKWISE: TooltipPlacement[] = ['top', 'left', 'bottom', 'right']; export function getDefaultFallbackPlacements(placement: TooltipPlacement): TooltipPlacement[] { const index = COUNTER_CLOCKWISE.indexOf(placement); const result: TooltipPlacement[] = []; for (let i = 1; i < COUNTER_CLOCKWISE.length; i++) { result.push(COUNTER_CLOCKWISE[(index + i) % COUNTER_CLOCKWISE.length]); } return result; } export function calcAnchorPoint( triggerRect: DOMRect, placement: TooltipPlacement, arrowPosition: TooltipProps['arrowPosition'], ): { top: number; left: number } { const horizontalLeft = (): number => arrowPosition === 'left' ? triggerRect.left - 24 : arrowPosition === 'right' ? triggerRect.right + 24 : triggerRect.left + triggerRect.width / 2; const verticalTop = (): number => triggerRect.top + triggerRect.height / 2; switch (placement) { case 'top': return { top: triggerRect.top, left: horizontalLeft() }; case 'bottom': return { top: triggerRect.bottom, left: horizontalLeft() }; case 'left': return { top: verticalTop(), left: triggerRect.left }; case 'right': return { top: verticalTop(), left: triggerRect.right }; } } type FitsInViewportParams = { anchor: { top: number; left: number }; tooltipWidth: number; tooltipHeight: number; placement: TooltipPlacement; arrowPosition: TooltipProps['arrowPosition']; }; export function fitsInViewport({ anchor, tooltipWidth, tooltipHeight, placement, arrowPosition, }: FitsInViewportParams): boolean { const horizontalLeft = (): number => arrowPosition === 'left' ? anchor.left : arrowPosition === 'right' ? anchor.left - tooltipWidth : anchor.left - tooltipWidth / 2; const verticalTop = (): number => anchor.top - tooltipHeight / 2; let top: number; let left: number; switch (placement) { case 'top': top = anchor.top - tooltipHeight - PLACEMENT_MARGIN; left = horizontalLeft(); break; case 'bottom': top = anchor.top + PLACEMENT_MARGIN; left = horizontalLeft(); break; case 'left': top = verticalTop(); left = anchor.left - tooltipWidth - PLACEMENT_MARGIN; break; case 'right': top = verticalTop(); left = anchor.left + PLACEMENT_MARGIN; break; } return ( top >= 0 && left >= 0 && left + tooltipWidth <= window.innerWidth && top + tooltipHeight <= window.innerHeight ); } type ResolvePlacementParams = { triggerRect: DOMRect; tooltipWidth: number; tooltipHeight: number; placement: TooltipPlacement; arrowPosition: TooltipProps['arrowPosition']; fallbackPlacements: TooltipPlacement[] | undefined; }; /** * Given the trigger rect, tooltip dimensions, primary placement/arrow, and * fallback list, returns the first placement that keeps the tooltip fully * inside the viewport. Falls back to the primary when nothing fits. */ export function resolvePlacement({ triggerRect, tooltipWidth, tooltipHeight, placement, arrowPosition, fallbackPlacements, }: ResolvePlacementParams): TooltipPlacement { if (!fallbackPlacements?.length || tooltipWidth === 0 || tooltipHeight === 0) { return placement; } const candidates: TooltipPlacement[] = [placement, ...fallbackPlacements]; for (const candidate of candidates) { const candidateArrow = candidate === placement ? arrowPosition : 'center'; const pos = calcAnchorPoint(triggerRect, candidate, candidateArrow); if ( fitsInViewport({ anchor: pos, tooltipWidth, tooltipHeight, placement: candidate, arrowPosition: candidateArrow, }) ) { return candidate; } } return placement; }