UNPKG

@dr.pogodin/react-utils

Version:

Collection of generic ReactJS components and utils

226 lines (215 loc) 7.34 kB
/** * 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