@atlaskit/tooltip
Version:
A tooltip is a floating, non-actionable label used to explain a user interface element or feature.
242 lines • 11.3 kB
JavaScript
;
/** @jsx jsx */
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var react_1 = tslib_1.__importStar(require("react"));
var core_1 = require("@emotion/core");
var bind_event_listener_1 = require("bind-event-listener");
var analytics_next_1 = require("@atlaskit/analytics-next");
var motion_1 = require("@atlaskit/motion");
var popper_1 = require("@atlaskit/popper");
var portal_1 = tslib_1.__importDefault(require("@atlaskit/portal"));
var constants_1 = require("@atlaskit/theme/constants");
var tooltip_manager_1 = require("./internal/tooltip-manager");
var TooltipContainer_1 = tslib_1.__importDefault(require("./TooltipContainer"));
var utilities_1 = require("./utilities");
var version_json_1 = require("./version.json");
var tooltipZIndex = constants_1.layers.tooltip();
var analyticsAttributes = {
componentName: 'tooltip',
packageName: version_json_1.name,
packageVersion: version_json_1.version,
};
function noop() { }
function Tooltip(_a) {
var children = _a.children, _b = _a.position, position = _b === void 0 ? 'bottom' : _b, _c = _a.mousePosition, mousePosition = _c === void 0 ? 'bottom' : _c, content = _a.content, _d = _a.truncate, truncate = _d === void 0 ? false : _d, _e = _a.component, Container = _e === void 0 ? TooltipContainer_1.default : _e, _f = _a.tag, TargetContainer = _f === void 0 ? 'div' : _f, testId = _a.testId, _g = _a.delay, delay = _g === void 0 ? 300 : _g, _h = _a.onShow, onShow = _h === void 0 ? noop : _h, _j = _a.onHide, onHide = _j === void 0 ? noop : _j, _k = _a.hideTooltipOnClick, hideTooltipOnClick = _k === void 0 ? false : _k, _l = _a.hideTooltipOnMouseDown, hideTooltipOnMouseDown = _l === void 0 ? false : _l, analyticsContext = _a.analyticsContext, _m = _a.strategy, strategy = _m === void 0 ? 'fixed' : _m;
var tooltipPosition = position === 'mouse' ? mousePosition : position;
var onShowHandler = analytics_next_1.usePlatformLeafEventHandler(tslib_1.__assign({ fn: onShow, action: 'displayed', analyticsData: analyticsContext }, analyticsAttributes));
var onHideHandler = analytics_next_1.usePlatformLeafEventHandler(tslib_1.__assign({ fn: onHide, action: 'hidden', analyticsData: analyticsContext }, analyticsAttributes));
var apiRef = react_1.useRef(null);
var _o = tslib_1.__read(react_1.useState('hide'), 2), state = _o[0], setState = _o[1];
var targetRef = react_1.useRef(null);
var containerRef = react_1.useRef(null);
var setRef = react_1.useCallback(function (node) {
if (!node || node.firstChild === null) {
return;
}
// @ts-ignore - React Ref typing is too strict for this use case
containerRef.current = node;
// @ts-ignore - React Ref typing is too strict for this use case
targetRef.current = node.firstChild;
}, []);
// Putting a few things into refs so that we don't have to break memoization
var lastState = react_1.useRef(state);
var lastDelay = react_1.useRef(delay);
var lastHandlers = react_1.useRef({ onShowHandler: onShowHandler, onHideHandler: onHideHandler });
var hasCalledShowHandler = react_1.useRef(false);
react_1.useEffect(function () {
lastState.current = state;
lastDelay.current = delay;
lastHandlers.current = { onShowHandler: onShowHandler, onHideHandler: onHideHandler };
}, [delay, onHideHandler, onShowHandler, state]);
var start = react_1.useCallback(function (api) {
// @ts-ignore
apiRef.current = api;
hasCalledShowHandler.current = false;
}, []);
var done = react_1.useCallback(function () {
if (!apiRef.current) {
return;
}
// Only call onHideHandler if we have called onShowHandler
if (hasCalledShowHandler.current) {
lastHandlers.current.onHideHandler();
}
// @ts-ignore
apiRef.current = null;
// @ts-ignore
hasCalledShowHandler.current = false;
// just in case
setState('hide');
}, []);
var abort = react_1.useCallback(function () {
if (!apiRef.current) {
return;
}
apiRef.current.abort();
// Only call onHideHandler if we have called onShowHandler
if (hasCalledShowHandler.current) {
lastHandlers.current.onHideHandler();
}
// @ts-ignore
apiRef.current = null;
}, []);
react_1.useEffect(function mount() {
return function unmount() {
if (apiRef.current) {
abort();
}
};
}, [abort]);
var showTooltip = react_1.useCallback(function (source) {
if (apiRef.current && !apiRef.current.isActive()) {
abort();
}
// Tell the tooltip to keep showing
if (apiRef.current && apiRef.current.isActive()) {
apiRef.current.keep();
return;
}
var entry = {
source: source,
delay: lastDelay.current,
show: function (_a) {
var isImmediate = _a.isImmediate;
// Call the onShow handler if it hasn't been called yet
if (!hasCalledShowHandler.current) {
hasCalledShowHandler.current = true;
lastHandlers.current.onShowHandler();
}
setState(isImmediate ? 'show-immediate' : 'show-fade-in');
},
hide: function (_a) {
var isImmediate = _a.isImmediate;
setState(function (current) {
if (current !== 'hide') {
return isImmediate ? 'hide' : 'fade-out';
}
return current;
});
},
done: done,
};
var api = tooltip_manager_1.show(entry);
start(api);
}, [abort, done, start]);
react_1.useEffect(function () {
if (state === 'hide') {
return noop;
}
var unbind = bind_event_listener_1.bind(window, {
type: 'scroll',
listener: function () {
if (apiRef.current) {
apiRef.current.requestHide({ isImmediate: true });
}
},
options: { capture: true, passive: true, once: true },
});
return unbind;
}, [state]);
var onMouseDown = react_1.useCallback(function () {
if (hideTooltipOnMouseDown && apiRef.current) {
apiRef.current.requestHide({ isImmediate: true });
}
}, [hideTooltipOnMouseDown]);
var onClick = react_1.useCallback(function () {
if (hideTooltipOnClick && apiRef.current) {
apiRef.current.requestHide({ isImmediate: true });
}
}, [hideTooltipOnClick]);
// Ideally we would be using onMouseEnter here, but
// because we are binding the event to the target parent
// we need to listen for the mouseover of all sub elements
// This means when moving along a tooltip we are quickly toggling
// between api.requestHide and api.keep. This it not ideal
var onMouseOver = react_1.useCallback(function (event) {
// Ignoring events from the container ref
if (event.target === containerRef.current) {
return;
}
// Using prevent default as a signal that parent tooltips
if (event.defaultPrevented) {
return;
}
event.preventDefault();
var source = position === 'mouse'
? {
type: 'mouse',
// TODO: ideally not recalculating this object each time
mouse: utilities_1.getMousePosition({
left: event.clientX,
top: event.clientY,
}),
}
: { type: 'keyboard' };
showTooltip(source);
}, [position, showTooltip]);
// Ideally we would be using onMouseEnter here, but
// because we are binding the event to the target parent
// we need to listen for the mouseout of all sub elements
// This means when moving along a tooltip we are quickly toggling
// between api.requestHide and api.keep. This it not ideal
var onMouseOut = react_1.useCallback(function (event) {
// Ignoring events from the container ref
if (event.target === containerRef.current) {
return;
}
// Using prevent default as a signal that parent tooltips
if (event.defaultPrevented) {
return;
}
event.preventDefault();
if (apiRef.current) {
apiRef.current.requestHide({ isImmediate: false });
}
}, []);
var onFocus = react_1.useCallback(function () {
showTooltip({ type: 'keyboard' });
}, [showTooltip]);
var onBlur = react_1.useCallback(function () {
if (apiRef.current) {
apiRef.current.requestHide({ isImmediate: false });
}
}, []);
var onAnimationFinished = react_1.useCallback(function (transition) {
// Using lastState here because motion is not picking up the latest value
if (transition === 'exiting' &&
lastState.current === 'fade-out' &&
apiRef.current) {
// @ts-ignore: refs are writeable
apiRef.current.finishHideAnimation();
}
}, []);
// Doing a cast because typescript is struggling to narrow the type
var CastTargetContainer = TargetContainer;
var shouldRenderTooltipContainer = state !== 'hide' && Boolean(content);
var shouldRenderTooltipChildren = state === 'show-immediate' || state === 'show-fade-in';
var getReferentElement = function () {
// Use the initial mouse position if appropriate, or the target element
var api = apiRef.current;
var initialMouse = api
? api.getInitialMouse()
: null;
if (position === 'mouse' && initialMouse) {
return initialMouse;
}
return targetRef.current || undefined;
};
return (core_1.jsx(react_1.default.Fragment, null,
core_1.jsx(CastTargetContainer, { onMouseOver: onMouseOver, onMouseOut: onMouseOut, onClick: onClick, onMouseDown: onMouseDown, onFocus: onFocus, onBlur: onBlur, ref: setRef, "data-testid": testId ? testId + "--container" : undefined }, children),
shouldRenderTooltipContainer ? (core_1.jsx(portal_1.default, { zIndex: tooltipZIndex },
core_1.jsx(popper_1.Popper, { placement: tooltipPosition, referenceElement: getReferentElement(), strategy: strategy }, function (_a) {
var ref = _a.ref, style = _a.style;
return (core_1.jsx(motion_1.ExitingPersistence, { appear: true }, shouldRenderTooltipChildren && (core_1.jsx(motion_1.FadeIn, { onFinish: onAnimationFinished, duration: state === 'show-immediate' ? 0 : undefined }, function (_a) {
var className = _a.className;
return (core_1.jsx(Container, { ref: ref, className: "Tooltip " + className, style: style, truncate: truncate, placement: tooltipPosition, testId: testId }, content));
}))));
}))) : null));
}
Tooltip.displayName = 'Tooltip';
exports.default = Tooltip;
//# sourceMappingURL=Tooltip.js.map