Initial commit from prod-batam
This commit is contained in:
104
platform/ui/src/components/Tooltip/PortalTooltip.tsx
Normal file
104
platform/ui/src/components/Tooltip/PortalTooltip.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Card from './PortalTooltipCard';
|
||||
|
||||
const portalNodes = {};
|
||||
|
||||
/**
|
||||
* A portal based tooltip component.
|
||||
*
|
||||
* This component has been repurposed and modified
|
||||
* for OHIF usage: https://github.com/romainberger/react-portal-tooltip
|
||||
*/
|
||||
export default class PortalTooltip extends React.Component {
|
||||
static propTypes = {
|
||||
parent: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
active: PropTypes.bool,
|
||||
group: PropTypes.string,
|
||||
tooltipTimeout: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
active: false,
|
||||
group: 'main',
|
||||
tooltipTimeout: 0,
|
||||
};
|
||||
|
||||
createPortal() {
|
||||
portalNodes[this.props.group] = {
|
||||
node: document.createElement('div'),
|
||||
timeout: false,
|
||||
};
|
||||
portalNodes[this.props.group].node.className = 'ToolTipPortal';
|
||||
document.body.appendChild(portalNodes[this.props.group].node);
|
||||
}
|
||||
|
||||
renderPortal(props) {
|
||||
if (!portalNodes[this.props.group]) {
|
||||
this.createPortal();
|
||||
}
|
||||
const { parent, ...other } = props;
|
||||
const parentEl = typeof parent === 'string' ? document.querySelector(parent) : parent;
|
||||
ReactDOM.render(
|
||||
<Card
|
||||
parentEl={parentEl}
|
||||
{...other}
|
||||
/>,
|
||||
portalNodes[this.props.group].node
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderPortal(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
(!portalNodes[this.props.group] && !nextProps.active) ||
|
||||
(!this.props.active && !nextProps.active)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const props = { ...nextProps };
|
||||
const newProps = { ...nextProps };
|
||||
|
||||
if (portalNodes[this.props.group] && portalNodes[this.props.group].timeout) {
|
||||
clearTimeout(portalNodes[this.props.group].timeout);
|
||||
}
|
||||
|
||||
if (this.props.active && !props.active) {
|
||||
newProps.active = true;
|
||||
portalNodes[this.props.group].timeout = setTimeout(() => {
|
||||
props.active = false;
|
||||
this.renderPortal(props);
|
||||
}, this.props.tooltipTimeout);
|
||||
}
|
||||
|
||||
this.renderPortal(newProps);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (portalNodes[this.props.group]) {
|
||||
// Todo: move this to root.unmount
|
||||
ReactDOM.unmountComponentAtNode(portalNodes[this.props.group].node);
|
||||
clearTimeout(portalNodes[this.props.group].timeout);
|
||||
|
||||
try {
|
||||
document.body.removeChild(portalNodes[this.props.group].node);
|
||||
} catch (e) {}
|
||||
|
||||
portalNodes[this.props.group] = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
385
platform/ui/src/components/Tooltip/PortalTooltipCard.tsx
Normal file
385
platform/ui/src/components/Tooltip/PortalTooltipCard.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FG_SIZE = 8;
|
||||
const BG_SIZE = 9;
|
||||
|
||||
/**
|
||||
* A portal based tooltip card component.
|
||||
*
|
||||
* This component has been repurposed and modified
|
||||
* for OHIF usage: https://github.com/romainberger/react-portal-tooltip
|
||||
*/
|
||||
export default class PortalTooltipCard extends Component {
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||
arrow: PropTypes.oneOf([null, 'center', 'top', 'right', 'bottom', 'left']),
|
||||
align: PropTypes.oneOf([null, 'center', 'right', 'left']),
|
||||
style: PropTypes.object,
|
||||
useHover: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
active: false,
|
||||
position: 'right',
|
||||
arrow: null,
|
||||
align: null,
|
||||
style: { style: {}, arrowStyle: {} },
|
||||
useHover: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
hover: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
offscreenDifference = 0;
|
||||
|
||||
margin = 15;
|
||||
|
||||
defaultArrowStyle = {
|
||||
color: '#090c29', // primary-dark
|
||||
borderColor: 'rgba(58, 63, 153, 1)', // secondary-light
|
||||
};
|
||||
|
||||
rootRef = React.createRef();
|
||||
|
||||
getGlobalStyle() {
|
||||
if (!this.props.parentEl) {
|
||||
return { display: 'none' };
|
||||
}
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
//padding: '5px',
|
||||
background: 'bg-primary-dark',
|
||||
//boxShadow: '0 0 4px rgba(0,0,0,.3)',
|
||||
borderRadius: '3px',
|
||||
//opacity: this.state.hover || this.props.active ? 1 : 0,
|
||||
visibility: this.state.hover || this.props.active ? 'visible' : 'hidden',
|
||||
zIndex: 50,
|
||||
...this.getStyle(this.props.position, this.props.arrow),
|
||||
};
|
||||
|
||||
return this.mergeStyle(style, this.props.style.style);
|
||||
}
|
||||
|
||||
getBaseArrowStyle() {
|
||||
return {
|
||||
position: 'absolute',
|
||||
content: '""',
|
||||
};
|
||||
}
|
||||
|
||||
getArrowStyle() {
|
||||
let fgStyle = this.getBaseArrowStyle();
|
||||
let bgStyle = this.getBaseArrowStyle();
|
||||
fgStyle.zIndex = 60;
|
||||
bgStyle.zIndex = 55;
|
||||
|
||||
let arrowStyle = {
|
||||
...this.defaultArrowStyle,
|
||||
...this.props.style.arrowStyle,
|
||||
};
|
||||
let bgBorderColor = arrowStyle.borderColor ? arrowStyle.borderColor : 'transparent';
|
||||
|
||||
let fgColorBorder = `10px solid ${arrowStyle.color}`;
|
||||
let fgTransBorder = `${FG_SIZE}px solid transparent`;
|
||||
let bgColorBorder = `12px solid ${bgBorderColor}`;
|
||||
let bgTransBorder = `${BG_SIZE}px solid transparent`;
|
||||
|
||||
let { position, arrow } = this.props;
|
||||
|
||||
if (position === 'left' || position === 'right') {
|
||||
fgStyle.top = '50%';
|
||||
fgStyle.borderTop = fgTransBorder;
|
||||
fgStyle.borderBottom = fgTransBorder;
|
||||
fgStyle.marginTop = -7;
|
||||
|
||||
bgStyle.borderTop = bgTransBorder;
|
||||
bgStyle.borderBottom = bgTransBorder;
|
||||
bgStyle.top = '50%';
|
||||
bgStyle.marginTop = -8;
|
||||
|
||||
if (position === 'left') {
|
||||
fgStyle.right = -10;
|
||||
fgStyle.borderLeft = fgColorBorder;
|
||||
bgStyle.right = -11;
|
||||
bgStyle.borderLeft = bgColorBorder;
|
||||
} else {
|
||||
fgStyle.left = -9;
|
||||
fgStyle.borderRight = fgColorBorder;
|
||||
bgStyle.left = -11;
|
||||
bgStyle.borderRight = bgColorBorder;
|
||||
}
|
||||
|
||||
if (arrow === 'top') {
|
||||
fgStyle.top = this.margin;
|
||||
bgStyle.top = this.margin;
|
||||
}
|
||||
if (arrow === 'bottom') {
|
||||
fgStyle.top = null;
|
||||
fgStyle.bottom = this.margin - 7;
|
||||
bgStyle.top = null;
|
||||
bgStyle.bottom = this.margin - 8;
|
||||
}
|
||||
} else {
|
||||
fgStyle.left = Math.round(this.state.width / 2 - FG_SIZE);
|
||||
fgStyle.borderLeft = fgTransBorder;
|
||||
fgStyle.borderRight = fgTransBorder;
|
||||
fgStyle.marginLeft = 0;
|
||||
bgStyle.left = fgStyle.left - 1;
|
||||
bgStyle.borderLeft = bgTransBorder;
|
||||
bgStyle.borderRight = bgTransBorder;
|
||||
bgStyle.marginLeft = 0;
|
||||
|
||||
if (position === 'top') {
|
||||
fgStyle.bottom = -10;
|
||||
fgStyle.borderTop = fgColorBorder;
|
||||
bgStyle.bottom = -11;
|
||||
bgStyle.borderTop = bgColorBorder;
|
||||
} else {
|
||||
fgStyle.top = -10;
|
||||
fgStyle.borderBottom = fgColorBorder;
|
||||
bgStyle.top = -11;
|
||||
bgStyle.borderBottom = bgColorBorder;
|
||||
}
|
||||
|
||||
if (arrow === 'right') {
|
||||
fgStyle.left = null;
|
||||
fgStyle.right = this.margin + 1 - FG_SIZE;
|
||||
bgStyle.left = null;
|
||||
bgStyle.right = this.margin - FG_SIZE;
|
||||
}
|
||||
if (arrow === 'left') {
|
||||
fgStyle.left = this.margin + 1 - FG_SIZE;
|
||||
bgStyle.left = this.margin - FG_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
let { color, borderColor, ...propsArrowStyle } = this.props.style.arrowStyle;
|
||||
|
||||
const state = {
|
||||
fgStyle: this.mergeStyle(fgStyle, propsArrowStyle),
|
||||
bgStyle: this.mergeStyle(bgStyle, propsArrowStyle),
|
||||
};
|
||||
|
||||
if (this.offscreenDifference > 0) {
|
||||
if (state.fgStyle.top >= 0 || state.fgStyle.top < 0) {
|
||||
state.fgStyle.top += this.offscreenDifference;
|
||||
}
|
||||
if (state.bgStyle.top >= 0 || state.bgStyle.top < 0) {
|
||||
state.bgStyle.top += this.offscreenDifference;
|
||||
}
|
||||
if (typeof state.fgStyle.top === 'string') {
|
||||
state.fgStyle.top = `calc(${state.fgStyle.top} + ${this.offscreenDifference}px)`;
|
||||
}
|
||||
if (typeof state.bgStyle.top === 'string') {
|
||||
state.bgStyle.top = `calc(${state.bgStyle.top} + ${this.offscreenDifference}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
mergeStyle(style, theme) {
|
||||
if (theme) {
|
||||
let { position, top, left, right, bottom, marginLeft, marginRight, ...validTheme } = theme;
|
||||
|
||||
return {
|
||||
...style,
|
||||
...validTheme,
|
||||
};
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
getStyle(position, arrow) {
|
||||
let alignOffset = 0;
|
||||
let parent = this.props.parentEl;
|
||||
let align = this.props.align;
|
||||
let tooltipPosition = parent.getBoundingClientRect();
|
||||
let scrollY = window.scrollY !== undefined ? window.scrollY : window.pageYOffset;
|
||||
let scrollX = window.scrollX !== undefined ? window.scrollX : window.pageXOffset;
|
||||
let top = scrollY + tooltipPosition.top;
|
||||
let left = scrollX + tooltipPosition.left;
|
||||
let style = {};
|
||||
|
||||
if (this.rootRef.current) {
|
||||
const newHeight = this.rootRef.current.offsetHeight / 2;
|
||||
const bottomPosition = tooltipPosition.bottom + newHeight;
|
||||
const isOffscreen = tooltipPosition.bottom + newHeight > window.innerHeight;
|
||||
const offscreenDifference = bottomPosition - window.innerHeight;
|
||||
if (isOffscreen) {
|
||||
const padding = 3;
|
||||
top -= offscreenDifference;
|
||||
this.offscreenDifference = Math.min(
|
||||
Math.max(offscreenDifference, 0),
|
||||
newHeight - parent.getBoundingClientRect().height / 2 - padding
|
||||
);
|
||||
} else {
|
||||
this.offscreenDifference = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const parentSize = {
|
||||
width: parent.offsetWidth,
|
||||
height: parent.offsetHeight,
|
||||
};
|
||||
|
||||
// fix for svg
|
||||
if (!parent.offsetHeight && parent.getBoundingClientRect) {
|
||||
parentSize.width = parent.getBoundingClientRect().width;
|
||||
parentSize.height = parent.getBoundingClientRect().height;
|
||||
}
|
||||
|
||||
if (align === 'left') {
|
||||
alignOffset = -parentSize.width / 2 + FG_SIZE;
|
||||
} else if (align === 'right') {
|
||||
alignOffset = parentSize.width / 2 - FG_SIZE;
|
||||
}
|
||||
|
||||
const stylesFromPosition = {
|
||||
left: () => {
|
||||
style.top = top + parentSize.height / 2 - this.state.height / 2;
|
||||
style.left = left - this.state.width - this.margin;
|
||||
},
|
||||
right: () => {
|
||||
style.top = top + parentSize.height / 2 - this.state.height / 2;
|
||||
style.left = left + parentSize.width + this.margin;
|
||||
},
|
||||
top: () => {
|
||||
style.left = left - this.state.width / 2 + parentSize.width / 2 + alignOffset;
|
||||
style.top = top - this.state.height - this.margin;
|
||||
},
|
||||
bottom: () => {
|
||||
style.left = left - this.state.width / 2 + parentSize.width / 2 + alignOffset;
|
||||
style.top = top + parentSize.height + this.margin;
|
||||
},
|
||||
};
|
||||
|
||||
const stylesFromArrow = {
|
||||
left: () => {
|
||||
style.left = left + parentSize.width / 2 - this.margin + alignOffset;
|
||||
},
|
||||
right: () => {
|
||||
style.left = left - this.state.width + parentSize.width / 2 + this.margin + alignOffset;
|
||||
},
|
||||
top: () => {
|
||||
style.top = top + parentSize.height / 2 - this.margin;
|
||||
},
|
||||
bottom: () => {
|
||||
style.top = top + parentSize.height / 2 - this.state.height + this.margin;
|
||||
},
|
||||
};
|
||||
|
||||
executeFunctionIfExist(stylesFromPosition, position);
|
||||
executeFunctionIfExist(stylesFromArrow, arrow);
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
checkWindowPosition(style, arrowStyle) {
|
||||
if (this.props.position === 'top' || this.props.position === 'bottom') {
|
||||
if (style.left < 0) {
|
||||
const parent = this.props.parentEl;
|
||||
if (parent) {
|
||||
const tooltipWidth = this.state.width;
|
||||
let bgStyleRight = arrowStyle.bgStyle.right;
|
||||
// For arrow = center
|
||||
if (!bgStyleRight) {
|
||||
bgStyleRight = tooltipWidth / 2 - BG_SIZE;
|
||||
}
|
||||
const newBgRight = Math.round(bgStyleRight - style.left + this.margin);
|
||||
arrowStyle = {
|
||||
...arrowStyle,
|
||||
bgStyle: {
|
||||
...arrowStyle.bgStyle,
|
||||
right: newBgRight,
|
||||
left: null,
|
||||
},
|
||||
fgStyle: {
|
||||
...arrowStyle.fgStyle,
|
||||
right: newBgRight + 1,
|
||||
left: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
style.left = this.margin;
|
||||
} else {
|
||||
let rightOffset = style.left + this.state.width - window.innerWidth;
|
||||
if (rightOffset > 0) {
|
||||
let originalLeft = style.left;
|
||||
style.left = window.innerWidth - this.state.width - this.margin;
|
||||
arrowStyle.fgStyle.marginLeft += originalLeft - style.left;
|
||||
arrowStyle.bgStyle.marginLeft += originalLeft - style.left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { style, arrowStyle };
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.props.active && this.props.useHover && this.setState({ hover: true });
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ hover: false });
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.updateSize();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props !== prevProps) {
|
||||
this.updateSize();
|
||||
}
|
||||
}
|
||||
|
||||
updateSize() {
|
||||
const newWidth = this.rootRef.current.offsetWidth;
|
||||
const newHeight = this.rootRef.current.offsetHeight;
|
||||
|
||||
if (newWidth !== this.state.width || newHeight !== this.state.height) {
|
||||
this.setState({
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let { style, arrowStyle } = this.checkWindowPosition(
|
||||
this.getGlobalStyle(),
|
||||
this.getArrowStyle()
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
ref={this.rootRef}
|
||||
>
|
||||
{this.props.arrow ? (
|
||||
<div>
|
||||
<span style={arrowStyle.fgStyle} />
|
||||
<span style={arrowStyle.bgStyle} />
|
||||
</div>
|
||||
) : null}
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const executeFunctionIfExist = (object, key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
||||
object[key]();
|
||||
}
|
||||
};
|
||||
206
platform/ui/src/components/Tooltip/Tooltip.tsx
Normal file
206
platform/ui/src/components/Tooltip/Tooltip.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
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;
|
||||
@@ -0,0 +1,91 @@
|
||||
import Tooltip from '../Tooltip';
|
||||
import Icon from '../../Icon';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: Tooltip,
|
||||
title: 'Components/Tooltip',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/Tooltip"
|
||||
component={Tooltip}
|
||||
/>
|
||||
|
||||
export const TooltipTemplate = args => (
|
||||
<div className="h-16 w-full">
|
||||
<div class="mx-auto h-8 w-8">
|
||||
<Tooltip {...args}>
|
||||
<Icon name="clipboard" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="Tooltip"
|
||||
componentRelativePath="Tooltip/Tooltip.js"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Usage](#usage)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
Tooltip is a component that renders the Tooltips.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
content: 'Tooltip',
|
||||
}}
|
||||
>
|
||||
{TooltipTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={Tooltip} />
|
||||
|
||||
## Usage
|
||||
|
||||
### Position
|
||||
|
||||
You can set the position of the tooltip by using the `position` prop.
|
||||
|
||||
<Canvas>
|
||||
<Story name="Position">
|
||||
<div className="m-8 h-16 w-full">
|
||||
<div class="mx-auto h-8 w-8">
|
||||
<Tooltip
|
||||
position="right"
|
||||
content="right"
|
||||
>
|
||||
<Icon name="clipboard" />
|
||||
<span>Right</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-8 h-16 w-full">
|
||||
<div class="mx-auto h-8 w-8">
|
||||
<Tooltip
|
||||
position="left"
|
||||
content="left"
|
||||
>
|
||||
<Icon name="clipboard" />
|
||||
<span>Left</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="Tooltip/__stories__/tooltip.stories.mdx" />
|
||||
2
platform/ui/src/components/Tooltip/index.js
Normal file
2
platform/ui/src/components/Tooltip/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Tooltip from './Tooltip';
|
||||
export default Tooltip;
|
||||
99
platform/ui/src/components/Tooltip/tooltip.css
Normal file
99
platform/ui/src/components/Tooltip/tooltip.css
Normal file
@@ -0,0 +1,99 @@
|
||||
.tooltip {
|
||||
@apply absolute z-20;
|
||||
}
|
||||
|
||||
/* TOOLTIP WORKAROUND FOR ARROW UP */
|
||||
.tooltip.tooltip-bottom .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
content: '';
|
||||
width: 14px;
|
||||
height: 1px;
|
||||
top: -1px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-top .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
content: '';
|
||||
width: 14px;
|
||||
height: 1px;
|
||||
bottom: -1px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom {
|
||||
@apply mt-1 pt-2;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-top {
|
||||
@apply mb-1 pb-2;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-left {
|
||||
@apply mt-1 pt-2;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-right {
|
||||
@apply mt-1 pt-2;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tooltip.tooltip-left {
|
||||
@apply mr-4;
|
||||
top: 50%;
|
||||
right: calc(100%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-right {
|
||||
@apply ml-4;
|
||||
top: 50%;
|
||||
left: calc(100%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-right .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 15px;
|
||||
left: -1px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-left .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 15px;
|
||||
right: -1px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-right .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 2px;
|
||||
right: 5px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-left .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 2px;
|
||||
left: 5px;
|
||||
top: -1px;
|
||||
}
|
||||
Reference in New Issue
Block a user