import React from 'react' ; import useRect , { HasBoundingClientRect } from './useRect' ; type AlignMode = 'center' | 'start' | 'end' | 'top' | 'right' | 'bottom' | 'left' ; type Side = 'left' | 'right' | 'top' | 'bottom' ; type SideOption = Side | `${ Side } ${ AlignMode }` ; type SideAlign = [ Side , AlignMode ] ; type StartKey = 'left' | 'top' ; type LengthKey = 'width' | 'height' ; type Dims = { left : number ; top : number ; right : number ; bottom : number ; width : number ; height : number ; } ; const sideSchemas = { left : { side : 'left' , startKey : 'left' , lengthKey : 'width' , crossStartKey : 'top' , crossLengthKey : 'height' , fromEnd : false , } , right : { side : 'right' , startKey : 'left' , lengthKey : 'width' , crossStartKey : 'top' , crossLengthKey : 'height' , fromEnd : true , } , top : { side : 'top' , startKey : 'top' , lengthKey : 'height' , crossStartKey : 'left' , crossLengthKey : 'width' , fromEnd : false , } , bottom : { side : 'bottom' , startKey : 'top' , lengthKey : 'height' , crossStartKey : 'left' , crossLengthKey : 'width' , fromEnd : true , } , } as const ; export function useAnchor ( options ) { const portalDims = useRect ( options . portalEl , options . show ) ; const anchorDims = useRect ( options . anchorEl , options . show ) ; const tooltipDims = useRect ( options . tooltipEl , options . show ) ; const sides = React . useMemo ( ( ) => { const preSides = Array . isArray ( options . side ) ? options . side : [ options . side ] ; return preSides . map ( alignStr => { const [ side , align = 'center' ] = alignStr . split ( ' ' ) as [ Side , AlignMode ] ; const incompatibleSide = ! [ 'top' , 'right' , 'bottom' , 'left' ] . find ( d => side === d ) ; if ( incompatibleSide ) { throw new Error ( `react-sticker: "${ side }" is not a valid side! Must be one of ['top', 'right', 'bottom', 'left'].` ) ; } const incompatibleAlign = ! [ 'center' , 'start' , 'end' , 'top' , 'right' , 'bottom' , 'left' , ] . find ( d => align === d ) ; if ( incompatibleAlign ) { throw new Error ( `react-sticker: "${ align }" is not a valid side-alignment! Must be one of ['center', 'start', 'end', 'top', 'right', 'bottom', 'left'].` ) ; } return [ side , align ] as [ Side , AlignMode ] ; } ) ; } , [ JSON . stringify ( options . side ) ] ) ; const ready = portalDims && tooltipDims && anchorDims ; const fit = React . useMemo ( ( ) => ready && options . show ? fitOnBestSide ( { portalDims , tooltipDims , anchorDims , sides , useLargest : options . useLargest , } ) : null , [ anchorDims , options . show , options . useLargest , portalDims , ready , sides , tooltipDims , ] ) ; return { fit , style : { position : 'absolute' as const , visibility : ready ? ( 'visible' as const ) : ( 'hidden' as const ) , ... fit ?. style , } , } ; } function fitOnBestSide ( { portalDims , tooltipDims , anchorDims , sides , useLargest , } ) { const fits = sides . map ( ( [ side , align ] ) => measureFit ( { ... sideSchemas [ side ] , align , portalDims , tooltipDims , anchorDims , } ) ) ; if ( useLargest ) { fits . sort ( ( a , b ) => b . fitRatio - a . fitRatio ) ; return fits [ 0 ] ; } return fits . find ( fit => fit . fitRatio >= 1 ) || fits [ 0 ] ; } function measureFit ( { side , align , startKey , lengthKey , crossStartKey , crossLengthKey , fromEnd , portalDims , tooltipDims , anchorDims , } ) { const parentStart = portalDims [ startKey ] ; const parentLength = portalDims [ lengthKey ] ; const crossParentStart = portalDims [ crossStartKey ] ; const crossParentLength = portalDims [ crossLengthKey ] ; const anchorStart = anchorDims [ startKey ] - portalDims [ startKey ] ; const anchorLength = anchorDims [ lengthKey ] ; const crossAnchorStart = anchorDims [ crossStartKey ] ; const crossAnchorLength = anchorDims [ crossLengthKey ] ; const crossAnchorWidth = anchorDims [ crossLengthKey ] ; const targetLength = tooltipDims [ lengthKey ] ; const crossTargetLength = tooltipDims [ crossLengthKey ] ; let targetStart ; let fitRatio ; if ( ! fromEnd ) { targetStart = anchorStart - targetLength ; fitRatio = Math . min ( anchorStart / targetLength ) ; } else { targetStart = anchorStart + anchorLength ; fitRatio = ( parentLength - ( anchorStart + anchorLength ) ) / targetLength ; } targetStart = Math . max ( parentStart , Math . min ( targetStart , parentLength ) ) ; let crossTargetStart ; if ( startKey === 'left' ) { if ( align === 'top' ) { align = 'start' ; } else if ( align === 'bottom' ) { align = 'end' ; } } else { if ( align === 'left' ) { align = 'start' ; } else if ( align === 'right' ) { align = 'end' ; } } if ( ! [ 'start' , 'center' , 'end' ] . includes ( align ) ) { align = 'center' ; } if ( align === 'start' ) { crossTargetStart = crossAnchorStart ; } else if ( align === 'end' ) { crossTargetStart = crossAnchorStart + crossAnchorWidth - crossTargetLength ; } else { crossTargetStart = crossAnchorStart + crossAnchorLength / 2 - crossTargetLength / 2 ; } crossTargetStart = Math . max ( crossParentStart , Math . min ( crossTargetStart , crossParentLength - crossTargetLength ) ) ; return { side , align , startKey , lengthKey , crossStartKey , crossLengthKey , fromEnd , portalDims , tooltipDims , anchorDims , fitRatio , style : { [ startKey ] : targetStart , [ crossStartKey ] : crossTargetStart , } as Record < StartKey , number > , } ; }