const DEFAULT_TOOLTIP_OFFSET = 10 ; export const ALL_PLACEMENTS = [ 'bottom' , 'bottom-left' , 'bottom-right' , 'top' , 'top-left' , 'top-right' , 'left' , 'left-bottom' , 'left-top' , 'right' , 'right-bottom' , 'right-top' , ] ; export const CORNERS_ONLY_PLACEMENTS = [ 'bottom-left' , 'bottom-right' , 'top-left' , 'top-right' , 'left-bottom' , 'left-top' , 'right-bottom' , 'right-top' , ] ; const inverted = { left : 'right' , right : 'left' , bottom : 'top' , top : 'bottom' , } ; export type Position = { top : number ; left : number ; '--arrow-offset' : 0 | string ; } ; type ElementRect = { top : number ; left : number ; width : number ; height : number ; } ; /**
 * Tries to maintain placement as close to the original as possible.
 * Places in this order of preference:
 * - same orientation as original (e.g. bottom-left, bottom, bottom-right)
 * - same orientation as original alignment (e.g. left-bottom => bottom-*)
 * - inverted orientation than original (e.g. top => bottom)
 * - whatever is left
 *
 * always prefers alignment to be the same as original alignment or orientation.
 */ export const orderPlacements = ( originalPlacement , placements = ALL_PLACEMENTS ) => { const orientation = originalPlacement . split ( '-' ) [ 0 ] ; const alignment = originalPlacement . split ( '-' ) [ 1 ] ; const compareAlignment = ( a , b ) => { if ( ( b . endsWith ( alignment ) && ! b . startsWith ( alignment ) && ! a . endsWith ( alignment ) ) || ( b . endsWith ( orientation ) && ! b . startsWith ( orientation ) && ! a . endsWith ( orientation ) ) ) { return 1 ; } if ( ( a . endsWith ( alignment ) && ! a . startsWith ( alignment ) && ! b . endsWith ( alignment ) ) || ( a . endsWith ( orientation ) && ! a . startsWith ( orientation ) && ! b . endsWith ( orientation ) ) ) { return - 1 ; } return 0 ; } ; const sameOrientation = placements . filter ( ( placement ) => placement . startsWith ( orientation ) ) . sort ( compareAlignment ) ; const sameOrientationAsAlignment = placements . filter ( ( placement ) => placement . startsWith ( alignment ) ) . sort ( compareAlignment ) ; const invertedOrientation = placements . filter ( ( placement ) => placement . startsWith ( inverted [ orientation ] ) ) . sort ( compareAlignment ) ; const preferred = [ ... sameOrientation , ... sameOrientationAsAlignment , ... invertedOrientation ] ; const remaining = placements . filter ( ( placement ) => ! preferred . includes ( placement ) ) . sort ( compareAlignment ) ; return [ ... preferred , ... remaining ] ; } ; const calculatePosition = ( target , tooltip , placement , offset = DEFAULT_TOOLTIP_OFFSET , originalPosition ? ) : Position => { let alignCenter = { top : target . top + target . height / 2 - tooltip . height / 2 , left : target . left + target . width / 2 - tooltip . width / 2 , } ; let alignTop = target . top ; let alignBottom = target . top + target . height - tooltip . height ; let alignLeft = target . left ; let alignRight = target . left - tooltip . width + target . width ; let placeAbove = target . top - tooltip . height - offset ; let placeBelow = target . top + target . height + offset ; let placeLeft = target . left - tooltip . width - offset ; let placeRight = target . left + target . width + offset ; const minHeight = target . height < tooltip . height ? target . height : tooltip . height ; const minWidth = target . width < tooltip . width ? target . width : tooltip . width ; const horizontalOffset = `${ minWidth / 2 }px` ; const verticalOffset = `${ minHeight / 2 }px` ; if ( originalPosition ) { alignCenter = { top : originalPosition . top + target . height / 2 - tooltip . height / 2 , left : originalPosition . left + target . width / 2 - tooltip . width / 2 , } ; alignTop = originalPosition . top ; alignBottom = originalPosition . top + tooltip . height ; alignLeft = originalPosition . left ; alignRight = originalPosition . left - tooltip . width ; placeAbove = originalPosition . top - tooltip . height - offset ; placeBelow = originalPosition . top + offset ; placeLeft = originalPosition . left - tooltip . width - offset ; placeRight = originalPosition . left + offset ; } const placementList = { top : { left : alignCenter . left , top : placeAbove , '--arrow-offset' : 0 } , bottom : { left : alignCenter . left , top : placeBelow , '--arrow-offset' : 0 } , left : { left : placeLeft , top : alignCenter . top , '--arrow-offset' : 0 } , right : { left : placeRight , top : alignCenter . top , '--arrow-offset' : 0 } , 'bottom-left' : { left : alignLeft , top : placeBelow , '--arrow-offset' : horizontalOffset } , 'top-left' : { left : alignLeft , top : placeAbove , '--arrow-offset' : horizontalOffset } , 'bottom-right' : { left : alignRight , top : placeBelow , '--arrow-offset' : horizontalOffset } , 'top-right' : { left : alignRight , top : placeAbove , '--arrow-offset' : horizontalOffset } , 'right-bottom' : { left : placeRight , top : alignBottom , '--arrow-offset' : verticalOffset } , 'right-top' : { left : placeRight , top : alignTop , '--arrow-offset' : verticalOffset } , 'left-bottom' : { left : placeLeft , top : alignBottom , '--arrow-offset' : verticalOffset } , 'left-top' : { left : placeLeft , top : alignTop , '--arrow-offset' : verticalOffset } , } ; return placementList [ placement ] ; } ; const isOutOfScreen = ( tooltip , position ) => { return ( position . top + tooltip . height > window . innerHeight || position . left + tooltip . width > window . innerWidth || position . top < 0 || position . left < 0 ) ; } ; const howMuchIsOutOfScreen = ( tooltip , position ) => { const offsets = [ position . top + tooltip . height - window . innerHeight , position . left + tooltip . width - window . innerWidth , 0 - position . top , 0 - position . left , ] ; return offsets . filter ( ( number ) => number > 0 ) . reduce ( ( acc , number ) => acc + number , 0 ) ; } ; const optimisePositionAndPlacement = ( target , tooltip , offset , availablePlacements = ALL_PLACEMENTS , originalPosition ? ) : { position : Position ; placement : string ; } | null => { if ( ! availablePlacements . length ) { return null ; } const [ placement , ... rest ] = availablePlacements ; const position = calculatePosition ( target , tooltip , placement , offset , originalPosition ) ; if ( ! isOutOfScreen ( tooltip , position ) ) { return { position , placement } ; } const otherPosition = optimisePositionAndPlacement ( target , tooltip , offset , rest , originalPosition ) ; return otherPosition && howMuchIsOutOfScreen ( tooltip , position ) > howMuchIsOutOfScreen ( tooltip , otherPosition . position ) ? otherPosition : { position , placement } ; } ; export const adjustPosition = ( target , tooltip , placement , offset , originalPosition ? , availablePlacements = ALL_PLACEMENTS ) => { const placementsByPriority = orderPlacements ( placement , availablePlacements ) ; const optimalLocation = optimisePositionAndPlacement ( target , tooltip , offset , placementsByPriority , originalPosition ) ; if ( ! optimalLocation ) { const position = calculatePosition ( target , tooltip , placement , offset , originalPosition ) ; return { position , placement , } ; } return optimalLocation ; } ; export const computedSize = ( stylePixels , boundsSize ) => { const computedStyleSize = Number ( stylePixels . replace ( 'px' , '' ) ) ; return Number . isNaN ( computedStyleSize ) ? boundsSize : computedStyleSize ; } ; export const shouldShowSideRadius = ( position , placement , radiusSize = 8 , arrowSize = 10 ) => { const arrowOffset = position [ '--arrow-offset' ] ; const offset = arrowOffset === 0 ? arrowOffset : Number ( arrowOffset . replace ( 'px' , '' ) ) ; return ! CORNERS_ONLY_PLACEMENTS . includes ( placement ) || offset > radiusSize + arrowSize ; } ;