@dr.pogodin/react-utils
Version:
Collection of generic ReactJS components and utils
226 lines (215 loc) • 7.34 kB
JavaScript
/**
* The actual tooltip component. It is rendered outside the regular document
* hierarchy, and with sub-components managed without React to achieve the best
* performance during animation.
*/
import { useImperativeHandle, useRef } from 'react';
import { createPortal } from 'react-dom';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* Valid placements of the rendered tooltip. They will be overriden when
* necessary to fit the tooltip within the viewport.
*/
export let PLACEMENTS = /*#__PURE__*/function (PLACEMENTS) {
PLACEMENTS["ABOVE_CURSOR"] = "ABOVE_CURSOR";
PLACEMENTS["ABOVE_ELEMENT"] = "ABOVE_ELEMENT";
PLACEMENTS["BELOW_CURSOR"] = "BELOW_CURSOR";
PLACEMENTS["BELOW_ELEMENT"] = "BELOW_ELEMENT";
return PLACEMENTS;
}({});
const ARROW_STYLE_DOWN = ['border-bottom-color:transparent', 'border-left-color:transparent', 'border-right-color:transparent'].join(';');
const ARROW_STYLE_UP = ['border-top-color:transparent', 'border-left-color:transparent', 'border-right-color:transparent'].join(';');
/**
* Generates bounding client rectangles for tooltip components.
* @ignore
* @param tooltip DOM references to the tooltip components.
* @param tooltip.arrow
* @param tooltip.container
* @return Object holding tooltip rectangles in
* two fields.
*/
function calcTooltipRects(tooltip) {
return {
arrow: tooltip.arrow.getBoundingClientRect(),
container: tooltip.container.getBoundingClientRect()
};
}
/**
* Calculates the document viewport size.
* @ignore
* @return {{x, y, width, height}}
*/
function calcViewportRect() {
const {
scrollX,
scrollY
} = window;
const {
documentElement: {
clientHeight,
clientWidth
}
} = document;
return {
bottom: scrollY + clientHeight,
left: scrollX,
right: scrollX + clientWidth,
top: scrollY
};
}
/**
* Calculates tooltip and arrow positions for the placement just above
* the cursor.
* @ignore
* @param {number} x Cursor page-x position.
* @param {number} y Cursor page-y position.
* @param {object} tooltipRects Bounding client rectangles of tooltip parts.
* @param {object} tooltipRects.arrow
* @param {object} tooltipRects.container
* @return {object} Contains the following fields:
* - {number} arrowX
* - {number} arrowY
* - {number} containerX
* - {number} containerY
* - {string} baseArrowStyle
*/
function calcPositionAboveXY(x, y, tooltipRects) {
const {
arrow,
container
} = tooltipRects;
return {
arrowX: 0.5 * (container.width - arrow.width),
arrowY: container.height,
containerX: x - container.width / 2,
containerY: y - container.height - arrow.height / 1.5,
// TODO: Instead of already setting the base style here, we should
// introduce a set of constants for arrow directions, which will help
// to do checks dependant on the arrow direction.
baseArrowStyle: ARROW_STYLE_DOWN
};
}
// const HIT = {
// NONE: false,
// LEFT: 'LEFT',
// RIGHT: 'RIGHT',
// TOP: 'TOP',
// BOTTOM: 'BOTTOM',
// };
/**
* Checks whether
* @param {object} pos
* @param {object} tooltipRects
* @param {object} viewportRect
* @return {HIT}
*/
// function checkViewportFit(pos, tooltipRects, viewportRect) {
// const { containerX, containerY } = pos;
// if (containerX < viewportRect.left + 6) return HIT.LEFT;
// if (containerX > viewportRect.right - 6) return HIT.RIGHT;
// return HIT.NONE;
// }
/**
* Shifts tooltip horizontally to fit into the viewport, while keeping
* the arrow pointed to the XY point.
* @param {number} x
* @param {number} y
* @param {object} pos
* @param {number} pageXOffset
* @param {number} pageXWidth
*/
// function xPageFitCorrection(x, y, pos, pageXOffset, pageXWidth) {
// if (pos.containerX < pageXOffset + 6) {
// pos.containerX = pageXOffset + 6;
// pos.arrowX = Math.max(6, pageX - containerX - arrowRect.width / 2);
// } else {
// const maxX = pageXOffset + docRect.width - containerRect.width - 6;
// if (containerX > maxX) {
// containerX = maxX;
// arrowX = Math.min(
// containerRect.width - 6,
// pageX - containerX - arrowRect.width / 2,
// );
// }
// }
// }
/**
* Sets positions of tooltip components to point the tooltip to the specified
* page point.
* @ignore
* @param pageX
* @param pageY
* @param placement
* @param element DOM reference to the element wrapped by the tooltip.
* @param tooltip
* @param tooltip.arrow DOM reference to the tooltip arrow.
* @param tooltip.container DOM reference to the tooltip container.
*/
function setComponentPositions(pageX, pageY, placement, element, tooltip) {
const tooltipRects = calcTooltipRects(tooltip);
const viewportRect = calcViewportRect();
/* Default container coords: tooltip at the top. */
const pos = calcPositionAboveXY(pageX, pageY, tooltipRects);
if (pos.containerX < viewportRect.left + 6) {
pos.containerX = viewportRect.left + 6;
pos.arrowX = Math.max(6, pageX - pos.containerX - tooltipRects.arrow.width / 2);
} else {
const maxX = viewportRect.right - 6 - tooltipRects.container.width;
if (pos.containerX > maxX) {
pos.containerX = maxX;
pos.arrowX = Math.min(tooltipRects.container.width - 6, pageX - pos.containerX - tooltipRects.arrow.width / 2);
}
}
/* If tooltip has not enough space on top - make it bottom tooltip. */
if (pos.containerY < viewportRect.top + 6) {
pos.containerY += tooltipRects.container.height + 2 * tooltipRects.arrow.height;
pos.arrowY -= tooltipRects.container.height + tooltipRects.arrow.height;
pos.baseArrowStyle = ARROW_STYLE_UP;
}
const containerStyle = `left:${pos.containerX}px;top:${pos.containerY}px`;
tooltip.container.setAttribute('style', containerStyle);
const arrowStyle = `${pos.baseArrowStyle};left:${pos.arrowX}px;top:${pos.arrowY}px`;
tooltip.arrow.setAttribute('style', arrowStyle);
}
/* The Tooltip component itself. */
const Tooltip = ({
children,
ref,
theme
}) => {
// NOTE: The way it has to be implemented, for clean mounting and unmounting
// at the client side, the <Tooltip> is fully mounted into DOM in the next
// rendering cycles, and only then it can be correctly measured and positioned.
// Thus, when we create the <Tooltip> we have to record its target positioning
// details, and then apply them when it is created.
const arrowRef = useRef(null);
const containerRef = useRef(null);
const contentRef = useRef(null);
const pointTo = (pageX, pageY, placement, element) => {
if (!arrowRef.current || !containerRef.current || !contentRef.current) {
throw Error('Internal error');
}
setComponentPositions(pageX, pageY, placement, element, {
arrow: arrowRef.current,
container: containerRef.current,
content: contentRef.current
});
};
useImperativeHandle(ref, () => ({
pointTo
}));
return /*#__PURE__*/createPortal(/*#__PURE__*/_jsxs("div", {
className: theme.container,
ref: containerRef,
children: [/*#__PURE__*/_jsx("div", {
className: theme.arrow,
ref: arrowRef
}), /*#__PURE__*/_jsx("div", {
className: theme.content,
ref: contentRef,
children: children
})]
}), document.body);
};
export default Tooltip;
//# sourceMappingURL=Tooltip.js.map