@atlaskit/tooltip
Version:
A tooltip briefly describes an interactive element on mouse hover or keyboard focus.
778 lines (747 loc) • 29 kB
JavaScript
/* tooltip.tsx generated by @compiled/babel-plugin v3.0.2 */
import _extends from "@babel/runtime/helpers/extends";
import "./tooltip.compiled.css";
import { ax, ix } from "@compiled/react/runtime";
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { bind } from 'bind-event-listener';
import { usePlatformLeafSyntheticEventHandler } from '@atlaskit/analytics-next/usePlatformLeafSyntheticEventHandler';
import noop from '@atlaskit/ds-lib/noop';
import useCloseOnEscapePress from '@atlaskit/ds-lib/use-close-on-escape-press';
import useStableRef from '@atlaskit/ds-lib/use-stable-ref';
import { useNotifyOpenLayerObserver } from '@atlaskit/layering/use-notify-open-layer-observer';
import ExitingPersistence from '@atlaskit/motion/exiting-persistence';
import FadeIn from '@atlaskit/motion/fade-in';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { Popper } from '@atlaskit/popper/main';
import Portal from '@atlaskit/portal/portal';
import { layers } from '@atlaskit/theme/constants';
import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map/index';
import { Popover } from '@atlaskit/top-layer/popover/popover';
import { useAnchoredPopover } from '@atlaskit/top-layer/use-anchored-popover';
import { useAnchoredPopoverAtPoint } from '@atlaskit/top-layer/use-anchored-popover-at-point';
import { register } from './internal/drag-manager';
import { getAnchorPoint } from './internal/get-anchor-point';
import { getVirtualElementFromMousePos } from './internal/get-virtual-element-from-mouse-pos';
import { show } from './internal/tooltip-manager';
import useUniqueId from './internal/use-unique-id';
import TooltipContainer from './tooltip-container';
const tooltipZIndex = layers.tooltip();
const analyticsAttributes = {
componentName: 'tooltip',
packageName: "@atlaskit/tooltip",
packageVersion: "24.3.3"
};
// Inverts motion direction
const invertedDirection = {
top: 'bottom',
bottom: 'top',
left: 'right',
right: 'left'
};
/**
* Converts a Popper placement to it's general direction.
*
* @param placement - Popper Placement value, e.g. 'top-start'
* @returns Popper Direction, e.g. 'top'
*/
const getDirectionFromPlacement = placement => placement.split('-')[0];
/**
* For the `platform-dst-top-layer-tooltip` path only. Always `false` when that
* gate is off, so this cannot affect the legacy path even if it gains a caller
* there.
*
* `mouseover` also fires when the pointer crosses boundaries between elements
* _inside_ the trigger. A `relatedTarget` the trigger does not contain is what
* distinguishes a real re-entry from one of those.
*/
function isTopLayerPointerReEntry({
trigger,
relatedTarget
}) {
if (!fg('platform-dst-top-layer-tooltip')) {
return false;
}
// Missing trigger, or pointer from outside the document. Treat both as an
// entry so we cannot get stuck suppressed.
if (!trigger || !(relatedTarget instanceof Node)) {
return true;
}
return !trigger.contains(relatedTarget);
}
/**
* __Tooltip__
*
* A tooltip is a floating, non-actionable label used to explain a user interface element or feature.
*/
function Tooltip({
children,
position = 'bottom',
mousePosition = 'bottom',
content,
truncate = false,
component: Container = TooltipContainer,
tag: TargetContainer = 'div',
testId,
delay = 300,
onShow = noop,
onHide = noop,
canAppear,
hideTooltipOnClick = false,
hideTooltipOnMouseDown = false,
analyticsContext,
strategy = 'fixed',
ignoreTooltipPointerEvents = false,
isScreenReaderAnnouncementDisabled = false,
shortcut,
shouldAlwaysFadeIn = false,
shouldRenderToParent = false
}) {
// Not using a gate for this check. When the gate is disabled `mouse-y` and `mouse-x` are treated as `mouse`.
const isMousePosition = position === 'mouse' || position === 'mouse-y' || position === 'mouse-x';
const tooltipPosition = isMousePosition ? mousePosition : position;
const onShowHandler = usePlatformLeafSyntheticEventHandler({
fn: onShow,
action: 'displayed',
analyticsData: analyticsContext,
...analyticsAttributes
});
const onHideHandler = usePlatformLeafSyntheticEventHandler({
fn: onHide,
action: 'hidden',
analyticsData: analyticsContext,
...analyticsAttributes
});
const apiRef = useRef(null);
const [state, setState] = useState('hide');
const targetRef = useRef(null);
const containerRef = useRef(null);
// This function is deliberately _not_ memoized as it needs to re-run every render
// to pick up any child ref changes. If you use render props you don't have this issue.
const setImplicitRefFromChildren = node => {
containerRef.current = node;
targetRef.current = node ? node.firstElementChild : null;
};
// This is memoized and passed into the render props callback.
const setDirectRef = useCallback(node => {
targetRef.current = node;
}, []);
// Putting a few things into refs so that we don't have to break memoization
const stableState = useStableRef(state);
// These props are placed in separate refs instead of a single object to reduce memory usage.
// Placing them in the same object previously caused an increase in the number of JavaScript event listeners
// before garbage collection.
const onShowHandlerStable = useStableRef(onShowHandler);
const onHideHandlerStable = useStableRef(onHideHandler);
const delayStable = useStableRef(delay);
const canAppearStable = useStableRef(canAppear);
const hasCalledShowHandler = useRef(false);
const shouldAlwaysFadeInStable = useStableRef(shouldAlwaysFadeIn);
const start = useCallback(api => {
apiRef.current = api;
hasCalledShowHandler.current = false;
}, []);
const done = useCallback(() => {
if (!apiRef.current) {
return;
}
// Only call onHideHandler if we have called onShowHandler
if (hasCalledShowHandler.current) {
onHideHandlerStable.current();
}
apiRef.current = null;
hasCalledShowHandler.current = false;
// just in case
setState('hide');
}, [onHideHandlerStable]);
const abort = useCallback(() => {
if (!apiRef.current) {
return;
}
apiRef.current.abort();
// Only call onHideHandler if we have called onShowHandler
if (hasCalledShowHandler.current) {
onHideHandlerStable.current();
}
apiRef.current = null;
}, [onHideHandlerStable]);
useEffect(function mount() {
return function unmount() {
if (apiRef.current) {
abort();
}
};
}, [abort]);
const isDraggingRef = useRef(false);
useEffect(() => {
return register({
onRegister({
isDragging
}) {
isDraggingRef.current = isDragging;
},
onDragStart() {
var _apiRef$current;
/**
* Hiding any visible tooltips when a drag starts because otherwise it
* looks janky (disappears and reappears), and is not required.
*/
(_apiRef$current = apiRef.current) === null || _apiRef$current === void 0 ? void 0 : _apiRef$current.requestHide({
isImmediate: true
});
isDraggingRef.current = true;
},
onDragEnd() {
isDraggingRef.current = false;
}
});
}, []);
// Set while a pointer press has dismissed the tooltip. Every read and write is
// behind `platform-dst-top-layer-tooltip`, where `popover="hint"` owns pointer
// dismissal, so the legacy path never sees this.
const isTopLayerPointerDismissedRef = useRef(false);
const tryShowTooltip = useCallback(source => {
var _canAppearStable$curr;
/**
* Prevent tooltips from being shown during a drag. This can occur with
* the native drag and drop API, where some pointer events can fire
* when they should not and lead to jank with tooltips.
*/
if (isDraggingRef.current) {
return;
}
// Another tooltip is has been active but we still have the old `api`
// around. We need to finish up the last usage.
// Note: just being safe - this should not happen
if (apiRef.current && !apiRef.current.isActive()) {
abort();
}
// This tooltip is already active, we can exit
if (apiRef.current && apiRef.current.isActive()) {
apiRef.current.keep();
return;
}
/**
* Stay dismissed until the trigger is re-entered or blurred. Otherwise
* light dismiss hides on pointerup and the next `mouseover` from a
* boundary crossing inside the trigger re-shows it. Deliberately after
* the "already active" branch, so a held press stays visible.
*/
if (isTopLayerPointerDismissedRef.current && fg('platform-dst-top-layer-tooltip')) {
return;
}
/**
* Check if tooltip is allowed to show.
*
* Once a tooltip has started, or has scheduled to start
* we won't be checking `canAppear` again.
*
* - We don't want tooltips to disappear once they are shown
* - For consistency, we start after a single positive `canAppear`.
* Otherwise the amount of times we ask consumers would depend on
* how many times we get a "mousemove", which _could_ lead to situations
* where moving the mouse could result in a different outcome to if
* the mouse was not moved.
*/
if (canAppearStable.current && !((_canAppearStable$curr = canAppearStable.current) !== null && _canAppearStable$curr !== void 0 && _canAppearStable$curr.call(canAppearStable))) {
return;
}
const entry = {
source,
delay: delayStable.current,
show: ({
isImmediate
}) => {
// Call the onShow handler if it hasn't been called yet
if (!hasCalledShowHandler.current) {
hasCalledShowHandler.current = true;
onShowHandlerStable.current();
}
setState(!isImmediate ? 'fade-in' : 'show-immediate');
},
hide: ({
isImmediate
}) => {
if (isImmediate) {
setState('hide');
} else if (fg('platform-dst-top-layer-tooltip')) {
// Top-layer path: set state to 'top-layer-exit'. The component
// stays mounted and Popover's isOpen prop transitions to
// false, triggering the CSS exit animation internally.
// finishHideAnimation is called after a brief delay matching
// the CSS exit animation duration.
setState('top-layer-exit');
} else {
setState('before-fade-out');
}
},
done,
shouldAlwaysFadeIn: shouldAlwaysFadeInStable.current
};
const api = show(entry);
start(api);
}, [canAppearStable, delayStable, done, start, abort, onShowHandlerStable, shouldAlwaysFadeInStable]);
const hideTooltipOnEsc = useCallback(() => {
var _apiRef$current2;
(_apiRef$current2 = apiRef.current) === null || _apiRef$current2 === void 0 ? void 0 : _apiRef$current2.requestHide({
isImmediate: true
});
}, [apiRef]);
// When using top-layer, popover="auto" handles Escape natively
useCloseOnEscapePress({
onClose: hideTooltipOnEsc,
isDisabled: state === 'hide' || state === 'fade-out' || state === 'top-layer-exit' || fg('platform-dst-top-layer-tooltip')
});
// ── Top-layer exit animation lifecycle ──
// When state is 'top-layer-exit', Popover's isOpen transitions to false and
// the CSS exit animation plays. The Popover's built-in `transitionend`
// detection (with timeout fallback) calls `onExitFinish` when the exit
// animation completes, which triggers the tooltip-manager lifecycle
// (finishHideAnimation → done → onHide, setState('hide'), cleanup).
const handleExitFinish = useCallback(() => {
var _apiRef$current3;
(_apiRef$current3 = apiRef.current) === null || _apiRef$current3 === void 0 ? void 0 : _apiRef$current3.finishHideAnimation();
}, []);
// Browser dismiss (light dismiss on pointerup, or Escape). Recorded so we stay
// hidden until the trigger is re-entered. Only wired up on the top-layer path,
// but gated anyway to keep every use of the ref flag-checked.
const handlePopoverClose = useCallback(() => {
var _apiRef$current4;
if (fg('platform-dst-top-layer-tooltip')) {
isTopLayerPointerDismissedRef.current = true;
}
(_apiRef$current4 = apiRef.current) === null || _apiRef$current4 === void 0 ? void 0 : _apiRef$current4.requestHide({
isImmediate: true
});
}, []);
useEffect(() => {
if (state === 'hide') {
return noop;
}
if (state === 'before-fade-out') {
setState('fade-out');
}
const unbind = bind(window, {
type: 'scroll',
listener: () => {
if (apiRef.current) {
apiRef.current.requestHide({
isImmediate: true
});
}
},
options: {
capture: true,
passive: true,
once: true
}
});
return unbind;
}, [state]);
const onMouseDown = useCallback(() => {
// Native light dismiss hides on pointerup, not here. Recorded now so the
// tooltip stays dismissed afterwards.
if (fg('platform-dst-top-layer-tooltip')) {
isTopLayerPointerDismissedRef.current = true;
}
// A press inside the show delay never reaches light dismiss, because no
// popover is open on pointerup. Cancel the pending show so a quick click
// does not surface a tooltip over content the press just changed.
const shouldCancelPendingShow = stableState.current === 'hide' && fg('platform-dst-top-layer-tooltip');
if ((hideTooltipOnMouseDown || shouldCancelPendingShow) && apiRef.current) {
apiRef.current.requestHide({
isImmediate: true
});
}
}, [hideTooltipOnMouseDown, stableState]);
const onClick = useCallback(() => {
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
const onMouseOver = useCallback(event => {
// Ignoring events from the container ref
if (containerRef.current && event.target === containerRef.current) {
return;
}
// Using prevent default as a signal that parent tooltips
if (event.defaultPrevented) {
return;
}
event.preventDefault();
// Re-arm on a real re-entry, not on a boundary crossing inside the trigger.
if (isTopLayerPointerReEntry({
trigger: targetRef.current,
relatedTarget: event.relatedTarget
}) && fg('platform-dst-top-layer-tooltip')) {
isTopLayerPointerDismissedRef.current = false;
}
const source = isMousePosition ? {
type: 'mouse',
clientX: event.clientX,
clientY: event.clientY
} : {
type: 'keyboard'
};
tryShowTooltip(source);
}, [isMousePosition, tryShowTooltip]);
// 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
const onMouseOut = useCallback(event => {
// Ignoring events from the container ref
if (containerRef.current && 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
});
}
}, []);
const onMouseMove = isMousePosition ? event => {
var _apiRef$current5;
if ((_apiRef$current5 = apiRef.current) !== null && _apiRef$current5 !== void 0 && _apiRef$current5.isActive()) {
apiRef.current.mousePos = {
clientX: event.clientX,
clientY: event.clientY
};
}
} : undefined;
const onMouseOverTooltip = useCallback(() => {
if (apiRef.current && apiRef.current.isActive()) {
apiRef.current.keep();
return;
}
}, []);
const onFocus = useCallback(e => {
// Check if focus-visible
// Prevents tooltips from showing when focus is not visible,
// i.e., when focus is moved onto tooltip trigger inside a popup on open
try {
if (!e.target.matches(':focus-visible')) {
return;
}
} catch {
// Ignore errors from environments that don't support :focus-visible
}
// TODO: this does not play well with `hideTooltipOnMouseDown`
// as "focus" will occur after the "mousedown".
tryShowTooltip({
type: 'keyboard'
});
}, [tryShowTooltip]);
const onBlur = useCallback(() => {
// Focus leaving ends the dismissal, so focus coming back can show the tooltip.
if (fg('platform-dst-top-layer-tooltip')) {
isTopLayerPointerDismissedRef.current = false;
}
if (apiRef.current) {
apiRef.current.requestHide({
isImmediate: false
});
}
}, []);
const onAnimationFinished = useCallback(transition => {
// Using lastState here because motion is not picking up the latest value
if (transition === 'exiting' && stableState.current === 'fade-out' && apiRef.current) {
apiRef.current.finishHideAnimation();
}
}, [stableState]);
// Doing a cast because typescript is struggling to narrow the type
const CastTargetContainer = TargetContainer;
const shouldRenderTooltipPopup = state !== 'hide' && Boolean(content);
const shouldRenderHiddenContent = !isScreenReaderAnnouncementDisabled && shouldRenderTooltipPopup;
const shouldRenderTooltipChildren = state !== 'hide' && state !== 'fade-out' && state !== 'top-layer-exit';
const handleOpenLayerObserverCloseSignal = useCallback(() => {
var _apiRef$current6;
(_apiRef$current6 = apiRef.current) === null || _apiRef$current6 === void 0 ? void 0 : _apiRef$current6.requestHide({
isImmediate: true
});
}, []);
// Registered unconditionally so the hook order never depends on the feature
// flag value. On the top-layer path the Popover primitive (used by
// TopLayerTooltipPopup) registers with the observer directly, so we pass
// isOpen: false to avoid double-counting (the hook is a no-op while the
// layer is not open).
useNotifyOpenLayerObserver({
// Layer is only visually open if both the tooltip popup (container) and children are rendered.
isOpen: fg('platform-dst-top-layer-tooltip') ? false : shouldRenderTooltipPopup && shouldRenderTooltipChildren,
/**
* We don't strictly need to provide an onClose callback at this time, as there is
* already code that handles hiding the tooltip when a drag is started (and the only
* usage right now is closing all layers when the user resizes the side nav).
*
* However, for future-proofing and semantic reasons, it makes sense to close the tooltip
* whenever the open layer observer requests a close.
*/
onClose: handleOpenLayerObserverCloseSignal
});
const getReferenceElement = () => {
var _apiRef$current7;
if (isMousePosition && (_apiRef$current7 = apiRef.current) !== null && _apiRef$current7 !== void 0 && _apiRef$current7.mousePos && targetRef.current) {
return getVirtualElementFromMousePos(apiRef.current.mousePos, {
targetElement: targetRef.current,
tooltipPosition: position
});
}
return targetRef.current || undefined;
};
const tooltipIdForHiddenContent = useUniqueId('tooltip', shouldRenderHiddenContent);
const tooltipTriggerProps = {
onMouseOver,
onMouseOut,
onMouseMove,
onMouseDown,
onClick,
onFocus,
onBlur
};
// This useEffect is purely for managing the aria attribute when using the
// wrapped children approach.
const isChildrenAFunction = typeof children === 'function';
useEffect(() => {
if (isChildrenAFunction) {
return;
}
// If `children` is _not_ a function, we are stepping outside of the public
// API to add a `aria-describedby` attribute.
const target = targetRef.current;
if (!target || !tooltipIdForHiddenContent) {
return;
}
target.setAttribute('aria-describedby', tooltipIdForHiddenContent);
return () => target.removeAttribute('aria-describedby');
}, [isChildrenAFunction, tooltipIdForHiddenContent]);
const hiddenContent = shouldRenderHiddenContent ? /*#__PURE__*/React.createElement("span", {
"data-testid": testId ? `${testId}-hidden` : undefined,
hidden: true,
id: tooltipIdForHiddenContent
}, typeof content === 'function' ? content({}) : content) : null;
const PopperWrapper = shouldRenderToParent ? Fragment : TooltipPortal;
const trigger = typeof children === 'function' ?
/*#__PURE__*/
// once we deprecate the wrapped approach, we can put the aria
// attribute back into the tooltipTriggerProps and make it required
// instead of optional in `types`
React.createElement(Fragment, null, children({
...tooltipTriggerProps,
// `testId` propagates to the trigger element so `data-testid` lands in the
// rendered DOM. Required because `@atlaskit/button/new` (and other Pressable-
// backed primitives) overwrite `data-testid` from spread; passing a typed
// `testId` lets their own destructure pick it up directly.
...(testId ? {
testId: `${testId}--container`
} : {}),
'aria-describedby': tooltipIdForHiddenContent,
ref: setDirectRef
}), hiddenContent) : /*#__PURE__*/React.createElement(CastTargetContainer, _extends({}, tooltipTriggerProps, testId ? {
'data-testid': `${testId}--container`
} : undefined, {
ref: setImplicitRefFromChildren
/**
* TODO: Why is role="presentation" added?
* - Is it only to "remove" the `Container` from screen readers?
* - Why is it added only to the `Container` but not to `tooltipTriggerProps`?
* - Should `role="presentation"` only be used if `shouldRenderHiddenContent == false`?
*/,
role: "presentation"
}), children, hiddenContent);
if (fg('platform-dst-top-layer-tooltip')) {
var _apiRef$current$mouse, _apiRef$current8;
return /*#__PURE__*/React.createElement(Fragment, null, trigger, shouldRenderTooltipPopup ? /*#__PURE__*/React.createElement(TopLayerTooltipPopup, {
targetRef: targetRef,
tooltipPosition: tooltipPosition,
mousePos: (_apiRef$current$mouse = (_apiRef$current8 = apiRef.current) === null || _apiRef$current8 === void 0 ? void 0 : _apiRef$current8.mousePos) !== null && _apiRef$current$mouse !== void 0 ? _apiRef$current$mouse : undefined,
position: position,
onMouseOut: onMouseOut,
onMouseOverTooltip: onMouseOverTooltip,
ignoreTooltipPointerEvents: ignoreTooltipPointerEvents,
truncate: truncate,
testId: testId,
shortcut: shortcut,
content: content,
Container: Container,
onClose: handlePopoverClose,
onExitFinish: handleExitFinish,
isOpen: state !== 'hide' && state !== 'top-layer-exit'
}) : null);
}
return /*#__PURE__*/React.createElement(Fragment, null, trigger, shouldRenderTooltipPopup ? /*#__PURE__*/React.createElement(PopperWrapper, null, /*#__PURE__*/React.createElement(Popper, {
placement: tooltipPosition,
referenceElement: getReferenceElement(),
strategy: strategy
}, ({
ref,
style,
update,
placement
}) => {
const direction = isMousePosition ? undefined : invertedDirection[getDirectionFromPlacement(placement)];
return /*#__PURE__*/React.createElement(ExitingPersistence, {
appear: true
}, shouldRenderTooltipChildren && /*#__PURE__*/React.createElement(FadeIn, {
distance: "constant",
entranceDirection: direction,
exitDirection: direction,
onFinish: onAnimationFinished,
duration: state !== 'show-immediate' ? 'medium' : 'none'
}, ({
className
}) => /*#__PURE__*/React.createElement(Container, {
ref: ref
/**
* "Tooltip" classname is a hook used by tests to manipulate
* and hide tooltips, including in VR snapshots
*/
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
,
className: `Tooltip ${className}`,
style: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
...style,
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
...(ignoreTooltipPointerEvents && {
pointerEvents: 'none'
})
},
truncate: truncate,
placement: tooltipPosition,
testId: getReferenceElement() ? testId : testId && `${testId}--unresolved`,
onMouseOut: onMouseOut,
onMouseOver: onMouseOverTooltip,
shortcut: shortcut
}, typeof content === 'function' ? content({
update
}) : content)));
})) : null);
}
export default Tooltip;
const TooltipPortal = ({
children
}) => {
return /*#__PURE__*/React.createElement(Portal, {
zIndex: tooltipZIndex
}, children);
};
const tooltipMouseAnimationStyles = {
enter: "_j7hq1bym",
exit: "_j7hq1a5s"
};
/**
* Top-layer tooltip popup component.
*
* Composes `Popover` (top-layer visibility + animation) with two positioning
* hooks, of which EXACTLY ONE is enabled - two writing to the same popover would
* fight for ownership, so `isEnabled` is derived from one boolean and negated:
*
* - `useAnchoredPopoverAtPoint` for cursor-tracking positions (`mouse`,
* `mouse-x`, `mouse-y`) activated by a pointer.
* - `useAnchoredPopover` for everything else, including a cursor position
* activated via keyboard focus, where there is no cursor to track and
* anchoring to the trigger is what keeps the tooltip beside the target.
*
* Exit animation is handled by `Popover`'s `isOpen` prop. When `isOpen`
* transitions to `false`, the primitive calls `hidePopover()` internally and
* the CSS exit animation plays via `allow-discrete`. No glue code needed.
*/
function TopLayerTooltipPopup({
targetRef,
tooltipPosition,
mousePos,
position,
onMouseOut,
onMouseOverTooltip,
ignoreTooltipPointerEvents,
truncate,
testId,
shortcut,
content,
Container,
onClose,
onExitFinish,
isOpen
}) {
const popoverRef = useRef(null);
// Translate the legacy Popper-style placement string ("right",
// "bottom-start", etc.) once and pass the same object to the hook and
// to `Popover`.
const placement = fromLegacyPlacement({
legacy: tooltipPosition
});
const isMousePosition = position === 'mouse' || position === 'mouse-x' || position === 'mouse-y';
const isMouseStrategyActive = isMousePosition && Boolean(mousePos);
// One object so the two calls cannot drift on `placement` or `isOpen`.
const sharedPositioning = {
popoverRef,
placement,
isOpen
};
useAnchoredPopover({
...sharedPositioning,
anchorRef: targetRef,
isEnabled: !isMouseStrategyActive
});
// `getPoint()` is latched once per activation, which is per-show here because
// `TopLayerTooltipPopup` mounts fresh on every show. `null` before the trigger
// has mounted applies no positioning at all.
useAnchoredPopoverAtPoint({
...sharedPositioning,
isEnabled: isMouseStrategyActive,
getPoint: () => {
if (!mousePos || !targetRef.current || !isMousePosition) {
return null;
}
return getAnchorPoint({
cursor: mousePos,
triggerRect: targetRef.current.getBoundingClientRect(),
tooltipPosition: position,
placement
});
}
});
return /*#__PURE__*/React.createElement(Popover, {
ref: popoverRef,
role: "tooltip",
mode: "hint",
isOpen: isOpen,
onClose: onClose,
onExitFinish: onExitFinish,
testId: testId ? `${testId}--popover` : undefined,
shouldAnimate: true,
enteringAnimationXcss: isMouseStrategyActive && tooltipMouseAnimationStyles.enter,
exitingAnimationXcss: isMouseStrategyActive && tooltipMouseAnimationStyles.exit,
placement: placement
}, /*#__PURE__*/React.createElement(Container
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- top-layer spike
, {
className: "Tooltip"
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- top-layer spike
,
style: ignoreTooltipPointerEvents ? {
pointerEvents: 'none'
} : undefined,
truncate: truncate,
placement: tooltipPosition,
testId: testId,
onMouseOut: onMouseOut,
onMouseOver: onMouseOverTooltip,
shortcut: shortcut,
role: "presentation"
}, typeof content === 'function' ? content({
update: noop
}) : content));
}