@zohodesk/components
Version:
Dot UI is a customizable React component library built to deliver a clean, accessible, and developer-friendly UI experience. It offers a growing set of reusable components designed to align with modern design systems and streamline application development
530 lines (472 loc) • 22.3 kB
JavaScript
import React from 'react';
import { defaultProps } from "./props/defaultProps";
import { propTypes } from "./props/propTypes";
import { getLibraryConfig } from "../Provider/Config";
import ResizeObserver from "../Responsive/ResizeObserver";
import selectn from 'selectn';
import { whiteSpaceClassMapping } from "../utils/cssUtils";
import style from "./Tooltip.module.css";
export default class Tooltip extends React.Component {
constructor(props) {
super(props);
this.state = {
title: null,
left: 0,
top: 0,
arrowLeft: 0,
isArrowRight: null,
isHtml: false
};
this.handleOver = this.handleOver.bind(this);
this.getToolTipDOM = this.getToolTipDOM.bind(this);
this.reset = this.reset.bind(this);
this.getDirection = document.getElementsByTagName('html')[0];
this.leftRightScreenEdge = this.leftRightScreenEdge.bind(this);
this.topBottomScreenEdge = this.topBottomScreenEdge.bind(this);
this.observer = new ResizeObserver(this.onResize);
this.isResized = true;
this.tooltipContainerClientRect = {};
this.tooltipContainerEl = {};
}
onResize(sizeOfObservedEl, observedEl) {
this.isResized = true;
}
observeElement() {
this.tooltipContainerEl = this.getToolTipContainerEl();
this.observer.observe(this.tooltipContainerEl);
}
unObserveElement() {
this.observer.disconnect();
}
getClientRectOfContEl(el) {
if (this.isResized) {
return this.setClientRectOfContEl(el);
}
return this.tooltipContainerClientRect;
}
setClientRectOfContEl(containerEl) {
this.isResized = false;
this.tooltipContainerClientRect = containerEl.getBoundingClientRect();
return this.tooltipContainerClientRect;
}
getToolTipContainerEl() {
const getTooltipContainer = getLibraryConfig('getTooltipContainer');
const tooltipContainer = typeof getTooltipContainer === 'function' ? getTooltipContainer() : null;
return tooltipContainer ? tooltipContainer : document.body;
}
getToolTipDOM(el) {
this.toolTip = el;
}
reset() {
let {
title
} = this.state;
if (title !== null) {
this.setState({
title: null,
top: 0,
left: 0
});
}
}
/* left and right screen edge check for top and bottom tooltip position */
leftRightScreenEdge(tLeft, toolTipArrowLeft, thisLeft, thisWidth, tooltipoffsetWidth, bodyWidth, rightEdge, bodyLeft) {
if (tLeft - bodyLeft <= 1) {
// top & bottom position left side screen edge case
tLeft = bodyLeft + 2;
toolTipArrowLeft = parseInt(thisLeft - bodyLeft + thisWidth / 2 - 6);
} else if (tLeft - bodyLeft + tooltipoffsetWidth > bodyWidth) {
// top & bottom position right side screen edge case
rightEdge = tLeft + tooltipoffsetWidth - bodyWidth + 2;
tLeft = tLeft + bodyLeft - rightEdge;
toolTipArrowLeft = toolTipArrowLeft - bodyLeft + rightEdge;
} else if (tLeft + tooltipoffsetWidth + tooltipoffsetWidth >= bodyWidth - 2) {
// top & bottom center screen tooltip collide with right edge screen
tLeft = parseInt(thisLeft - (tooltipoffsetWidth - thisWidth) / 2);
toolTipArrowLeft = parseInt(tooltipoffsetWidth / 2 - 4);
}
return {
tLeft,
toolTipArrowLeft,
thisLeft,
thisWidth,
tooltipoffsetWidth,
bodyWidth,
rightEdge
};
}
/* top and bottom screen edge check for left and right tooltip position */
topBottomScreenEdge(tTop, toolTipArrowTop, thisTop, thisHeight, tooltipoffsetHeight, bodyHeight, bottomEdge) {
if (tTop <= 1) {
tTop = 2;
toolTipArrowTop = parseInt(thisTop + thisHeight / 2);
} else if (tTop + tooltipoffsetHeight > bodyHeight) {
bottomEdge = tTop + tooltipoffsetHeight - bodyHeight + 2;
tTop -= bottomEdge;
toolTipArrowTop += bottomEdge;
}
return {
tTop,
toolTipArrowTop,
thisTop,
thisHeight,
tooltipoffsetHeight,
bodyHeight,
bottomEdge
};
}
handleOver(e, targetElement) {
let containerElement = this.tooltipContainerEl;
let element = e.target;
let titleDiv = element.hasAttribute('data-title') || element.hasAttribute('title') ? element : element.closest('[data-title]') || element.closest('[title]');
if (titleDiv) {
let title = titleDiv.getAttribute('data-title') || titleDiv.getAttribute('title');
/* if data-title-position is left or right change right and left in rtl case */
let isPosition = titleDiv.getAttribute('data-title-position');
let elem = this.getDirection;
if (elem.getAttribute('dir') == 'rtl') {
if (isPosition == 'left') {
isPosition = 'right';
} else if (isPosition == 'right') {
isPosition = 'left';
}
}
if (title !== '' && title) {
titleDiv.setAttribute('data-title', title);
titleDiv.removeAttribute('title');
let isInputElementType = element.type === 'text';
let elementText = isInputElementType ? element.value : element.innerText;
if (element.nodeName !== 'I' && elementText && elementText.trim() !== '') {
let isContentDotted = '';
if (element.scrollWidth !== 0) {
isContentDotted = element.offsetWidth < element.scrollWidth;
if (!isContentDotted) {
isContentDotted = element.offsetHeight < element.scrollHeight;
} // if (!isContentDotted) {Need to check the code Sivanesh
// isContentDotted = element.offsetHeight < element.scrollHeight;
// }
} else {
const offWidth = selectn('parentElement.offsetWidth', element) || 0;
const scrollWidth = selectn('parentElement.scrollWidth', element) || 0;
isContentDotted = offWidth < scrollWidth;
}
let originText = elementText.replace(/\s/g, '').toLowerCase();
let tooltipText = title.replace(/\s/g, '').toLowerCase();
let isDefaultTooltip = element.hasAttribute('data-istitle') || element.hasAttribute('istitle') ? element.getAttribute('data-istitle') || element.getAttribute('istitle') : 'false';
isDefaultTooltip = isDefaultTooltip === 'true';
let isSameText = originText.indexOf(tooltipText) !== -1 ? true : false;
if (!isContentDotted && isSameText && !isDefaultTooltip) {
return false;
}
if (isContentDotted && titleDiv.getAttribute('data-dottedTitle')) {
title = titleDiv.getAttribute('data-dottedTitle');
}
}
let isHtml = titleDiv.getAttribute('data-ishtml');
let dataTooltipnoArrow = titleDiv.getAttribute('data-tooltip-noarrow') === 'true' ? true : false;
let dataTooltipWrap = titleDiv.getAttribute('data-title-wrap') || 'normal'; // values: normal, pre, pre-wrap, pre-line, nowrap, break-spaces
let clientRect = titleDiv.getBoundingClientRect();
let boxLayout = this.getClientRectOfContEl(containerElement);
this.setState({
title,
isHtml,
dataTooltipnoArrow,
dataTooltipWrap
}, () => {
let tooltip = this.toolTip;
if (tooltip) {
/* element top, left, height, width */
let thisTop = clientRect.top;
let thisLeft = clientRect.left;
let thisHeight = clientRect.height;
let thisWidth = clientRect.width;
/* box layout left spacing */
let bodyLeft = boxLayout.left; // let checkTop = thisTop + thisHeight;
/* element left plus element width */
let checkLeft = thisLeft + thisWidth;
let tTop;
let tLeft;
let toolTipArrowTop;
let toolTipArrowLeft;
let rightEdge;
let bottomEdge;
let tooltipLeft;
/* offset width, height of body */
let bodyWidth = containerElement.offsetWidth;
let bodyHeight = containerElement.offsetHeight;
let isArrowHorizontal = false;
let isArrowDown = false;
let isArrowRight = false;
let tWidth = '';
/* overall body height minus element top + element height */
let thisBottom = bodyHeight - (thisTop + thisHeight);
/* overall body width minus element left + element width */
let thisRight = bodyWidth - (thisLeft + thisWidth);
/* tooltip width and height */
let tooltipoffsetWidth = tooltip.offsetWidth;
let tooltipoffsetHeight = tooltip.offsetHeight;
if (isPosition) {
if (isPosition == 'top') {
/* if top does not have enough space show tooltip in bottom area */
if (thisTop <= tooltipoffsetHeight + 5) {
/* if top space is larger than bottom space show tooltip in top area */
if (thisTop > thisBottom) {
tTop = parseInt(thisTop - (tooltipoffsetHeight + 10));
tLeft = parseInt(thisLeft - (tooltipoffsetWidth - thisWidth) / 2 + 2);
toolTipArrowTop = parseInt(tooltipoffsetHeight - 4);
toolTipArrowLeft = parseInt(tooltipoffsetWidth / 2 - 6);
isArrowDown = true;
} else {
/* bottom position */
tTop = parseInt(thisTop + (thisHeight + 10));
tLeft = parseInt(thisLeft - (tooltipoffsetWidth - thisWidth) / 2 + 2);
toolTipArrowTop = -4;
toolTipArrowLeft = parseInt(tooltipoffsetWidth / 2 - 6);
isArrowDown = false;
}
} else {
/* if top have enough space show tooltip in top area */
tTop = parseInt(thisTop - (tooltipoffsetHeight + 10));
tLeft = parseInt(thisLeft - (tooltipoffsetWidth - thisWidth) / 2 + 2);
toolTipArrowTop = parseInt(tooltipoffsetHeight - 4);
toolTipArrowLeft = parseInt(tooltipoffsetWidth / 2 - 6);
isArrowDown = true;
}
/* top tooltip left and right corner edge case */
let values = this.leftRightScreenEdge(tLeft, toolTipArrowLeft, thisLeft, thisWidth, tooltipoffsetWidth, bodyWidth, rightEdge, bodyLeft);
tLeft = values.tLeft, toolTipArrowLeft = values.toolTipArrowLeft, thisLeft = values.thisLeft, thisWidth = values.thisWidth, tooltipoffsetWidth = values.tooltipoffsetWidth, bodyWidth = values.bodyWidth, rightEdge = values.rightEdge;
} else if (isPosition == 'bottom') {
/* if bottom does not have enough space show tooltip in top area */
if (thisBottom <= tooltipoffsetHeight + 5) {
/* if bottom space is larger than top space show tooltip in bottom area */
if (thisTop > thisBottom) {
tTop = parseInt(thisTop - (tooltipoffsetHeight + 10));
tLeft = parseInt(thisLeft - (tooltipoffsetWidth - thisWidth) / 2 + 2);
toolTipArrowTop = parseInt(tooltipoffsetHeight - 4);
toolTipArrowLeft = parseInt(tooltipoffsetWidth / 2 - 6);
isArrowDown = true;
} else {
/* top position */
tTop = parseInt(thisTop + (thisHeight + 10));
tLeft = parseInt(thisLeft - (tooltipoffsetWidth - thisWidth) / 2 + 2);
toolTipArrowTop = -4;
toolTipArrowLeft = parseInt(tooltipoffsetWidth / 2 - 6);
isArrowDown = false;
}
} else {
/* if bottom have enough space show tooltip in bottom area */
tTop = parseInt(thisTop + (thisHeight + 10));
tLeft = parseInt(thisLeft - (tooltipoffsetWidth - thisWidth) / 2 + 2);
toolTipArrowTop = -4;
toolTipArrowLeft = parseInt(tooltipoffsetWidth / 2 - 6);
isArrowDown = false;
}
/* bottom tooltip left and right corner edge case */
let values = this.leftRightScreenEdge(tLeft, toolTipArrowLeft, thisLeft, thisWidth, tooltipoffsetWidth, bodyWidth, rightEdge, bodyLeft);
tLeft = values.tLeft, toolTipArrowLeft = values.toolTipArrowLeft, thisLeft = values.thisLeft, thisWidth = values.thisWidth, tooltipoffsetWidth = values.tooltipoffsetWidth, bodyWidth = values.bodyWidth, rightEdge = values.rightEdge;
} else if (isPosition == 'left') {
isArrowHorizontal = true;
if (thisLeft - bodyLeft <= tooltipoffsetWidth) {
if (thisLeft - bodyLeft <= thisRight) {
/* if left does not have enough space show tooltip in right area */
tTop = parseInt(thisTop + (thisHeight / 2 - tooltipoffsetHeight / 2));
tLeft = parseInt(thisLeft + thisWidth + 10);
toolTipArrowTop = parseInt(tooltipoffsetHeight / 2 - 1);
toolTipArrowLeft = -1;
isArrowRight = false;
} else {
/* if left space is larger than right space show tooltip in left (default) area */
if (bodyLeft + tooltipoffsetWidth >= thisLeft) {
/* if tooltip width is greater than left space, set left space width to tooltip */
tWidth = thisLeft;
tTop = parseInt(thisTop + (thisHeight / 2 - tooltipoffsetHeight / 2));
tLeft = parseInt(thisLeft - tWidth - 7);
toolTipArrowTop = parseInt(tooltipoffsetHeight / 2 - 1);
toolTipArrowLeft = parseInt(tWidth - 2);
isArrowRight = true;
}
}
} else {
/* if left have enough space show tooltip in left area */
tTop = parseInt(thisTop + (thisHeight / 2 - tooltipoffsetHeight / 2));
tLeft = parseInt(thisLeft - tooltipoffsetWidth - 7);
toolTipArrowTop = parseInt(tooltipoffsetHeight / 2 - 1);
toolTipArrowLeft = parseInt(tooltipoffsetWidth - 2);
isArrowRight = true;
}
/* left tooltip top and bottom corner edge case */
let values = this.topBottomScreenEdge(tTop, toolTipArrowTop, thisTop, thisHeight, tooltipoffsetHeight, bodyHeight, bottomEdge);
tTop = values.tTop, toolTipArrowTop = values.toolTipArrowTop, thisTop = values.thisTop, thisHeight = values.thisHeight, tooltipoffsetHeight = values.tooltipoffsetHeight, bodyHeight = values.bodyHeight, bottomEdge = values.bottomEdge;
} else if (isPosition == 'right') {
isArrowHorizontal = true;
if (thisRight + bodyLeft <= tooltipoffsetWidth) {
if (thisLeft + bodyLeft <= thisRight) {
/* if right space is larger than left space show tooltip in right (default) area */
tTop = parseInt(thisTop + (thisHeight / 2 - tooltipoffsetHeight / 2));
tLeft = parseInt(thisLeft + thisWidth + 7);
toolTipArrowTop = parseInt(tooltipoffsetHeight / 2 - 1);
toolTipArrowLeft = -1;
isArrowRight = false;
} else {
/* if right does not have enough space show tooltip in left area */
if (tooltipoffsetWidth >= thisLeft) {
/* if tooltip width is greater than left space, set left space width to tooltip */
tWidth = thisLeft;
tTop = parseInt(thisTop + (thisHeight / 2 - tooltipoffsetHeight / 2));
tLeft = parseInt(thisLeft - tWidth - 7);
toolTipArrowTop = parseInt(tooltipoffsetHeight / 2 - 1);
toolTipArrowLeft = parseInt(tWidth - 2);
isArrowRight = true;
} else {
/* show tooltip in left area without setting left space width to tooltip */
tTop = parseInt(thisTop + (thisHeight / 2 - tooltipoffsetHeight / 2));
tLeft = parseInt(thisLeft - tooltipoffsetWidth - 7);
toolTipArrowTop = parseInt(tooltipoffsetHeight / 2 - 1);
toolTipArrowLeft = parseInt(tooltipoffsetWidth - 2);
isArrowRight = true;
}
}
} else {
/* if right have enough space show tooltip in right area */
tTop = parseInt(thisTop + (thisHeight / 2 - tooltipoffsetHeight / 2));
tLeft = parseInt(thisLeft + thisWidth + 7);
toolTipArrowTop = parseInt(tooltipoffsetHeight / 2 - 1);
toolTipArrowLeft = -1;
isArrowRight = false;
}
/* right tooltip left and right corner edge case */
let values = this.topBottomScreenEdge(tTop, toolTipArrowTop, thisTop, thisHeight, tooltipoffsetHeight, bodyHeight, bottomEdge);
tTop = values.tTop, toolTipArrowTop = values.toolTipArrowTop, thisTop = values.thisTop, thisHeight = values.thisHeight, tooltipoffsetHeight = values.tooltipoffsetHeight, bodyHeight = values.bodyHeight, bottomEdge = values.bottomEdge;
}
this.setState({
top: tTop,
left: tLeft,
arrowTop: toolTipArrowTop,
arrowLeft: toolTipArrowLeft,
isArrowHorizontal: isArrowHorizontal,
isArrowDown: isArrowDown,
isArrowRight: isArrowRight,
width: tWidth
});
} else {
this.setState({
isArrowRight: null
});
tTop = parseInt(thisTop + thisHeight + 10);
tLeft = parseInt(thisLeft - (tooltipoffsetWidth / 2 - thisWidth / 2) + 2);
tooltipLeft = parseInt(tooltipoffsetWidth / 2 - 6);
if (tLeft - bodyLeft <= 1) {
// default left side screen edge case
tooltipLeft = parseInt(thisLeft - bodyLeft + thisWidth / 2 - 6);
tLeft = bodyLeft + 2;
} else if (tLeft - bodyLeft + tooltipoffsetWidth > bodyWidth) {
checkLeft = tLeft + tooltipoffsetWidth - bodyWidth + 2;
tooltipLeft = tooltipLeft - bodyLeft + checkLeft;
tLeft = tLeft + bodyLeft - checkLeft;
} else if (tLeft + tooltipoffsetWidth >= bodyWidth - 2) {
// default center screen tooltip collide with right edge screen
tLeft = parseInt(thisLeft - (tooltipoffsetWidth - thisWidth) / 2);
tooltipLeft = parseInt(tooltipoffsetWidth / 2 - 4);
}
isArrowDown = false;
if (tTop + tooltipoffsetHeight > bodyHeight) {
/* if bottom does not have enough space show tooltip in top area */
if (thisTop > thisBottom) {
isArrowDown = true;
tTop -= tooltipoffsetHeight + thisHeight + 20;
}
}
if (isArrowDown) {
let arrowTop = tooltipoffsetHeight - 4;
this.setState({
top: tTop,
left: tLeft,
arrowLeft: tooltipLeft,
arrowTop,
isArrowDown: true,
isArrowHorizontal: isArrowHorizontal
});
} else {
this.setState({
top: tTop,
left: tLeft,
arrowLeft: tooltipLeft,
arrowTop: -4,
isArrowDown: false,
isArrowHorizontal: isArrowHorizontal
});
}
}
}
});
titleDiv.addEventListener('click', this.reset);
titleDiv.addEventListener('mouseup', this.reset);
titleDiv.addEventListener('mouseleave', this.reset);
}
} else {
this.reset();
}
}
render() {
let {
title,
left,
top,
arrowLeft,
arrowTop,
isArrowDown,
isArrowRight,
isArrowHorizontal,
width,
isHtml,
dataTooltipnoArrow,
dataTooltipWrap
} = this.state;
let {
dataId,
customClass
} = this.props;
let arrowStyle = isArrowHorizontal ? isArrowRight ? style.arrowRight : style.arrowLeft : isArrowDown ? style.arrowDown : style.arrowUp;
title = title ? title.trim() : null;
let tooltipWhiteSpaceModification = dataTooltipWrap;
if (dataTooltipWrap == 'pre') {
tooltipWhiteSpaceModification = 'pre-wrap';
} else if (dataTooltipWrap == 'nowrap') {
tooltipWhiteSpaceModification = 'normal';
}
let tooltipCss = `${style.tooltipcont} ${whiteSpaceClassMapping[tooltipWhiteSpaceModification]}`;
return title ? /*#__PURE__*/React.createElement("div", {
className: `${style.tooltiptext} ${customClass}`,
style: {
left,
top,
width
},
ref: this.getToolTipDOM,
"data-id": dataId,
"data-test-id": dataId
}, !dataTooltipnoArrow ? /*#__PURE__*/React.createElement("span", {
className: `${style.tooltiparrow} ${arrowStyle}`,
style: {
left: arrowLeft,
top: arrowTop
}
}) : null, isHtml ? /*#__PURE__*/React.createElement("div", {
className: tooltipCss,
dangerouslySetInnerHTML: {
__html: title
}
}) : /*#__PURE__*/React.createElement("div", {
className: tooltipCss
}, title)) : null;
}
}
Tooltip.propTypes = propTypes;
Tooltip.defaultProps = defaultProps; // if (__DOCS__) {
// Tooltip.docs = {
// componentGroup: 'Atom',
// folderName: 'Style Guide',
// description: ' ',
// external: true
// };
// }