import React, { useMemo, useRef, useState } from 'react'; import isEqual from 'lodash/isEqual'; import { useIsomorphicLayoutEffect, useObjectDependency, usePrevious, } from '@leafygreen-ui/hooks'; import { useMigrationContext, usePopoverPortalContainer, usePopoverPropsContext, } from '@leafygreen-ui/leafygreen-provider'; import { getElementDocumentPosition } from '../../utils/positionUtils'; import { ElementPosition, PopoverProps, RenderMode, UseContentNodeReturnObj, UseReferenceElementReturnObj, } from '../Popover.types'; /** * This hook handles logic for determining what prop values are used for the `Popover` * component. If a prop is not provided, the value from the `PopoverContext` will be used. */ export function usePopoverProps({ renderMode: renderModeProp, dismissMode, onToggle, portalClassName, portalContainer, portalRef, scrollContainer, onEnter, onEntering, onEntered, onExit, onExiting, onExited, popoverZIndex: popoverZIndexProp, spacing, ...rest }: Partial< Omit< PopoverProps, | 'active' | 'adjustOnMutation' | 'align' | 'children' | 'className' | 'justify' | 'refEl' > >) { const { forceUseTopLayer } = useMigrationContext(); const context = usePopoverPropsContext(); const popoverPortalContext = usePopoverPortalContainer(); const renderMode = forceUseTopLayer ? RenderMode.TopLayer : renderModeProp || context.renderMode; const usePortal = renderMode === RenderMode.Portal; const useTopLayer = renderMode === RenderMode.TopLayer; const topLayerProps = useTopLayer ? { dismissMode: dismissMode || context.dismissMode, onToggle: onToggle || context.onToggle, } : {}; const portalProps = usePortal ? { portalClassName: portalClassName || context.portalClassName, portalContainer: portalContainer || context.portalContainer || popoverPortalContext.portalContainer, portalRef: portalRef || context.portalRef, scrollContainer: scrollContainer || context.scrollContainer || popoverPortalContext.scrollContainer, } : {}; const reactTransitionGroupProps = { onEnter: onEnter || context.onEnter, onEntering: onEntering || context.onEntering, onEntered: onEntered || context.onEntered, onExit: onExit || context.onExit, onExiting: onExiting || context.onExiting, onExited: onExited || context.onExited, }; const styleProps = { popoverZIndex: useTopLayer ? undefined : popoverZIndexProp || context.popoverZIndex, spacing: spacing || context.spacing, }; return { renderMode, usePortal, ...topLayerProps, ...portalProps, ...reactTransitionGroupProps, ...styleProps, ...rest, }; } /** * This hook handles logic for determining the reference element for the popover element. * 1. If a `refEl` is provided, the ref value is used as the reference element. * 2. As a fallback, a hidden placeholder element is rendered, and the parent element of the * placeholder is used as the reference element. * * Additionally, this hook calculates the document position of the reference element. */ export function useReferenceElement( refEl?: PopoverProps['refEl'], ): UseReferenceElementReturnObj { const [placeholderElement, setPlaceholderElement] = useState(null); const [referenceElement, setReferenceElement] = useState( null, ); const prevReferenceElement = usePrevious(refEl?.current); // If the DOM element has changed, we need to update the reference element. // Store this variable so we can trigger the useLayoutEffect const didRefElementChange = !isEqual(prevReferenceElement, refEl?.current); useIsomorphicLayoutEffect(() => { if (refEl && refEl.current) { if (didRefElementChange) { setReferenceElement(refEl.current); } return; } const maybeParentEl = placeholderElement !== null && placeholderElement.parentNode; if (maybeParentEl && maybeParentEl instanceof HTMLElement) { setReferenceElement(maybeParentEl); return; } }, [didRefElementChange, placeholderElement, refEl]); return { referenceElement, setPlaceholderElement, }; } /** * */ export function useReferenceElementPosition( referenceElement: HTMLElement | null, scrollContainer?: PopoverProps['scrollContainer'], ): ElementPosition { // Recalculate the currentPos when the refEl object changes const currentPosMemo = useMemo( () => getElementDocumentPosition(referenceElement, scrollContainer, true), [referenceElement, scrollContainer], ); // Ensures the same object reference is returned // even if `currentPosMemo` is a new object reference with identical values const referenceElDocumentPos = useObjectDependency(currentPosMemo); return referenceElDocumentPos; } export function useContentNode(): UseContentNodeReturnObj { const [contentNode, setContentNode] = React.useState( null, ); const contentNodeRef = useRef(contentNode); contentNodeRef.current = contentNode; return { contentNode, contentNodeRef, setContentNode, }; }