'use client'; import * as React from 'react'; import { hasReactNode } from '@vkontakte/vkjs'; import { mergeStyle } from '../../helpers/mergeStyle'; import { useExternRef } from '../../hooks/useExternRef'; import { type UseFocusTrapProps } from '../../hooks/useFocusTrap'; import { usePatchChildren } from '../../hooks/usePatchChildren'; import { createPortal } from '../../lib/createPortal'; import { autoUpdateFloatingElement, convertFloatingDataToReactCSSProperties, type FloatingComponentProps, useFloating, useFloatingMiddlewaresBootstrap, usePlacementChangeCallback, } from '../../lib/floating'; import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { warnOnce } from '../../lib/warnOnce'; import { DEFAULT_ARROW_HEIGHT, DEFAULT_ARROW_PADDING } from '../FloatingArrow/DefaultIcon'; import type { FloatingArrowProps } from '../FloatingArrow/FloatingArrow'; import { FocusTrap } from '../FocusTrap/FocusTrap'; import { useNavTransition } from '../NavTransitionContext/NavTransitionContext'; import { TOOLTIP_MAX_WIDTH, TooltipBase, type TooltipBaseProps } from '../TooltipBase/TooltipBase'; import { onboardingTooltipContainerAttr } from './OnboardingTooltipContainer'; import styles from './OnboardingTooltip.module.css'; const warn = warnOnce('OnboardingTooltip'); type AllowedFloatingComponentProps = Pick< FloatingComponentProps, | 'arrowHeight' | 'arrowPadding' | 'arrowRef' | 'placement' | 'offsetByMainAxis' | 'offsetByCrossAxis' | 'shown' | 'children' | 'onPlacementChange' | 'disableFlipMiddleware' | 'disableShiftMiddleware' | 'disableFocusTrap' | 'overflowPadding' >; type AllowedTooltipBaseProps = Omit< TooltipBaseProps, 'arrowProps' | 'closeIconLabel' | 'onCloseIconClick' >; type AllowedFloatingArrowProps = { /** * Сдвиг стрелки относительно текущих координат. */ arrowOffset?: FloatingArrowProps['offset']; /** * Включает абсолютное смещение по `arrowOffset`. */ isStaticArrowOffset?: FloatingArrowProps['isStaticOffset']; }; export interface OnboardingTooltipProps extends AllowedFloatingComponentProps, AllowedTooltipBaseProps, AllowedFloatingArrowProps, Pick { /** * Скрывает стрелку, указывающую на якорный элемент. */ disableArrow?: boolean; /** * Обработчик, который вызывается при нажатии по любому месту в пределах экрана. */ onClose?: (this: void) => void; /** * [a11y] Метка для подложки-кнопки, для описания того, что произойдёт при нажатии. */ overlayLabel?: string; } /** * @see https://vkui.io/components/onboarding-tooltip */ export const OnboardingTooltip = ({ 'id': idProp, children, 'shown': shownProp = true, arrowPadding = DEFAULT_ARROW_PADDING, arrowHeight = DEFAULT_ARROW_HEIGHT, offsetByMainAxis = 0, offsetByCrossAxis = 0, arrowOffset = 0, isStaticArrowOffset = false, onClose, 'placement': placementProp = 'bottom-start', maxWidth = TOOLTIP_MAX_WIDTH, 'style': styleProp, getRootRef, disableArrow = false, onPlacementChange, disableFlipMiddleware = false, disableShiftMiddleware = false, overlayLabel = 'Закрыть', title, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, restoreFocus, disableFocusTrap, overflowPadding, ...restProps }: OnboardingTooltipProps): React.ReactNode => { const generatedId = React.useId(); const tooltipId = idProp || generatedId; const { entering } = useNavTransition(); const [arrowRef, setArrowRef] = React.useState(null); const [tooltipContainer, setTooltipContainer] = React.useState(null); const [positionStrategy, setPositionStrategy] = React.useState<'fixed' | 'absolute'>('absolute'); const shown = shownProp && tooltipContainer && !entering; const { middlewares, strictPlacement } = useFloatingMiddlewaresBootstrap({ placement: placementProp, offsetByMainAxis, offsetByCrossAxis, arrowRef, arrow: !disableArrow, arrowHeight, arrowPadding, disableFlipMiddleware, disableShiftMiddleware, overflowPadding, }); const { x: floatingDataX, y: floatingDataY, refs, placement: resolvedPlacement, middlewareData: { arrow: arrowCoords }, } = useFloating({ strategy: positionStrategy, placement: strictPlacement, middleware: middlewares, whileElementsMounted: autoUpdateFloatingElement, }); const tooltipRef = useExternRef(getRootRef, refs.setFloating); const [childRef, child] = usePatchChildren(children, { 'aria-describedby': shown ? tooltipId : undefined, }); usePlacementChangeCallback(placementProp, resolvedPlacement, onPlacementChange); const titleId = React.useId(); if (process.env.NODE_ENV === 'development' && !title && !ariaLabel && !ariaLabelledBy) { warn( 'Если "title" не используется, то необходимо задать либо "aria-label", либо "aria-labelledby" (см. правило axe aria-dialog-name)', ); } let tooltip: React.ReactPortal | null = null; if (shown) { const floatingStyle = convertFloatingDataToReactCSSProperties({ strategy: positionStrategy, x: floatingDataX, y: floatingDataY, }); tooltip = createPortal(