Files
ohif-viewer/platform/ui/src/components/Tooltip/Tooltip.tsx
T

207 lines
5.6 KiB
TypeScript

import React, { useState, useEffect, useRef, useMemo } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import debounce from 'lodash.debounce';
import ReactDOM from 'react-dom';
import './tooltip.css';
const arrowPositionStyle = {
bottom: {
top: -15,
left: '50%',
transform: 'translateX(-50%)',
},
'bottom-left': { top: -15, left: 5 },
'bottom-right': { top: -15, right: 5 },
right: {
top: 'calc(50% - 8px)',
left: -15,
transform: 'rotate(270deg)',
},
left: {
top: 'calc(50% - 8px)',
right: -15,
transform: 'rotate(-270deg)',
},
top: {
bottom: -15,
left: '50%',
transform: 'translateX(-50%) rotate(180deg)',
},
};
const Tooltip = ({
content,
secondaryContent = null,
isSticky = false,
position = 'bottom',
className,
tight = false,
children,
isDisabled = false,
tooltipBoxClassName,
// time to show/hide the tooltip on mouse over and mouse out events (default: 300ms)
showHideDelay = 300,
onHide,
}) => {
const [isActive, setIsActive] = useState(false);
const isOpen = useMemo(
() => (isSticky || isActive) && !isDisabled,
[isSticky, isActive, isDisabled]
);
const { t } = useTranslation('Buttons');
const tooltipContainer = document.getElementById('react-portal');
const [coords, setCoords] = useState({ x: 999999, y: 999999 });
const parentRef = useRef(null);
const tooltipRef = useRef(null);
const handleMouseOverDebounced = useMemo(
() => debounce(() => setIsActive(true), showHideDelay),
[showHideDelay]
);
const handleMouseOutDebounced = useMemo(
() => debounce(() => setIsActive(false), showHideDelay),
[showHideDelay]
);
const handleMouseOver = () => {
handleMouseOutDebounced.cancel();
handleMouseOverDebounced();
};
const handleMouseOut = () => {
handleMouseOverDebounced.cancel();
handleMouseOutDebounced();
};
useEffect(() => {
return () => {
handleMouseOverDebounced.cancel();
handleMouseOutDebounced.cancel();
};
}, [handleMouseOverDebounced, handleMouseOutDebounced]);
useEffect(() => {
if (!isOpen && onHide) {
onHide();
}
}, [isOpen, onHide]);
useEffect(() => {
if (parentRef.current && tooltipRef.current) {
const parentRect = parentRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const parentWidth = parentRect.width;
const parentHeight = parentRect.height;
const tooltipWidth = tooltipRect.width;
let newX = 0;
let newY = 0;
switch (position) {
case 'bottom':
newX = parentRect.left + parentWidth / 2;
newY = parentRect.top + parentHeight;
break;
case 'top':
newX = parentRect.left + parentWidth / 2;
newY = parentRect.top - parentHeight * 2;
break;
case 'right':
newX = parentRect.left + parentWidth;
newY = parentRect.top + parentHeight / 2;
break;
case 'left':
newX = parentRect.left - tooltipWidth - 10;
newY = parentRect.top + parentHeight / 2;
break;
case 'bottom-left':
newX = parentRect.left;
newY = parentRect.top + parentHeight;
break;
case 'bottom-right':
newX = parentRect.left - tooltipWidth + parentWidth;
newY = parentRect.top + parentHeight;
break;
default:
break;
}
setCoords({ x: newX, y: newY });
}
}, [isOpen, position, parentRef.current, tooltipRef.current]);
const tooltipContent = (
<div
className={classnames(`tooltip tooltip-${position} block`, 'z-50')}
style={{
position: 'fixed',
top: coords.y,
left: isOpen ? coords.x : 999999,
}}
>
<div
ref={tooltipRef}
className={classnames(
'tooltip-box bg-primary-dark border-secondary-light w-max-content relative inset-x-auto top-full rounded border text-base text-white',
{
'py-[6px] px-[8px]': !tight,
},
tooltipBoxClassName
)}
>
<div>{typeof content === 'string' ? t(content) : content}</div>
<div className="text-aqua-pale">
{typeof secondaryContent === 'string' ? t(secondaryContent) : secondaryContent}
</div>
<svg
className="text-primary-dark stroke-secondary-light absolute h-4"
style={arrowPositionStyle[position]}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M24 22l-12-20l-12 20"
/>
</svg>
</div>
</div>
);
return (
<div
ref={parentRef}
className={classnames('relative', className)}
onMouseOver={handleMouseOver}
onFocus={handleMouseOver}
onMouseOut={handleMouseOut}
onBlur={handleMouseOut}
role="tooltip"
>
{children}
{tooltipContainer && ReactDOM.createPortal(tooltipContent, tooltipContainer)}
</div>
);
};
Tooltip.propTypes = {
isDisabled: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
secondaryContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
position: PropTypes.oneOf(['bottom', 'bottom-left', 'bottom-right', 'left', 'right', 'top']),
isSticky: PropTypes.bool,
tight: PropTypes.bool,
children: PropTypes.node.isRequired,
className: PropTypes.string,
tooltipBoxClassName: PropTypes.string,
showHideDelay: PropTypes.number,
onHide: PropTypes.func,
};
export default Tooltip;