import { useRef, useState, cloneElement, useId, useEffect, useMemo, forwardRef, } from 'react'; import { arrow, autoUpdate, flip, FloatingArrow, FloatingPortal, offset, safePolygon, shift, useClick, useClientPoint, useDismiss, useFloating, useFocus, useHover, useInteractions, } from '@floating-ui/react'; import { CSSTransition } from 'react-transition-group'; import classNames from 'classnames/bind'; import { RtText } from '@dpa/react-text'; import mergeRefs from 'merge-refs'; import { RtFrame } from '@dpa/react-frame-intro'; import { RtBaseProps, useRtBaseProps } from '@dpa/react-hoc'; import { RtPopoverProps, RtPopoverStyles } from './rt-popover.types'; import config from './styles/component-values-popover.module.css'; import styles from './styles/rt-popover.module.css'; import { arrowData, getArrowD } from './utils/get-arrow-data'; import { mapFontSizes, getLabelsAlign, mapPlacementProps, getRelativePlacement, } from './utils'; import React from 'react'; import { getIsRtl } from './utils/get-is-rtl'; const cn = classNames.bind(styles); export const RtPopover = forwardRef( ( props: RtPopoverProps & RtBaseProps, ref: React.ForwardedRef ) => { const { children, use, modifier, position = 'top', alignment = 'middle', trigger = ['focus', 'hover'], show, label, subLabel, slot, size = 'md', zIndex = 2, disabled = false, closeOnOutsidePress = true, animation = 'smooth', tail = true, maxWidth, shouldUseSafePolygon = true, followCursor = false, onShowChange, } = props; const { id: propsId, ...baseProps } = useRtBaseProps(props, '0.2.2'); const localId = useId(); const id = propsId || localId; const arrowRef = useRef(null); const localChildrenRef = useRef(null); const animatedNodeRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const isRtl = getIsRtl(localChildrenRef); const relativePlacement = getRelativePlacement(position, isRtl); const placement = mapPlacementProps(relativePlacement, alignment); const labelAlign = getLabelsAlign(relativePlacement, alignment, isRtl); const styles: RtPopoverStyles = useMemo(() => { const styles = { padding: 0, arrowWidth: 0, arrowHeight: 0, offset: 0, }; if (animatedNodeRef.current) { const animatedNodeRefcomputedStyle = getComputedStyle( animatedNodeRef.current ); const padding = animatedNodeRefcomputedStyle.getPropertyValue( `--padding-${relativePlacement}` ); const breakpointCoefficient = parseFloat(animatedNodeRefcomputedStyle.getPropertyValue( '--v0-breakpoint-coefficient' )); styles.padding = parseFloat(padding) * breakpointCoefficient; if (['top', 'bottom'].includes(relativePlacement)) { const arrowWidth = animatedNodeRefcomputedStyle.getPropertyValue( '--tail-size-width-top-bottom' ); const arrowHeight = animatedNodeRefcomputedStyle.getPropertyValue( '--tail-size-height-top-bottom' ); styles.arrowWidth = parseFloat(arrowWidth) * breakpointCoefficient; styles.arrowHeight = parseFloat(arrowHeight) * breakpointCoefficient; styles.offset = styles.arrowWidth / 2; } else { const arrowHeight = animatedNodeRefcomputedStyle.getPropertyValue( '--tail-size-width-left-right' ); const arrowWidth = animatedNodeRefcomputedStyle.getPropertyValue( '--tail-size-height-left-right' ); styles.arrowWidth = parseFloat(arrowWidth) * breakpointCoefficient; styles.arrowHeight = parseFloat(arrowHeight) * breakpointCoefficient; styles.offset = styles.arrowWidth / 2; } } return styles; // eslint-disable-next-line react-hooks/exhaustive-deps }, [relativePlacement, animatedNodeRef.current, size]); const { refs, floatingStyles, context } = useFloating({ placement, strategy: 'fixed', open: isOpen && !disabled, middleware: [ offset(styles.padding), shift(), flip(), arrow({ element: arrowRef, padding: arrowData[size]?.offset }), ], onOpenChange: (nextValue) => { if (!disabled) { setIsOpen(nextValue); onShowChange?.(nextValue); } }, whileElementsMounted: autoUpdate, }); useEffect(() => { if (trigger.includes('manual')) { setIsOpen(Boolean(show)); } }, [show, trigger]); const focus = useFocus(context, { enabled: trigger.includes('focus') && !disabled, }); const hover = useHover(context, { enabled: trigger.includes('hover') && !disabled, handleClose: !followCursor && shouldUseSafePolygon ? safePolygon() : null, }); const click = useClick(context, { enabled: trigger.includes('click') && !disabled, }); const dismiss = useDismiss(context, { outsidePress: closeOnOutsidePress, }); const clientPoint = useClientPoint(context, { enabled: followCursor, }); const { getReferenceProps, getFloatingProps } = useInteractions([ focus, hover, click, dismiss, clientPoint, ]); const staticLabelOffset = alignment === 'middle' ? `calc(50% - ${styles.offset}px` : arrowData[size].offset; // @ts-expect-error: Свойство ref не существует в типе ReactElement в React@19 const childrenRef = children.ref || children.props.ref; const referenceProps = getReferenceProps(); return ( <> {cloneElement(children, { ...children.props, ...referenceProps, onFocus(e: React.FocusEvent) { // @ts-expect-error: Ожидается callback referenceProps.onFocus?.(e); children.props.onFocus?.(e); }, onBlur(e: React.FocusEvent) { // @ts-expect-error: Ожидается callback referenceProps.onBlur?.(e); children.props.onBlur?.(e); }, ref: mergeRefs(refs.setReference, childrenRef, localChildrenRef), 'data-popover-state': context.open ? 'open' : 'closed', 'aria-labelledby': id, 'aria-controls': id, })}
{tail && ( )} {(label || subLabel) && (
{label && (
{label}
)} {subLabel && (
{subLabel}
)}
)} {slot}
); } ); RtPopover.displayName = 'RtPopover';