ndla-ui
Version:
UI component library for NDLA.
184 lines (168 loc) • 5.5 kB
JSX
/**
* Copyright (c) 2016-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import BEMHelper from 'react-bem-helper';
import { isMobile, isIE } from 'react-device-detect';
import { Fade } from '../Animation';
const classes = new BEMHelper({
name: 'tooltip',
prefix: 'c-',
});
class Tooltip extends Component {
constructor(props) {
super(props);
this.state = {
showTooltip: false,
};
this.handleShowTooltip = this.handleShowTooltip.bind(this);
this.handleHideTooltip = this.handleHideTooltip.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.contentRef = React.createRef();
this.tooltipRef = React.createRef();
this.widthRef = 0;
this.heightRef = 0;
this.leftRef = 0;
this.tooltipRefWidth = props.tooltip ? props.tooltip.length * 5 : 0; // Estimate incase user only uses keyboard navigation.
this.currentStyles = {};
}
getPosition() {
if (this.state.showTooltip) {
this.currentStyles = {};
this.widthRef = this.contentRef.current.offsetWidth;
this.heightRef = this.contentRef.current.offsetHeight;
const elementRect = this.contentRef.current.getBoundingClientRect();
this.leftRef = elementRect.left;
const tooltipWidth = this.tooltipRef.current
? this.tooltipRef.current.offsetWidth
: this.tooltipRefWidth;
this.tooltipRefWidth = tooltipWidth;
if (isIE) {
// IE is bad with transform % + px..
this.currentStyles.left = `-${(this.tooltipRef.current.offsetWidth -
this.widthRef) /
2}px`;
this.currentStyles.top = `-${this.tooltipRef.current.offsetHeight +
10}px`;
} else if (
this.props.align === 'top' ||
this.props.align === 'bottom' ||
(this.props.align === 'left' && this.leftRef - tooltipWidth < 20) ||
(this.props.align === 'right' &&
this.leftRef + this.widthRef + tooltipWidth > window.innerWidth - 40)
) {
const centeredLeft = this.leftRef + this.widthRef / 2;
let moveHorizontal = Math.max(
centeredLeft + tooltipWidth / 2 + 20 - window.innerWidth,
0,
);
if (moveHorizontal === 0) {
moveHorizontal = Math.min(-(tooltipWidth / 2 - centeredLeft + 20), 0);
}
if (this.props.align === 'bottom') {
this.currentStyles.transform = `translate(calc(-50% + ${this
.widthRef /
2 -
moveHorizontal}px), calc(${this.heightRef}px + 0.25rem))`;
} else {
this.currentStyles.transform = `translate(calc(-50% + ${this
.widthRef /
2 -
moveHorizontal}px), calc(-100% - 0.25rem))`;
}
} else if (this.props.align === 'left') {
this.currentStyles.transform = `translate(calc(-100% - 0.25rem), calc(-50% + ${this
.heightRef / 2}px))`;
} else {
this.currentStyles.transform = `translate(calc(${
this.widthRef
}px + 0.25rem), calc(-50% + ${this.heightRef / 2}px))`;
}
}
return this.currentStyles;
}
handleShowTooltip() {
this.setState({ showTooltip: !this.props.disabled });
}
handleHideTooltip() {
this.setState({ showTooltip: false });
}
handleKeyPress(e) {
if (e.key === 'Enter') {
try {
this.contentRef.current
.querySelectorAll('[type="button"], a')[0]
.click();
} catch (err) {
console.log('error', err); // eslint-disable-line no-console
}
}
}
render() {
// If phone ignore all tooltips //
if (isMobile) {
return (
<div
className={`${classes('').className} ${
this.props.tooltipContainerClass
}`}>
<span className={`c-tooltip__content ${this.props.className}`}>
{this.props.children}
</span>
</div>
);
}
return (
<div
className={`${classes('').className} ${
this.props.tooltipContainerClass
}`}>
<Fade in={this.state.showTooltip} delay={this.props.delay}>
<span
role="tooltip"
{...classes('tooltip')}
style={this.getPosition()}
ref={this.tooltipRef}>
{this.props.tooltip}
</span>
</Fade>
<span
role="button"
tabIndex={0}
aria-label={this.props.tooltip}
ref={this.contentRef}
onMouseEnter={this.handleShowTooltip}
onMouseOut={this.handleHideTooltip}
onMouseMove={this.handleShowTooltip}
onFocus={this.handleShowTooltip}
onKeyPress={this.handleKeyPress}
onBlur={this.handleHideTooltip}
className={`c-tooltip__content ${this.props.className}`}>
{this.props.children}
</span>
</div>
);
}
}
Tooltip.propTypes = {
children: PropTypes.node.isRequired,
tooltip: PropTypes.string.isRequired,
delay: PropTypes.number,
disabled: PropTypes.bool,
align: PropTypes.oneOf(['left', 'right', 'top', 'bottom']),
className: PropTypes.string,
tooltipContainerClass: PropTypes.string,
};
Tooltip.defaultProps = {
align: 'top',
disabled: false,
delay: 0,
className: '',
tooltipContainerClass: '',
};
export default Tooltip;