import React from 'react'; import type { AriaPositionProps } from 'react-aria'; import { mergeProps, useObjectRef, useOverlayPosition, useTooltip } from 'react-aria'; import { createPortal } from 'react-dom'; import type { TooltipTriggerState } from 'react-stately'; import { useEnterAnimation, useExitAnimation } from '@react-aria/utils'; import type { DOMAttributes, FocusableElement } from '@react-types/shared'; import { forwardRef } from '../../utils/forwardRef'; export const PREVIEW_OVERLAY_DEFAULT_PROPS = { offset: 8, crossOffset: 0, placement: 'start top', } satisfies Partial; export interface PreviewOverlayProps extends Omit, 'children'> { /** * Preview overlay trigger state. */ state: TooltipTriggerState; /** * Ref to the trigger element. */ triggerRef: React.RefObject; /** * Children to render inside the preview overlay. * This function receives the current state of the tooltip trigger. */ children: (state: TooltipTriggerState) => React.ReactNode; /** * The additional offset applied along the main axis between the overlay and the trigger element. */ offset?: AriaPositionProps['offset']; /** * The additional offset applied along the cross axis between the overlay and the trigger element. */ crossOffset?: AriaPositionProps['crossOffset']; /** * The placement of the overlay with respect to the trigger element. */ placement?: AriaPositionProps['placement']; } export const PreviewOverlay = forwardRef(function PreviewOverlay( props: PreviewOverlayProps, forwardedRef: React.ForwardedRef ) { const overlayRef = useObjectRef(forwardedRef); const isExiting = useExitAnimation(overlayRef, props.state.isOpen); return (props.state.isOpen || isExiting) && ; }); const usePreviewOverlayInnerBehavior = (props: AriaPositionProps, state: TooltipTriggerState) => { const { tooltipProps } = useTooltip(props, state); const { overlayProps } = useOverlayPosition({ ...props, onClose: () => state.close(true), arrowSize: 0, }); return mergeProps(tooltipProps, overlayProps); }; interface PreviewOverlayInnerVisualProps { overlayRef: React.RefObject; isExiting?: boolean; } const usePreviewOverlayInnerVisuals = ({ overlayRef, isExiting }: PreviewOverlayInnerVisualProps) => { const isEntering = useEnterAnimation(overlayRef); return { className: 'preview', 'data-entering': isEntering || undefined, 'data-exiting': isExiting || undefined, }; }; interface PreviewOverlayInnerProps extends PreviewOverlayProps { isExiting?: boolean; } const PreviewOverlayInner = forwardRef(function PreviewOverlayInner( { children, state, triggerRef, placement: placementProp = PREVIEW_OVERLAY_DEFAULT_PROPS.placement, offset = PREVIEW_OVERLAY_DEFAULT_PROPS.offset, crossOffset = PREVIEW_OVERLAY_DEFAULT_PROPS.crossOffset, isExiting, ...domProps }: PreviewOverlayInnerProps, forwardedRef: React.ForwardedRef ) { domProps satisfies DOMAttributes; const overlayRef = useObjectRef(forwardedRef); const previewOverlayInnerBehaviorProps = usePreviewOverlayInnerBehavior( { targetRef: triggerRef, overlayRef, offset, crossOffset, placement: placementProp }, state ); const previewOverlayInnerVisualProps = usePreviewOverlayInnerVisuals({ overlayRef, isExiting }); const previewOverlayInnerProps = mergeProps( previewOverlayInnerBehaviorProps, previewOverlayInnerVisualProps, domProps ); return createPortal(
{children(state)}
, document.body ); });