UNPKG

@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
/** @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