import styles from './styles/rt-dropdown.module.css'; import config from './styles/component-values-dropdown.module.css'; import { autoUpdate, flip, FloatingPortal, shift, useDismiss, useFloating, useFocus, useHover, useClick, useInteractions, safePolygon, } from '@floating-ui/react'; import classnames from 'classnames/bind'; import React, { useId } from 'react'; import { cloneElement, forwardRef, useEffect, useMemo, useRef, useState, } from 'react'; import { RtDropdownProps } from './rt-dropdown.types'; import { RtFrame, RtFrameSurfaceFoundation, RtSurface, } from '@dpa/react-frame-intro'; import mergeRefs from 'merge-refs'; import { mapFloatingPlacement, mapPlacementProps, } from './utils/map-placement-props'; import { CSSTransition } from 'react-transition-group'; import { RtBaseProps, useRtBaseProps } from '@dpa/react-hoc'; import { RtGridChildContext, useGridChild } from '@dpa/react-grid'; // eslint-disable-next-line @nx/enforce-module-boundaries import { _V_PREFIX_ } from '../../../../../tools/constants/index'; const cn = classnames.bind(styles); export const RtDropdown = forwardRef( ( props: RtDropdownProps & RtBaseProps, ref: React.ForwardedRef ) => { const { children, slot, trigger = ['focus', 'hover'], position = 'top', alignment = 'middle', model = 'full', radius = 'primary', size = 'md', use, animation = 'smooth', closeOnOutsidePress = true, onShowChange, } = props; const surface: RtSurface = use === 'cloudy' ? 'foundation-pure' : 'foundation-low-tint'; const { id: propsId, ...baseProps } = useRtBaseProps(props, '0.2.2'); const localId = useId(); const id = propsId || localId; const { gridChildClassname, gridAttributes } = useGridChild({ size, radius: radius === 'inherit' ? 'primary' : radius, }); const isRadiusInherit = radius === 'inherit'; const localChildrenRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [sizeStyles, setSizeStyles] = useState({ width: 0, height: 0 }); const [radiusStyles, setRadiusStyles] = useState({ tr: '0px', br: '0px', bl: '0px', tl: '0px', }); function getSizesAndRadius() { if (typeof document !== 'undefined') { if (refs.reference.current) { const { width, height } = refs.reference.current.getBoundingClientRect(); setSizeStyles({ width, height }); const surface = (refs.reference.current as Element).querySelector( `.${_V_PREFIX_}-surface` ); if (surface) { const { borderTopRightRadius: tr, borderBottomRightRadius: br, borderBottomLeftRadius: bl, borderTopLeftRadius: tl, } = window.getComputedStyle(surface); setRadiusStyles({ tr, br, bl, tl, }); } } } } const { refs, floatingStyles, context, placement } = useFloating({ placement: mapPlacementProps(position, alignment, localChildrenRef), strategy: 'fixed', open: isOpen && !props.disabled, middleware: [flip(), shift()], onOpenChange: (nextOpen) => { if (props.disabled) return; setIsOpen(nextOpen); if (onShowChange) onShowChange(nextOpen); if (nextOpen) getSizesAndRadius(); }, whileElementsMounted: autoUpdate, }); useEffect(() => { if (trigger.includes('manual')) { setIsOpen(!!props.show); } }, [props.show, trigger]); const focus = useFocus(context, { enabled: trigger.includes('focus') && !props.disabled, }); const hover = useHover(context, { enabled: trigger.includes('hover') && !props.disabled, handleClose: safePolygon(), }); const click = useClick(context, { enabled: trigger.includes('click') && !props.disabled, }); const dismiss = useDismiss(context, { outsidePress: closeOnOutsidePress, }); const { getReferenceProps, getFloatingProps } = useInteractions([ focus, hover, click, dismiss, ]); const customBorderRadiusStyles = useMemo(() => { const styles: React.CSSProperties = {}; if (radius === 'inherit') { styles.borderRadius = `${radiusStyles.tr} ${radiusStyles.br} ${radiusStyles.bl} ${radiusStyles.tl}`; } return styles; }, [ radius, radiusStyles.bl, radiusStyles.br, radiusStyles.tl, radiusStyles.tr, ]); const customStyles = useMemo(() => { const styles: React.CSSProperties = { ...customBorderRadiusStyles }; if (model === 'full') { if (['top', 'bottom'].includes(position)) { styles.width = `${sizeStyles.width}px`; } else { styles.height = `${sizeStyles.height}px`; } } if (props.zIndex) { styles.zIndex = props.zIndex; } return styles; }, [ customBorderRadiusStyles, model, position, sizeStyles.height, sizeStyles.width, props.zIndex, ]); useEffect(getSizesAndRadius, [refs.reference]); // @ts-expect-error: Свойство ref не существует в типе ReactElement в React@19 const childrenRef = children.ref || children.props.ref; const referenceProps = getReferenceProps(); return ( <> {cloneElement(children, { ...children.props, ref: mergeRefs(refs.setReference, childrenRef, localChildrenRef), 'aria-expanded': isOpen ? 'true' : 'false', 'aria-controls': id, ...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); }, })} } style={{ ...floatingStyles, ...customStyles }} className={cn( config[`rt-dropdown-${size}`], 'rt-dropdown', `rt-dropdown--${alignment}`, `rt-dropdown--${mapFloatingPlacement(placement)}`, `rt-dropdown--${model}`, `rt-dropdown--${radius}` )} {...getFloatingProps()} {...baseProps} >
{slot}
); } ); RtDropdown.displayName = 'RtDropdown';