@dr.pogodin/react-utils
Version:
Collection of generic ReactJS components and utils
97 lines • 6.66 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