@shopify/polaris
Version:
Shopify’s admin product component library
184 lines (180 loc) • 5.78 kB
JavaScript
import React, { useState, useId, useRef, useCallback, useEffect } from 'react';
import { findFirstFocusableNode } from '../../utilities/focus.js';
import { useToggle } from '../../utilities/use-toggle.js';
import { classNames } from '../../utilities/css.js';
import styles from './Tooltip.css.js';
import { useEphemeralPresenceManager } from '../../utilities/ephemeral-presence-manager/hooks.js';
import { Portal } from '../Portal/Portal.js';
import { TooltipOverlay } from './components/TooltipOverlay/TooltipOverlay.js';
import { Text } from '../Text/Text.js';
const HOVER_OUT_TIMEOUT = 150;
function Tooltip({
children,
content,
dismissOnMouseOut,
active: originalActive,
hoverDelay,
preferredPosition = 'above',
activatorWrapper = 'span',
accessibilityLabel,
width = 'default',
padding = 'default',
borderRadius: borderRadiusProp,
zIndexOverride,
hasUnderline,
persistOnClick,
onOpen,
onClose
}) {
const borderRadius = borderRadiusProp || '200';
const WrapperComponent = activatorWrapper;
const {
value: active,
setTrue: setActiveTrue,
setFalse: handleBlur
} = useToggle(Boolean(originalActive));
const {
value: persist,
toggle: togglePersisting
} = useToggle(Boolean(originalActive) && Boolean(persistOnClick));
const [activatorNode, setActivatorNode] = useState(null);
const {
presenceList,
addPresence,
removePresence
} = useEphemeralPresenceManager();
const id = useId();
const activatorContainer = useRef(null);
const mouseEntered = useRef(false);
const [shouldAnimate, setShouldAnimate] = useState(Boolean(!originalActive));
const hoverDelayTimeout = useRef(null);
const hoverOutTimeout = useRef(null);
const handleFocus = useCallback(() => {
if (originalActive !== false) {
setActiveTrue();
}
}, [originalActive, setActiveTrue]);
useEffect(() => {
const firstFocusable = activatorContainer.current ? findFirstFocusableNode(activatorContainer.current) : null;
const accessibilityNode = firstFocusable || activatorContainer.current;
if (!accessibilityNode) return;
accessibilityNode.tabIndex = 0;
accessibilityNode.setAttribute('aria-describedby', id);
accessibilityNode.setAttribute('data-polaris-tooltip-activator', 'true');
}, [id, children]);
useEffect(() => {
return () => {
if (hoverDelayTimeout.current) {
clearTimeout(hoverDelayTimeout.current);
}
if (hoverOutTimeout.current) {
clearTimeout(hoverOutTimeout.current);
}
};
}, []);
const handleOpen = useCallback(() => {
setShouldAnimate(!presenceList.tooltip && !active);
onOpen?.();
addPresence('tooltip');
}, [addPresence, presenceList.tooltip, onOpen, active]);
const handleClose = useCallback(() => {
onClose?.();
setShouldAnimate(false);
hoverOutTimeout.current = setTimeout(() => {
removePresence('tooltip');
}, HOVER_OUT_TIMEOUT);
}, [removePresence, onClose]);
const handleKeyUp = useCallback(event => {
if (event.key !== 'Escape') return;
handleClose?.();
handleBlur();
persistOnClick && togglePersisting();
}, [handleBlur, handleClose, persistOnClick, togglePersisting]);
useEffect(() => {
if (originalActive === false && active) {
handleClose();
handleBlur();
}
}, [originalActive, active, handleClose, handleBlur]);
const portal = activatorNode ? /*#__PURE__*/React.createElement(Portal, {
idPrefix: "tooltip"
}, /*#__PURE__*/React.createElement(TooltipOverlay, {
id: id,
preferredPosition: preferredPosition,
activator: activatorNode,
active: active,
accessibilityLabel: accessibilityLabel,
onClose: noop,
preventInteraction: dismissOnMouseOut,
width: width,
padding: padding,
borderRadius: borderRadius,
zIndexOverride: zIndexOverride,
instant: !shouldAnimate
}, /*#__PURE__*/React.createElement(Text, {
as: "span",
variant: "bodyMd"
}, content))) : null;
const wrapperClassNames = classNames(activatorWrapper === 'div' && styles.TooltipContainer, hasUnderline && styles.HasUnderline);
return /*#__PURE__*/React.createElement(WrapperComponent, {
onFocus: () => {
handleOpen();
handleFocus();
},
onBlur: () => {
handleClose();
handleBlur();
if (persistOnClick) {
togglePersisting();
}
},
onMouseLeave: handleMouseLeave,
onMouseOver: handleMouseEnterFix,
onMouseDown: persistOnClick ? togglePersisting : undefined,
ref: setActivator,
onKeyUp: handleKeyUp,
className: wrapperClassNames
}, children, portal);
function setActivator(node) {
const activatorContainerRef = activatorContainer;
if (node == null) {
activatorContainerRef.current = null;
setActivatorNode(null);
return;
}
if (node.firstElementChild) {
setActivatorNode(node.firstElementChild);
}
activatorContainerRef.current = node;
}
function handleMouseEnter() {
mouseEntered.current = true;
if (hoverDelay && !presenceList.tooltip) {
hoverDelayTimeout.current = setTimeout(() => {
handleOpen();
handleFocus();
}, hoverDelay);
} else {
handleOpen();
handleFocus();
}
}
function handleMouseLeave() {
if (hoverDelayTimeout.current) {
clearTimeout(hoverDelayTimeout.current);
hoverDelayTimeout.current = null;
}
mouseEntered.current = false;
handleClose();
if (!persist) {
handleBlur();
}
}
// https://github.com/facebook/react/issues/10109
// Mouseenter event not triggered when cursor moves from disabled button
function handleMouseEnterFix() {
!mouseEntered.current && handleMouseEnter();
}
}
function noop() {}
export { Tooltip };