import React, { ReactElement, ReactNode, Component, Fragment } from 'react'; import PropTypes, { Requireable } from 'prop-types'; import classNames from '../../lib/classNames'; import getClassName from '../../helpers/getClassName'; import ReactDOM from 'react-dom'; import { OldRef } from '../../types'; import { canUseDOM } from '../../lib/dom'; interface TooltipPortalProps extends Partial { target?: HTMLElement; } interface TooltipPortalState { x: number; y: number; } interface TooltipPortalContextType { document: Requireable<{}>; panel: Requireable; } type GetBoundingTargetRect = () => { x: number; y: number; width: number; height: number; }; const isDOMTypeElement = (element: ReactElement) => { return React.isValidElement(element) && typeof element.type === 'string'; }; const baseClassName = getClassName('Tooltip'); class TooltipPortal extends Component { constructor(props: TooltipPortalProps) { super(props); this.state = { x: 0, y: 0, }; this.fixedPortal = false; const { target } = props; const closestFixed = target.closest('.FixedLayout'); const closestHeader = target.closest('.PanelHeader__in'); const closestPanel = target.closest('.Panel__in'); if (closestFixed || closestHeader) { this.fixedPortal = true; } this.portalTarget = closestFixed || closestHeader || closestPanel; } static contextTypes: TooltipPortalContextType = { document: PropTypes.object, panel: PropTypes.string, }; get document() { return this.context.document || document; } fixedPortal: boolean; el: HTMLDivElement; portalTarget: HTMLElement; getBoundingTargetRect: GetBoundingTargetRect = () => { const { target } = this.props; const targetBounds = target.getBoundingClientRect(); const portalBounds = this.portalTarget.getBoundingClientRect(); return { width: targetBounds.width, height: targetBounds.height, x: targetBounds.left - portalBounds.left, y: targetBounds.top - portalBounds.top, }; }; componentWillUnmount() { this.document.removeEventListener('click', this.props.onClose); } componentDidMount() { const { offsetY, offsetX, alignX, alignY } = this.props; const coords = this.getBoundingTargetRect(); this.document.addEventListener('click', this.props.onClose); this.setState({ x: coords.x + offsetX + (alignX === 'right' ? coords.width - this.el.offsetWidth : 0), y: coords.y + (alignY === 'top' ? -this.el.offsetHeight - offsetY : coords.height + offsetY), }); } getRef: OldRef = (el: HTMLDivElement) => this.el = el; render() { const { header, text, alignX, alignY, cornerOffset } = this.props; return ReactDOM.createPortal(
{header &&
{header}
} {text &&
{text}
}
, this.portalTarget); } } export interface TooltipProps { /** * **Важно**: если в `children` передан React-компонент, то необходимо убедиться в том, что он поддерживает * свойство `getRootRef`, которое должно возвращаться ссылку на корневой DOM-элемент компонента, * иначе тултип показан не будет. Если передан React-element, то такой проблемы нет. */ children: ReactNode; /** * Если передан `false`, то рисуется просто `children`. */ isShown: boolean; /** * Текст тултипа. */ text?: ReactNode; /** * Заголовок тултипа. */ header?: ReactNode; /** * Положение по горизонтали (прижатие к левому или правому краю `children`). */ alignX?: 'left' | 'right'; /** * Положение по вертикали (расположение над или под `children`). */ alignY?: 'top' | 'bottom'; /** * Сдвиг по горизонтали (относительно портала, в котором рисуется тултип). */ offsetX?: number; /** * Сдвиг по вертикали (относительно портала, в котором рисуется тултип). */ offsetY?: number; /** * Сдвиг треугольника (относительно ширины тултипа). */ cornerOffset?: number; /** * Callback, который вызывается при клике по любому месту в пределах экрана. */ onClose(): void; } export interface TooltipState { ready: boolean; } export default class Tooltip extends Component { static defaultProps: Partial = { offsetX: 0, offsetY: 15, alignX: 'left', alignY: 'bottom', cornerOffset: 0, isShown: true, }; state: TooltipState = { ready: false, }; targetEl: HTMLElement; componentDidMount() { if (canUseDOM) { this.targetEl && this.setState({ ready: true }); } } getRef: OldRef = (el: HTMLDivElement) => this.targetEl = el; render() { const { children, isShown, ...portalProps } = this.props; const child = React.cloneElement(children as ReactElement, { [isDOMTypeElement(children as ReactElement) ? 'ref' : 'getRootRef']: this.getRef, }); if (!isShown || !this.state.ready) { return child; } return ( {child} ); } }