UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

196 lines (195 loc) 10.5 kB
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); }