import React, { memo, useEffect, useRef, useId } from 'react'; import styled, { css } from 'styled-components'; import type { JSX, PropsWithChildren } from 'react'; import type { TooltipProps } from '@redocly/theme/core/types'; import { useControl, useOutsideClick, useTooltipFallbackPlacement, } from '@redocly/theme/core/hooks'; import { Portal } from '@redocly/theme/components/Portal/Portal'; type Props = Exclude & { arrowPosition?: 'left' | 'right' | 'center'; }; function TooltipComponent({ children, isOpen, tip, withArrow = true, placement = 'top', fallbackPlacements, className = 'default', width, dataTestId, disabled = false, arrowPosition = 'center', onClick, }: PropsWithChildren): JSX.Element { const tooltipWrapperRef = useRef(null); const tooltipBodyRef = useRef(null); const { isOpened, handleOpen, handleClose } = useControl(isOpen); const anchorName = `--tooltip${useId().replace(/:/g, '')}`; const { activePlacement, activeArrowPosition } = useTooltipFallbackPlacement({ isOpened, placement, arrowPosition, fallbackPlacements, tooltipBodyRef, }); useOutsideClick(isOpened ? [tooltipWrapperRef, tooltipBodyRef] : tooltipWrapperRef, handleClose); const isControlled = isOpen !== undefined; useEffect(() => { if (!isControlled) return; if (isOpen && !disabled) { handleOpen(); } else { handleClose(); } }, [isOpen, disabled, isControlled, handleOpen, handleClose]); const controllers = !isControlled && !disabled ? { onMouseEnter: handleOpen, onMouseLeave: handleClose, onClick: (e: React.MouseEvent) => { onClick?.(e); handleClose(); }, onFocus: handleOpen, onBlur: handleClose, } : {}; return ( {children} {isOpened && !disabled && ( {tip} )} ); } export const Tooltip = memo>(TooltipComponent); const PLACEMENTS = { top: css>` bottom: anchor(top); ${({ withArrow, arrowPosition }) => withArrow && arrowPosition === 'left' ? css` transform: translate(-32px, -6px); left: anchor(center); ` : arrowPosition === 'right' ? css` transform: translate(32px, -6px); right: anchor(center); ` : css` transform: translate(-50%, -6px); left: anchor(center); `} ${({ withArrow, arrowPosition }) => withArrow && css` &::after { border-left: 14px solid transparent; border-right: 14px solid transparent; border-top-width: 8px; border-top-style: solid; border-radius: 2px; bottom: 0; ${arrowPosition === 'left' && 'left: 16px; transform: translateY(100%);'} ${arrowPosition === 'center' && 'left: 50%; transform: translate(-50%, 100%);'} ${arrowPosition === 'right' && 'right: 16px; transform: translateY(100%);'} } `} `, bottom: css>` top: anchor(bottom); ${({ withArrow, arrowPosition }) => withArrow && arrowPosition === 'left' ? css` transform: translate(-32px, 6px); left: anchor(center); ` : arrowPosition === 'right' ? css` transform: translate(32px, 6px); right: anchor(center); ` : css` transform: translate(-50%, 6px); left: anchor(center); `} ${({ withArrow, arrowPosition }) => withArrow && css` &::after { border-left: 14px solid transparent; border-right: 14px solid transparent; border-bottom-width: 8px; border-bottom-style: solid; border-radius: 0 0 2px 2px; top: 0; ${arrowPosition === 'left' && 'left: 16px; transform: translateY(-100%);'} ${arrowPosition === 'center' && 'left: 50%; transform: translate(-50%, -100%);'} ${arrowPosition === 'right' && 'right: 16px; transform: translateY(-100%);'} } `} `, left: css>` transform: translate(-100%, -50%); margin-left: -7px; top: anchor(center); left: anchor(left); ${({ withArrow }) => withArrow && css` &::after { border-top: 14px solid transparent; border-bottom: 14px solid transparent; border-left-width: 8px; border-left-style: solid; border-radius: 2px 0 0 2px; right: -9px; top: 50%; transform: translateY(-50%); } `} `, right: css>` transform: translate(0, -50%); margin-left: 7px; top: anchor(center); left: anchor(right); ${({ withArrow }) => withArrow && css` &::after { border-top: 14px solid transparent; border-bottom: 14px solid transparent; border-right-width: 8px; border-right-style: solid; border-radius: 0 2px 2px 0; left: -9px; top: 50%; transform: translateY(-50%); } `} `, }; const TooltipWrapper = styled.div.attrs<{ anchorName: string }>(({ anchorName }) => ({ style: { anchorName: anchorName, } as React.CSSProperties, }))<{ anchorName: string }>` display: flex; `; const TooltipBody = styled.span.attrs<{ anchorName: string }>(({ anchorName }) => ({ style: { positionAnchor: anchorName, } as React.CSSProperties, }))< Pick, 'placement' | 'withArrow' | 'arrowPosition'> & { width?: string; anchorName: string; } >` position: fixed; min-width: 64px; padding: var(--tooltip-padding); max-width: var(--tooltip-max-width); white-space: normal; word-break: normal; overflow-wrap: break-word; text-align: left; border-radius: var(--border-radius-md); transition: opacity 0.3s ease-out; font-size: var(--font-size-base); line-height: var(--line-height-base); z-index: var(--z-index-overlay); &::after { position: absolute; content: ' '; display: inline-block; width: 0; height: 0; border-color: var(--tooltip-arrow-color, var(--tooltip-bg-color)); } background: var(--tooltip-bg-color); color: var(--tooltip-text-color); border: var(--tooltip-border-width, 0) var(--tooltip-border-style, solid) var(--tooltip-border-color, transparent); box-shadow: var(--bg-raised-shadow); width: ${({ width }) => width || 'max-content'}; ${({ placement }) => css` ${PLACEMENTS[placement]}; `} `;