@nomios/web-uikit
Version:
Nomios' living web UIKit
218 lines (193 loc) • 6.96 kB
JavaScript
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';
import { Popper } from 'react-popper';
import { CSSTransition } from 'react-transition-group';
import classNames from 'classnames';
import getModifiers from './modifiers';
import styles from './Tooltip.css';
const CLOSE_TRANSITION_TIMEOUT = 250; // Must be 50ms higher than the actual CSS duration
class Tooltip extends Component {
constructor(...args) {
super(...args);
_defineProperty(this, "boxRef", createRef());
_defineProperty(this, "renderPopper", ({
ref,
style,
placement,
arrowProps,
scheduleUpdate
}) => {
// Fix arrow not being positioned correctly in first render or when the placement changes
// See: https://github.com/FezVrasta/react-popper/issues/88
if (!this.placement || placement !== this.placement) {
cancelAnimationFrame(this.scheduleUpdateRequestId);
this.scheduleUpdateRequestId = requestAnimationFrame(() => scheduleUpdate());
}
this.placement = placement;
const {
open,
placement: _placement,
viewportPadding,
boundariesElement,
className,
contentClassName,
onRequestCancelClose,
onRequestClose,
shouldCloseOnEsc,
shouldCloseOnOutsideClick,
children,
variant,
onEntered,
onExited,
style: styleProp,
...rest
} = this.props;
return React.createElement("div", Object.assign({}, rest, {
ref: ref,
style: { ...style,
...styleProp
},
className: classNames(styles.tooltip, className),
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave,
"data-placement": placement
}), React.createElement("div", {
className: styles.container
}, React.createElement("div", {
ref: this.boxRef,
className: classNames(styles.tooltipContent, styles[variant], contentClassName)
}, children), React.createElement("div", {
ref: arrowProps.ref,
className: classNames(styles.arrow, styles[variant]),
style: arrowProps.style
})));
});
_defineProperty(this, "handleMouseEnter", () => {
this.props.onRequestCancelClose && this.props.onRequestCancelClose('mouseEnter');
});
_defineProperty(this, "handleMouseLeave", () => {
// If user is selecting text, skip any check!
if (!this.mouseDownEventTarget) {
this.props.onRequestClose && this.props.onRequestClose('mouseLeave');
}
});
_defineProperty(this, "handleKeyDown", event => {
// Handle escape
if (event.key === 'Escape') {
this.props.onRequestClose && this.props.onRequestClose('escapePress');
}
});
_defineProperty(this, "handleMouseDown", event => {
// Store the event target, with support for shadow dom
this.mouseDownEventTarget = event.composedPath ? event.composedPath()[0] : event.target;
});
_defineProperty(this, "handleMouseUp", event => {
// Use also mouse down event because user might be selecting text and accidently left the tooltip
const target = this.mouseDownEventTarget || event.target;
this.mouseDownEventTarget = undefined; // Check if we clicked outside of the tooltip AND outside the reference as well
const isOutsideBox = !this.boxRef.current.contains(target);
const isOutsideReference = !this.referenceNode || !this.referenceNode.contains(target);
if (isOutsideBox && isOutsideReference) {
this.props.onRequestClose && this.props.onRequestClose('clickOutside');
}
});
_defineProperty(this, "handleEntered", () => {
this.props.onEntered && this.props.onEntered();
});
_defineProperty(this, "handleExited", () => {
this.props.onExited && this.props.onExited();
});
}
componentDidMount() {
if (this.props.open) {
this.addEscapeOutsideListeners();
}
}
componentDidUpdate(prevProps) {
if (prevProps.open !== this.props.open) {
if (this.props.open) {
this.addEscapeOutsideListeners();
} else {
this.removeEscapeOutsideListeners();
}
}
}
componentWillUnmount() {
cancelAnimationFrame(this.scheduleUpdateRequestId);
this.removeEscapeOutsideListeners();
}
render() {
const {
placement,
open
} = this.props;
return React.createElement(CSSTransition, {
in: open,
mountOnEnter: true,
unmountOnExit: true,
onEntered: this.handleEntered,
onExited: this.handleExited,
timeout: CLOSE_TRANSITION_TIMEOUT,
classNames: {
enterActive: styles.enterActive,
enterDone: styles.enterDone,
exit: styles.exit
}
}, React.createElement(Popper, {
modifiers: getModifiers(this.props),
placement: placement
}, this.renderPopper));
}
setReferenceNode(node) {
this.referenceNode = node;
}
addEscapeOutsideListeners() {
const {
shouldCloseOnOutsideClick,
shouldCloseOnEsc
} = this.props;
if (shouldCloseOnOutsideClick) {
document.addEventListener('mousedown', this.handleMouseDown);
document.addEventListener('mouseup', this.handleMouseUp);
document.addEventListener('touchstart', this.handleMouseDown);
document.addEventListener('touchend', this.handleMouseUp);
}
if (shouldCloseOnEsc) {
document.body.addEventListener('keydown', this.handleKeyDown);
}
}
removeEscapeOutsideListeners() {
document.removeEventListener('mousedown', this.handleMouseDown);
document.removeEventListener('mouseup', this.handleMouseUp);
document.removeEventListener('touchstart', this.handleMouseDown);
document.removeEventListener('touchend', this.handleMouseUp);
document.removeEventListener('keydown', this.handleKeyDown);
}
}
Tooltip.propTypes = {
variant: PropTypes.oneOf(['light', 'dark']),
placement: PropTypes.oneOf(['auto', 'top', 'right', 'bottom', 'left']),
viewportPadding: PropTypes.number,
boundariesElement: PropTypes.string,
shouldCloseOnEsc: PropTypes.bool,
shouldCloseOnOutsideClick: PropTypes.bool,
className: PropTypes.string,
contentClassName: PropTypes.string,
children: PropTypes.node.isRequired,
onEntered: PropTypes.func,
onExited: PropTypes.func,
style: PropTypes.object,
// The properties below are "private"
open: PropTypes.bool,
onRequestCancelClose: PropTypes.func,
onRequestClose: PropTypes.func
};
Tooltip.defaultProps = {
placement: 'auto',
viewportPadding: 10,
shouldCloseOnEsc: true,
shouldCloseOnOutsideClick: true,
variant: 'light'
};
export default Tooltip;