@atlaskit/tooltip
Version:
A tooltip is a floating, non-actionable label used to explain a user interface element or feature.
240 lines • 10.7 kB
JavaScript
/** @jsx jsx */
import { __assign, __read } from "tslib";
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { jsx } from '@emotion/core';
import { bind } from 'bind-event-listener';
import { usePlatformLeafEventHandler } from '@atlaskit/analytics-next';
import { ExitingPersistence, FadeIn } from '@atlaskit/motion';
import { Popper } from '@atlaskit/popper';
import Portal from '@atlaskit/portal';
import { layers } from '@atlaskit/theme/constants';
import { show } from './internal/tooltip-manager';
import TooltipContainer from './TooltipContainer';
import { getMousePosition } from './utilities';
import { name as packageName, version as packageVersion } from './version.json';
var tooltipZIndex = layers.tooltip();
var analyticsAttributes = {
componentName: 'tooltip',
packageName: packageName,
packageVersion: packageVersion,
};
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 : _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;
var tooltipPosition = position === 'mouse' ? mousePosition : position;
var onShowHandler = usePlatformLeafEventHandler(__assign({ fn: onShow, action: 'displayed', analyticsData: analyticsContext }, analyticsAttributes));
var onHideHandler = usePlatformLeafEventHandler(__assign({ fn: onHide, action: 'hidden', analyticsData: analyticsContext }, analyticsAttributes));
var apiRef = useRef(null);
var _m = __read(useState('hide'), 2), state = _m[0], setState = _m[1];
var targetRef = useRef(null);
var containerRef = useRef(null);
var setRef = 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 = useRef(state);
var lastDelay = useRef(delay);
var lastHandlers = useRef({ onShowHandler: onShowHandler, onHideHandler: onHideHandler });
var hasCalledShowHandler = useRef(false);
useEffect(function () {
lastState.current = state;
lastDelay.current = delay;
lastHandlers.current = { onShowHandler: onShowHandler, onHideHandler: onHideHandler };
}, [delay, onHideHandler, onShowHandler, state]);
var start = useCallback(function (api) {
// @ts-ignore
apiRef.current = api;
hasCalledShowHandler.current = false;
}, []);
var done = 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 = 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;
}, []);
useEffect(function mount() {
return function unmount() {
if (apiRef.current) {
abort();
}
};
}, [abort]);
var showTooltip = 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 = show(entry);
start(api);
}, [abort, done, start]);
useEffect(function () {
if (state === 'hide') {
return noop;
}
var unbind = 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 = useCallback(function () {
if (hideTooltipOnMouseDown && apiRef.current) {
apiRef.current.requestHide({ isImmediate: true });
}
}, [hideTooltipOnMouseDown]);
var onClick = 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 = 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: 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 = 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 = useCallback(function () {
showTooltip({ type: 'keyboard' });
}, [showTooltip]);
var onBlur = useCallback(function () {
if (apiRef.current) {
apiRef.current.requestHide({ isImmediate: false });
}
}, []);
var onAnimationFinished = 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 (jsx(React.Fragment, null,
jsx(CastTargetContainer, { onMouseOver: onMouseOver, onMouseOut: onMouseOut, onClick: onClick, onMouseDown: onMouseDown, onFocus: onFocus, onBlur: onBlur, ref: setRef, "data-testid": testId ? testId + "--container" : undefined }, children),
shouldRenderTooltipContainer ? (jsx(Portal, { zIndex: tooltipZIndex },
jsx(Popper, { placement: tooltipPosition, referenceElement: getReferentElement() }, function (_a) {
var ref = _a.ref, style = _a.style;
return (jsx(ExitingPersistence, { appear: true }, shouldRenderTooltipChildren && (jsx(FadeIn, { onFinish: onAnimationFinished, duration: state === 'show-immediate' ? 0 : undefined }, function (_a) {
var className = _a.className;
return (jsx(Container, { ref: ref, className: "Tooltip " + className, style: style, truncate: truncate, placement: tooltipPosition, testId: testId }, content));
}))));
}))) : null));
}
Tooltip.displayName = 'Tooltip';
export default Tooltip;
//# sourceMappingURL=Tooltip.js.map