import cx from 'classnames'; import { Subject } from 'rxjs'; import { IPopoverContext, usePopoverContext } from './Context'; import Portal, { IPortalImperativeHandlers } from '../portal'; import { useWindowEventHandler } from '../utils/component/WindowEventHandler'; import findPositionedParent from '../utils/dom/findPositionedParent'; import { IPopoverPosition } from './position-function'; import { INVISIBLE_POSITION } from './placement'; import { useLazy } from '../utils/hooks/useLazy'; import { useAnimationFramed } from '../utils/hooks/useAnimationFramed'; import { createContext, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { useMounted } from '../utils/hooks/useMounted'; interface IPopoverContentContext { positionChanged$: Subject; } const ContentContext = createContext({ positionChanged$: new Subject(), }); ContentContext.displayName = 'PopoverContentContext'; function translateToContainerCoordinates( containerRect: ClientRect | DOMRect, bb: ClientRect | DOMRect ): ClientRect { const { left, top } = containerRect; return { width: bb.width, height: bb.height, top: bb.top - top, left: bb.left - left, bottom: bb.bottom - top, right: bb.right - left, }; } function getPosition( { visible, placement, popover, cushion }: IPopoverContext, getContainer: () => Element | null, getPositionedParent: () => Element | null, portalRef: React.RefObject ): IPopoverPosition { // skip expensive DOM operations if (!visible) { return INVISIBLE_POSITION; } const container = getContainer(); const parent = getPositionedParent(); const portal = portalRef.current; const anchor = popover.getAnchor?.(); if ( !container || !parent || !portal || !anchor || !(anchor instanceof HTMLElement) ) { return INVISIBLE_POSITION; } const parentRect = parent.getBoundingClientRect(); const { container: content } = portal; const contentRect = content.getBoundingClientRect(); const anchorRect = anchor.getBoundingClientRect(); const relativeRect = translateToContainerCoordinates(parentRect, anchorRect); const position = placement({ relativeRect, anchor, anchorRect, content, contentRect, containerRect: parentRect, container: parent, cushion, }); return position; } export interface IPopoverContentProps { children?: React.ReactNode; } function PopoverContent({ children }: IPopoverContentProps) { const ctx = usePopoverContext(); const { positionChanged$: parentPositionChanged$ } = useContext(ContentContext); const contentCtx = useMemo( () => ({ positionChanged$: new Subject(), }), [] ); const [position, setPosition] = useState(INVISIBLE_POSITION); const contextRef = useRef(ctx); contextRef.current = ctx; const { containerSelector, portalRef } = ctx; const getContainer = useLazy( () => document.querySelector(containerSelector), [containerSelector] ); const getPositionedParent = useLazy(() => { const container = getContainer(); return container && findPositionedParent(container); }, [getContainer]); const mounted = useMounted(); const adjustPosition = useAnimationFramed(() => { if (!mounted.current) { return; } const position = getPosition( contextRef.current, getContainer, getPositionedParent, portalRef ); setPosition(position); }); useImperativeHandle( ctx.contentRef, () => ({ adjustPosition, }), [adjustPosition] ); useWindowEventHandler('resize', adjustPosition); useWindowEventHandler('scroll', adjustPosition, { capture: true, }); useEffect(() => { ctx.popover.positionUpdated(); contentCtx.positionChanged$.next(); }, [ctx.popover, position, contentCtx]); useEffect(() => { const $ = parentPositionChanged$.subscribe(() => { adjustPosition(); }); return () => $.unsubscribe(); }, [parentPositionChanged$, adjustPosition]); return ( {children} ); } export default PopoverContent;