UNPKG

@carbon/react

Version:

React components for the Carbon Design System

197 lines (195 loc) 5.92 kB
/** * 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 };