azure-devops-ui
Version:
React components for building web UI in Azure DevOps
196 lines (195 loc) • 10.5 kB
JavaScript
import "../../CommonImports";
import "../../Core/core.css";
import "./Tooltip.css";
import * as React from "react";
import { Callout } from '../../Callout';
import { FocusWithin } from '../../FocusWithin';
import { MouseWithin } from '../../MouseWithin';
import { css, getFocusVisible, getSafeId, KeyCode, Mouse } from '../../Util';
import { Location } from '../../Utilities/Position';
export var TooltipStatus;
(function (TooltipStatus) {
TooltipStatus[TooltipStatus["hidden"] = 0] = "hidden";
TooltipStatus[TooltipStatus["visible"] = 1] = "visible";
TooltipStatus[TooltipStatus["fadingout"] = 2] = "fadingout";
})(TooltipStatus || (TooltipStatus = {}));
let tooltipId = 1;
export class Tooltip extends React.Component {
constructor(props) {
super(props);
this.contentRef = React.createRef();
this.tooltipId = `tooltip-${tooltipId++}`;
this.focus = false;
this.mouse = false;
this.showTooltip = (event) => {
const anchorElement = event.currentTarget;
if (this.shouldShowTooltip(anchorElement)) {
// If no anchorOrigin was specified use the Mouse.position when we show the toolip.
let anchorPoint;
if (!this.props.anchorOrigin) {
anchorPoint = Mouse.position;
}
this.mouse = true;
document.addEventListener("keydown", this.onKeyDown);
this.setState({
anchorElement,
anchorOffset: { horizontal: 8, vertical: 8 },
anchorOrigin: { horizontal: Location.center, vertical: Location.end },
anchorPoint,
innerText: this.props.overflowOnly && !this.props.text ? anchorElement.innerText : undefined,
tooltipStatus: TooltipStatus.visible,
tooltipOrigin: { horizontal: Location.start, vertical: Location.start }
});
}
};
this.closeTooltip = () => {
if (!(this.focus && getFocusVisible()) && this.state.tooltipStatus === TooltipStatus.visible) {
this.mouse = false;
document.removeEventListener("keydown", this.onKeyDown);
this.setState({ tooltipStatus: this.getDismissStatus() });
}
};
this.onKeyDown = (event) => {
var _a;
if (event.which === KeyCode.escape && this.state.tooltipStatus === TooltipStatus.visible) {
this.closeTooltip();
}
if (event.which === KeyCode.ctrl && this.state.tooltipStatus === TooltipStatus.visible) {
const container = this.contentRef.current;
const selectionNode = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.anchorNode;
const hasSelectionInTooltip = container && selectionNode && container.contains(selectionNode);
// Ctrl keystroke anywhere will dismiss the callout as per MAS 1.4.13,
// except if user has selected something inside, in which case we allow Ctrl-C.
if (!hasSelectionInTooltip) {
this.closeTooltip();
}
}
};
this.onAnimationEnd = () => {
if (this.state.tooltipStatus === TooltipStatus.fadingout) {
this.setState({
tooltipStatus: TooltipStatus.hidden
});
}
};
this.getDismissStatus = () => {
return this.props.disabled ? TooltipStatus.hidden : TooltipStatus.fadingout;
};
this.shouldShowTooltip = (anchorElement) => {
if (this.state.tooltipStatus !== TooltipStatus.hidden) {
return false;
}
// If the tooltip only appears when the anchorElement overflows its parent then
// we need to check on mouse enter.
if (this.props.overflowOnly && !this.overflowDetected(anchorElement)) {
return false;
}
// Dont show the tooltip if there is not content to show.
if (!(this.props.text || this.props.renderContent || (anchorElement.innerText && this.props.overflowOnly))) {
return false;
}
return !this.props.disabled;
};
this.overflowDetected = props.overflowDetected || overflowDetected;
this.state = {
tooltipStatus: TooltipStatus.hidden
};
}
render() {
return (React.createElement(MouseWithin, { leaveDelay: 50, enterDelay: this.props.delayMs, onMouseLeave: this.closeTooltip, onMouseEnter: this.showTooltip }, (mouseWithinEvents) => {
const child = React.Children.only(this.props.children);
const id = this.props.id || this.tooltipId;
const showTooltip = this.state.tooltipStatus !== TooltipStatus.hidden && !this.props.disabled && this.state.anchorElement;
// Save the existing events we will potentially proxy.
const existingMouseEnter = child.props.onMouseEnter;
const existingMouseLeave = child.props.onMouseLeave;
const existingKeyDown = child.props.onKeyDown;
let existingBlur;
let existingFocus;
const onMouseEnter = (event) => {
if (mouseWithinEvents.onMouseEnter) {
mouseWithinEvents.onMouseEnter(event);
}
if (existingMouseEnter) {
existingMouseEnter(event);
}
};
const onMouseLeave = (event) => {
if (mouseWithinEvents.onMouseLeave) {
mouseWithinEvents.onMouseLeave(event);
}
if (existingMouseLeave) {
existingMouseLeave(event);
}
};
const onKeyDown = (event) => {
if (event.which === KeyCode.escape && showTooltip) {
this.setState({ tooltipStatus: TooltipStatus.hidden });
}
if (existingKeyDown) {
existingKeyDown(event);
}
};
// to not let consumers have to care about an implementation detail, wrap
// the tooltip id in getSafeId and use that as the aria-describedBy property
// on the child.
const ariaDescribedById = this.props.addAriaDescribedBy && this.state.tooltipStatus !== TooltipStatus.hidden ? getSafeId(id) : undefined;
const childProps = Object.assign(Object.assign({}, child.props), { onMouseEnter, onMouseLeave, onKeyDown });
if (childProps["aria-describedby"] === undefined) {
childProps["aria-describedby"] = ariaDescribedById;
}
let clonedChild = React.cloneElement(child, childProps, child.props.children);
// If this tooltip should become visible when focus is within the component add the focus tracking.
if (this.props.showOnFocus && (this.props.text || this.props.renderContent || this.props.overflowOnly)) {
existingBlur = child.props.onBlur;
existingFocus = child.props.onFocus;
const onBlur = () => {
this.focus = false;
if (!this.mouse) {
this.closeTooltip();
}
if (existingBlur) {
existingBlur();
}
};
const onFocus = (event) => {
const anchorElement = event.currentTarget;
if (this.shouldShowTooltip(anchorElement)) {
this.focus = true;
getFocusVisible() &&
this.setState({
anchorElement: event.target,
anchorOffset: { horizontal: 0, vertical: 8 },
anchorOrigin: { horizontal: Location.center, vertical: Location.end },
anchorPoint: undefined,
innerText: this.props.overflowOnly && !this.props.text ? anchorElement.innerText : undefined,
tooltipStatus: TooltipStatus.visible,
tooltipOrigin: { horizontal: Location.center, vertical: Location.start }
});
}
if (existingFocus) {
existingFocus(event);
}
};
clonedChild = (React.createElement(FocusWithin, { onBlur: onBlur, onFocus: onFocus, updateStateOnFocusChange: false }, clonedChild));
}
return (React.createElement(React.Fragment, null,
clonedChild,
showTooltip ? (React.createElement(Callout, { anchorElement: this.state.anchorElement, anchorOffset: this.props.anchorOffset || this.state.anchorOffset, anchorOrigin: this.props.anchorOrigin || this.state.anchorOrigin, anchorPoint: this.state.anchorPoint, calloutOrigin: this.props.tooltipOrigin || this.state.tooltipOrigin, className: css(this.props.className, "bolt-tooltip", this.state.tooltipStatus === TooltipStatus.fadingout && "bolt-tooltip-fade-out"), fixedLayout: this.props.fixedLayout, id: id, key: id, onAnimationEnd: this.onAnimationEnd, onMouseEnter: mouseWithinEvents.onMouseEnter, onMouseLeave: mouseWithinEvents.onMouseLeave, portalProps: {
className: "bolt-tooltip-portal",
bypassActiveElementFocusOnUnmount: true
}, contentRef: this.contentRef, role: "tooltip" },
React.createElement("div", { className: "bolt-tooltip-content body-m" }, (this.props.renderContent && this.props.renderContent()) || this.props.text || this.state.innerText))) : null));
}));
}
componentWillUnmount() {
document.removeEventListener("keydown", this.onKeyDown);
}
}
Tooltip.defaultProps = {
delayMs: 250,
showOnFocus: true
};
function overflowDetected(anchorElement) {
return anchorElement.scrollWidth > Math.ceil(anchorElement.offsetWidth);
}