'use client'; import * as React from 'react'; import { hasReactNode } from '@vkontakte/vkjs'; import { mergeStyle } from '../../helpers/mergeStyle'; import { useExternRef } from '../../hooks/useExternRef'; import { useGlobalEscKeyDown } from '../../hooks/useGlobalEscKeyDown'; import { usePatchChildren } from '../../hooks/usePatchChildren'; import { createPortal } from '../../lib/createPortal'; import { autoUpdateFloatingElement, convertFloatingDataToReactCSSProperties, type FloatingComponentProps, useFloating, useFloatingMiddlewaresBootstrap, usePlacementChangeCallback, } from '../../lib/floating'; import { LockFloatingPositionContext } from '../../lib/floating/LockFloatingPosition/LockFloatingPosition'; 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 { RootComponent } from '../RootComponent/RootComponent'; 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'] | undefined; /** * Включает абсолютное смещение по `arrowOffset`. */ isStaticArrowOffset?: FloatingArrowProps['isStaticOffset'] | undefined; }; export interface OnboardingTooltipProps extends AllowedFloatingComponentProps, AllowedTooltipBaseProps, AllowedFloatingArrowProps { /** * Управление поведением возврата фокуса при закрытии всплывающего окна. * @default true */ restoreFocus?: boolean | (() => boolean | HTMLElement) | undefined; /** * Скрывает стрелку, указывающую на якорный элемент. */ disableArrow?: boolean | undefined; /** * Обработчик, который вызывается при нажатии по любому месту в пределах экрана. */ onClose?: ((this: void) => void) | undefined; /** * [a11y] Метка для подложки-кнопки, для описания того, что произойдёт при нажатии. */ overlayLabel?: string | undefined; } /** * @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 isLock = React.useContext(LockFloatingPositionContext); const { x: floatingDataX, y: floatingDataY, isPositioned, refs, placement: resolvedPlacement, middlewareData: { arrow: arrowCoords }, } = useFloating({ strategy: positionStrategy, ...(strictPlacement !== undefined && { placement: strictPlacement }), middleware: middlewares, ...(!isLock && { whileElementsMounted: (...args) => autoUpdateFloatingElement(...args, { elementResize: true }), }), }); const tooltipRef = useExternRef(getRootRef, refs.setFloating); const focusTrapRootRef = React.useRef(null); 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)', ); } useGlobalEscKeyDown(!!shown, onClose); let tooltip: React.ReactPortal | null = null; if (shown) { const floatingStyle = convertFloatingDataToReactCSSProperties({ strategy: positionStrategy, x: floatingDataX, y: floatingDataY, }); if (!isPositioned) { floatingStyle.visibility = 'hidden'; } tooltip = createPortal(