@carbon/react
Version:
React components for the Carbon Design System
197 lines (195 loc) • 5.92 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { usePrefix } from "../../internal/usePrefix.js";
import { Enter, Escape, Space } from "../../internal/keyboard/keys.js";
import { match } from "../../internal/keyboard/match.js";
import useIsomorphicEffect from "../../internal/useIsomorphicEffect.js";
import { useId } from "../../internal/useId.js";
import { Popover, PopoverContent } from "../Popover/index.js";
import { useDelayedState } from "../../internal/useDelayedState.js";
import { useNoInteractiveChildren } from "../../internal/useNoInteractiveChildren.js";
import classNames from "classnames";
import React, { useCallback, useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import { jsx, jsxs } from "react/jsx-runtime";
//#region src/components/Tooltip/Tooltip.tsx
/**
* Copyright IBM Corp. 2016, 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Event types that trigger a "drag" to stop.
*/
const DRAG_STOP_EVENT_TYPES = new Set([
"mouseup",
"touchend",
"touchcancel"
]);
const Tooltip = React.forwardRef(({ as, align = "top", className: customClassName, children, label, description, enterDelayMs = 100, leaveDelayMs = 300, defaultOpen = false, closeOnActivation = false, dropShadow = false, highContrast = true, ...rest }, ref) => {
const tooltipRef = useRef(null);
const [open, setOpen] = useDelayedState(defaultOpen);
const [isDragging, setIsDragging] = useState(false);
const [focusByMouse, setFocusByMouse] = useState(false);
const [isPointerIntersecting, setIsPointerIntersecting] = useState(false);
const id = useId("tooltip");
const prefix = usePrefix();
const child = React.Children.only(children);
const { "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy } = child?.props ?? {};
const hasLabel = !!label;
const triggerProps = {
onFocus: () => !focusByMouse && setOpen(true),
onBlur: () => {
setOpen(false);
setFocusByMouse(false);
},
onClick: () => closeOnActivation && setOpen(false),
onMouseEnter,
onMouseLeave,
onMouseDown,
onMouseMove,
onTouchStart: onDragStart,
"aria-labelledby": ariaLabelledBy ?? (hasLabel ? id : void 0),
"aria-describedby": ariaDescribedBy ?? (!hasLabel ? id : void 0)
};
function getChildEventHandlers(childProps) {
const eventHandlerFunctions = Object.keys(triggerProps).filter((prop) => prop.startsWith("on"));
const eventHandlers = {};
eventHandlerFunctions.forEach((functionName) => {
eventHandlers[functionName] = (evt) => {
triggerProps[functionName](evt);
if (childProps?.[functionName]) childProps?.[functionName](evt);
};
});
return eventHandlers;
}
const onKeyDown = useCallback((event) => {
if (open && match(event, Escape)) {
event.stopPropagation();
setOpen(false);
}
if (open && closeOnActivation && (match(event, Enter) || match(event, Space))) setOpen(false);
}, [
closeOnActivation,
open,
setOpen
]);
useIsomorphicEffect(() => {
if (!open) return;
function handleKeyDown(event) {
if (match(event, Escape)) onKeyDown(event);
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [open, onKeyDown]);
function onMouseEnter() {
if (!rest?.onMouseEnter) {
setIsPointerIntersecting(true);
setOpen(true, enterDelayMs);
}
}
function onMouseDown() {
setFocusByMouse(true);
onDragStart();
}
function onMouseLeave() {
setIsPointerIntersecting(false);
if (isDragging) return;
setOpen(false, leaveDelayMs);
}
function onMouseMove(evt) {
if (evt.buttons === 1) setIsDragging(true);
else setIsDragging(false);
}
function onDragStart() {
setIsDragging(true);
}
const onDragStop = useCallback(() => {
setIsDragging(false);
if (!isPointerIntersecting) setOpen(false, leaveDelayMs);
}, [
isPointerIntersecting,
leaveDelayMs,
setOpen
]);
useNoInteractiveChildren(tooltipRef, "The Tooltip component must have no interactive content rendered by the`label` or `description` prop");
useEffect(() => {
if (isDragging) DRAG_STOP_EVENT_TYPES.forEach((eventType) => {
document.addEventListener(eventType, onDragStop);
});
return () => {
DRAG_STOP_EVENT_TYPES.forEach((eventType) => {
document.removeEventListener(eventType, onDragStop);
});
};
}, [isDragging, onDragStop]);
return /* @__PURE__ */ jsxs(Popover, {
as,
ref,
...rest,
align,
className: classNames(`${prefix}--tooltip`, customClassName),
dropShadow,
highContrast,
onKeyDown,
onMouseLeave,
open,
children: [/* @__PURE__ */ jsx("div", {
className: `${prefix}--tooltip-trigger__wrapper`,
children: typeof child !== "undefined" ? React.cloneElement(child, {
...triggerProps,
...getChildEventHandlers(child.props)
}) : null
}), /* @__PURE__ */ jsx(PopoverContent, {
"aria-hidden": open ? "false" : "true",
className: `${prefix}--tooltip-content`,
id,
onMouseEnter,
role: "tooltip",
children: label || description
})]
});
});
Tooltip.propTypes = {
align: PropTypes.oneOf([
"top",
"top-left",
"top-right",
"bottom",
"bottom-left",
"bottom-right",
"left",
"left-bottom",
"left-top",
"right",
"right-bottom",
"right-top",
"top-start",
"top-end",
"bottom-start",
"bottom-end",
"left-end",
"left-start",
"right-end",
"right-start"
]),
children: PropTypes.node,
className: PropTypes.string,
closeOnActivation: PropTypes.bool,
defaultOpen: PropTypes.bool,
description: PropTypes.node,
dropShadow: PropTypes.bool,
enterDelayMs: PropTypes.number,
highContrast: PropTypes.bool,
label: PropTypes.node,
leaveDelayMs: PropTypes.number
};
//#endregion
export { Tooltip };