import React , { useEffect , useRef , useCallback } from 'react' ; import { getScrollParent , getScrollParents , isElementInViewport } from '../getScrollParent' ; import { useDebounce } from '../useDebounce' ; interface Coords { x : number ; y : number ; } export interface UseDropCallbacks { onHide ? : ( ) => void ; } export interface UseDropConfig { align : 'vertical' | 'horizontal' ; float : 'start' | 'end' ; margins : Coords ; fixed ? : boolean ; coords ? : Coords ; container ? : any ; parentRef ? : React . RefObject < any > ; followParentWhenScroll ? : boolean ; callbacks ? : UseDropCallbacks ; fitListToParent ? : boolean ; } const setInitPositionByParent = ( element , parentRect , float , align , margins , fixed ) => { if ( align === 'horizontal' ) { const left = fixed ? parentRect . left - element . offsetWidth - margins . x : - element . offsetWidth - margins . x ; element . style . left = `${ left }px` ; if ( float === 'start' ) { const top = fixed ? parentRect . top + margins . y : margins . y ; element . style . top = `${ top }px` ; element . style . bottom = '' ; } else { if ( fixed ) { element . style . top = `${ parentRect . bottom - element . offsetHeight - margins . y }px` ; element . style . bottom = '' ; } else { element . style . bottom = `${ margins . y }px` ; element . style . top = '' ; } } } if ( align === 'vertical' ) { const top = fixed ? parentRect . top + parentRect . height + margins . y : parentRect . height + margins . y ; element . style . top = `${ top }px` ; if ( float === 'start' ) { const left = fixed ? parentRect . left + margins . x : margins . x ; element . style . left = `${ left }px` ; element . style . right = '' ; } else { if ( fixed ) { element . style . left = `${ parentRect . left - element . offsetWidth + parentRect . width - margins . x }px` ; element . style . right = '' ; } else { element . style . right = `${ - margins . x }px` ; element . style . left = '' ; } } } } ; const setInitPositionByCoords = ( element , coords , margins ) => { element . style . top = `${ coords . y + margins . y }px` ; element . style . left = `${ coords . x + margins . x }px` ; } ; const fixFloat = ( element , parentRect , overflowRect , margins , align , fixed ) => { const elementRect = element . getBoundingClientRect ( ) ; if ( align === 'vertical' ) { const left = fixed ? elementRect . left + elementRect . width : parentRect . left + elementRect . width + margins . x ; if ( left > overflowRect . right ) { element . style . left = `${ fixed ? parentRect . right - elementRect . width - margins . x : parentRect . width - margins . x - elementRect . width }px` ; } } else if ( elementRect . top + elementRect . height > overflowRect . bottom ) { element . style . top = '' ; element . style . bottom = `${ fixed ? parentRect . bottom + margins . y : margins . y }px` ; } } ; const resetPositionByParent = ( element , parentRect , align , margins = { x : 0 , y : 0 } , fixed , container ) => { const overflowElement = container ?? ( getScrollParent ( element ) as HTMLElement ) ; const overflowElementRect = fixed ? { top : 0 , left : 0 , width : window . innerWidth , height : window . innerHeight , bottom : window . innerHeight , right : window . innerWidth , } : overflowElement . getBoundingClientRect ( ) ; const rect = element . getBoundingClientRect ( ) ; if ( align === 'horizontal' ) { if ( parentRect . right + rect . width > overflowElementRect . right ) { const right = fixed ? parentRect . right + margins . x : parentRect . width + margins . x ; element . style . right = `${ right }px` ; element . style . left = '' ; const top = fixed ? parentRect . top + margins . y : margins . y ; element . style . top = `${ top }px` ; } if ( parentRect . left - rect . width < overflowElementRect . left ) { const left = fixed ? parentRect . left + margins . x : parentRect . width + margins . x ; element . style . left = `${ left }px` ; element . style . right = '' ; const top = fixed ? parentRect . top + margins . y : margins . y ; element . style . top = `${ top }px` ; } } if ( parentRect . bottom + rect . height > overflowElementRect . bottom ) { if ( align === 'vertical' ) { const top = fixed ? parentRect . top - rect . height - margins . y : - rect . height - margins . y ; element . style . top = `${ top }px` ; } else { const top = fixed ? parentRect . top - rect . height + parentRect . height + margins . y : - rect . height + parentRect . height + margins . y ; element . style . top = `${ top }px` ; } } if ( rect . top < overflowElementRect . top ) { const top = fixed ? parentRect . top + margins . y : margins . y ; element . style . top = `${ top }px` ; element . style . bottom = '' ; } fixFloat ( element , parentRect , overflowElementRect , margins , align , fixed ) ; } ; const resetPositionByCoords = ( element , coords , margins = { x : 0 , y : 0 } ) => { const rect = element . getBoundingClientRect ( ) ; if ( coords . x + rect . width > window . innerWidth ) { element . style . left = `${ coords . x - rect . width - margins . x }px` ; element . style . right = '' ; } else if ( coords . x - rect . width < 0 ) { element . style . left = `${ coords . x + margins . x }px` ; element . style . right = '' ; } if ( coords . y + rect . height > window . innerHeight ) { element . style . top = `${ coords . y - rect . height - margins . y }px` ; } else if ( coords . y - rect . height < 0 ) { element . style . top = `${ coords . y + margins . y }px` ; } } ; const setElement = ( dropRef , align , float , margins , fixed , coords , container ? , parentRef ? , scrollHandler ? , fitListToParent ? ) => { const drop = dropRef . current as HTMLElement ; if ( drop ) { const parent = parentRef ?. current ?. parentElement ?? ( drop . parentElement as HTMLElement ) ; if ( parent ) { drop . style . opacity = '0' ; const parentRect = parent . getBoundingClientRect ( ) ; if ( fixed ) { drop . style . position = 'fixed' ; if ( fitListToParent ) { drop . style . minWidth = `${ parent . offsetWidth }px` ; drop . style . maxWidth = `${ parent . offsetWidth }px` ; drop . style . width = `${ parent . offsetWidth }px` ; } } else { drop . style . position = 'absolute' ; drop . style . minWidth = `` ; drop . style . maxWidth = `` ; drop . style . width = '' ; } drop . style . top = '' ; drop . style . left = '' ; drop . style . bottom = '' ; drop . style . right = '' ; if ( coords ) { setInitPositionByCoords ( drop , coords , margins ) ; resetPositionByCoords ( drop , coords , margins ) ; } else { setInitPositionByParent ( drop , parentRect , float , align , margins , fixed ) ; resetPositionByParent ( drop , parentRect , align , margins , fixed , container ) ; } if ( scrollHandler ) { const scrollable = getScrollParents ( parent ) ; scrollable . forEach ( ( s ) => s . addEventListener ( 'scroll' , scrollHandler ) ) ; } drop . style . opacity = '1' ; } } } ; const updateElement = ( dropRef , align , float , margins , fixed , coords , container ? , parentRef ? , callbacks ? ) => { const drop = dropRef . current as HTMLElement ; if ( drop ) { const parent = parentRef ?. current ?. parentElement ?? ( drop . parentElement as HTMLElement ) ; const parentRect = parent . getBoundingClientRect ( ) ; if ( coords ) { setInitPositionByCoords ( drop , coords , margins ) ; resetPositionByCoords ( drop , coords , margins ) ; } else { setInitPositionByParent ( drop , parentRect , float , align , margins , fixed ) ; resetPositionByParent ( drop , parentRect , align , margins , fixed , container ) ; } const scrollable = getScrollParent ( parent ) ; if ( ! isElementInViewport ( parent , scrollable ) ) { if ( typeof callbacks ?. onHide === 'function' ) { callbacks ?. onHide ( ) ; } } } } ; export const useDrop = ( config ) : React . RefObject < any > => { const { align = 'vertical' , float = 'start' , margins = { x : 0 , y : 0 } , fixed , coords , container , parentRef , followParentWhenScroll , callbacks , fitListToParent , } = config ; const dropRef = useRef < HTMLElement > ( null ) ; const debouncerRender = useDebounce ( setElement , 0 ) ; const debouncerScroll = useDebounce ( updateElement , 0 ) ; const scrollHandler = useCallback ( ( ) => debouncerScroll ( dropRef , align , float , margins , fixed , coords , container , parentRef , callbacks ) , [ debouncerScroll , align , margins , float , fixed , coords , container , parentRef , callbacks ] ) ; useEffect ( ( ) => { debouncerRender ( dropRef , align , float , margins , fixed , coords , container , parentRef , followParentWhenScroll && scrollHandler , fitListToParent , null ) ; const drop = dropRef . current as HTMLElement ; const overflowElement = container ?? ( getScrollParent ( parentRef ?. current ) as HTMLElement ) ; let resizeObserver ; if ( ( window as any ) ?. ResizeObserver && overflowElement ) { resizeObserver = new ( window as any ) . ResizeObserver ( ( ) => { scrollHandler ( ) ; } ) ; resizeObserver . observe ( overflowElement ) ; } return ( ) => { if ( followParentWhenScroll ) { const parent = container ?? ( drop . parentElement as HTMLElement ) ; const scrollable = getScrollParents ( parent ) ; scrollable . forEach ( ( s ) => s . removeEventListener ( 'scroll' , scrollHandler ) ) ; } if ( resizeObserver ) { resizeObserver . unobserve ( overflowElement ) ; } } ; } , [ debouncerRender , scrollHandler , dropRef , align , float , margins , fixed , coords , container , followParentWhenScroll , parentRef , fitListToParent , ] ) ; return dropRef ; } ;