@dr.pogodin/react-utils
Version:
Collection of generic ReactJS components and utils
117 lines (114 loc) • 4.12 kB
JavaScript
/* global window */
import { useEffect, useRef, useState } from 'react';
import themed from '@dr.pogodin/react-themes';
import Tooltip, { PLACEMENTS } from "./Tooltip";
import defaultTheme from "./default-theme.scss";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* Implements a simple to use and themeable tooltip component, _e.g._
* ```js
* <WithTooltip tip="This is example tooltip.">
* <p>Hover to see the tooltip.</p>
* </WithTooltip>
* ```
* **Children:** Children are rendered in the place of `<WithTooltip>`,
* and when hovered the tooltip is shown. By default the wrapper itself is
* `<div>` block with `display: inline-block`.
* @param tip – Anything React is able to render,
* _e.g._ a tooltip text. This will be the tooltip content.
* @param {WithTooltipTheme} props.theme _Ad hoc_ theme.
*/
const Wrapper = ({
children,
placement = PLACEMENTS.ABOVE_CURSOR,
tip,
theme
}) => {
const {
current: heap
} = useRef({
lastCursorX: 0,
lastCursorY: 0,
timerId: undefined,
triggeredByTouch: false
});
const tooltipRef = useRef(null);
const wrapperRef = useRef(null);
const [showTooltip, setShowTooltip] = useState(false);
const updatePortalPosition = (cursorX, cursorY) => {
if (showTooltip) {
const wrapperRect = wrapperRef.current.getBoundingClientRect();
if (cursorX < wrapperRect.left || cursorX > wrapperRect.right || cursorY < wrapperRect.top || cursorY > wrapperRect.bottom) {
setShowTooltip(false);
} else if (tooltipRef.current) {
tooltipRef.current.pointTo(cursorX + window.scrollX, cursorY + window.scrollY, placement, wrapperRef.current);
}
} else {
heap.lastCursorX = cursorX;
heap.lastCursorY = cursorY;
// If tooltip was triggered by a touch, we delay its opening by a bit,
// to ensure it was not a touch-click - in the case of touch click we
// want to do the click, rather than show the tooltip, and the delay
// gives click handler a chance to abort the tooltip openning.
if (heap.triggeredByTouch) {
heap.timerId ??= setTimeout(() => {
heap.triggeredByTouch = false;
heap.timerId = undefined;
setShowTooltip(true);
}, 300);
// Otherwise we can just open the tooltip right away.
} else setShowTooltip(true);
}
};
useEffect(() => {
if (showTooltip && tip !== null) {
// This is necessary to ensure that even when a single mouse event
// arrives to a tool-tipped component, the tooltip is correctly positioned
// once opened (because similar call above does not have effect until
// the tooltip is fully mounted, and that is delayed to future rendering
// cycle due to the implementation).
if (tooltipRef.current) {
tooltipRef.current.pointTo(heap.lastCursorX + window.scrollX, heap.lastCursorY + window.scrollY, placement, wrapperRef.current);
}
const listener = () => {
setShowTooltip(false);
};
window.addEventListener('scroll', listener);
return () => {
window.removeEventListener('scroll', listener);
};
}
return undefined;
}, [heap.lastCursorX, heap.lastCursorY, placement, showTooltip, tip]);
return /*#__PURE__*/_jsxs("div", {
className: theme.wrapper,
onClick: () => {
if (heap.timerId) {
clearTimeout(heap.timerId);
heap.timerId = undefined;
heap.triggeredByTouch = false;
}
},
onMouseLeave: () => {
setShowTooltip(false);
},
onMouseMove: e => {
updatePortalPosition(e.clientX, e.clientY);
},
onTouchStart: () => {
heap.triggeredByTouch = true;
},
ref: wrapperRef,
role: "presentation",
children: [showTooltip && tip !== null ? /*#__PURE__*/_jsx(Tooltip, {
ref: tooltipRef,
theme: theme,
children: tip
}) : null, children]
});
};
const ThemedWrapper = themed(Wrapper, 'WithTooltip', defaultTheme);
const e = ThemedWrapper;
e.PLACEMENTS = PLACEMENTS;
export default e;
//# sourceMappingURL=index.js.map