UNPKG

@carbon/react

Version:

React components for the Carbon Design System

281 lines (267 loc) 9.84 kB
/** * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var cx = require('classnames'); var PropTypes = require('prop-types'); var React = require('react'); var index = require('../Popover/index.js'); var keys = require('../../internal/keyboard/keys.js'); var match = require('../../internal/keyboard/match.js'); var useDelayedState = require('../../internal/useDelayedState.js'); var useId = require('../../internal/useId.js'); var useNoInteractiveChildren = require('../../internal/useNoInteractiveChildren.js'); var usePrefix = require('../../internal/usePrefix.js'); var useIsomorphicEffect = require('../../internal/useIsomorphicEffect.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); /** * Event types that trigger a "drag" to stop. */ const DRAG_STOP_EVENT_TYPES = new Set(['mouseup', 'touchend', 'touchcancel']); const Tooltip = /*#__PURE__*/React__default["default"].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 = React.useRef(null); const [open, setOpen] = useDelayedState.useDelayedState(defaultOpen); const [isDragging, setIsDragging] = React.useState(false); const [focusByMouse, setFocusByMouse] = React.useState(false); const [isPointerIntersecting, setIsPointerIntersecting] = React.useState(false); const id = useId.useId('tooltip'); const prefix = usePrefix.usePrefix(); const child = React__default["default"].Children.only(children); const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy } = child?.props ?? {}; const hasLabel = !!label; const hasAriaLabel = typeof ariaLabel === 'string' ? ariaLabel.trim() !== '' : false; // An `aria-label` takes precedence over `aria-describedby`, but when it's // needed and the user doesn't specify one, the fallback `id` is used. const labelledBy = hasAriaLabel ? undefined : hasLabel ? ariaLabelledBy ?? id : undefined; // If `aria-label` is present, use any provided `aria-describedby`. // If not, fallback to child's `aria-describedby` or the tooltip `id` if needed. const describedBy = hasAriaLabel ? ariaDescribedBy : ariaDescribedBy ?? (!hasLabel && !ariaLabelledBy ? id : undefined); const triggerProps = { onFocus: () => !focusByMouse && setOpen(true), onBlur: () => { setOpen(false); setFocusByMouse(false); }, onClick: () => closeOnActivation && setOpen(false), // This should be placed on the trigger in case the element is disabled onMouseEnter, onMouseLeave, onMouseDown, onMouseMove: onMouseMove, onTouchStart: onDragStart, 'aria-labelledby': labelledBy, 'aria-describedby': describedBy }; 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 = React.useCallback(event => { if (open && match.match(event, keys.Escape)) { event.stopPropagation(); setOpen(false); } if (open && closeOnActivation && (match.match(event, keys.Enter) || match.match(event, keys.Space))) { setOpen(false); } }, [closeOnActivation, open, setOpen]); useIsomorphicEffect["default"](() => { if (!open) { return undefined; } function handleKeyDown(event) { if (match.match(event, keys.Escape)) { onKeyDown(event); } } document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [open, onKeyDown]); function onMouseEnter() { // Interactive Tags should not support 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 = React.useCallback(() => { setIsDragging(false); // Close the tooltip, unless the mouse pointer is within the bounds of the // trigger. if (!isPointerIntersecting) { setOpen(false, leaveDelayMs); } }, [isPointerIntersecting, leaveDelayMs, setOpen]); useNoInteractiveChildren.useNoInteractiveChildren(tooltipRef, 'The Tooltip component must have no interactive content rendered by the' + '`label` or `description` prop'); React.useEffect(() => { if (isDragging) { // Register drag stop handlers. 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__*/React__default["default"].createElement(index.Popover, _rollupPluginBabelHelpers["extends"]({ as: as, ref: ref }, rest, { align: align, className: cx__default["default"](`${prefix}--tooltip`, customClassName), dropShadow: dropShadow, highContrast: highContrast, onKeyDown: onKeyDown, onMouseLeave: onMouseLeave, open: open }), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--tooltip-trigger__wrapper` }, typeof child !== 'undefined' ? /*#__PURE__*/React__default["default"].cloneElement(child, { ...triggerProps, ...getChildEventHandlers(child.props) }) : null), /*#__PURE__*/React__default["default"].createElement(index.PopoverContent, { "aria-hidden": open ? 'false' : 'true', className: `${prefix}--tooltip-content`, id: id, onMouseEnter: onMouseEnter, role: "tooltip" }, label || description)); }); Tooltip.propTypes = { /** * Specify how the trigger should align with the tooltip */ align: PropTypes__default["default"].oneOf(['top', 'top-left', // deprecated use top-start instead 'top-right', // deprecated use top-end instead 'bottom', 'bottom-left', // deprecated use bottom-start instead 'bottom-right', // deprecated use bottom-end instead 'left', 'left-bottom', // deprecated use left-end instead 'left-top', // deprecated use left-start instead 'right', 'right-bottom', // deprecated use right-end instead 'right-top', // deprecated use right-start instead // new values to match floating-ui 'top-start', 'top-end', 'bottom-start', 'bottom-end', 'left-end', 'left-start', 'right-end', 'right-start']), /** * Pass in the child to which the tooltip will be applied */ children: PropTypes__default["default"].node, /** * Specify an optional className to be applied to the container node */ className: PropTypes__default["default"].string, /** * Determines wether the tooltip should close when inner content is activated (click, Enter or Space) */ closeOnActivation: PropTypes__default["default"].bool, /** * Specify whether the tooltip should be open when it first renders */ defaultOpen: PropTypes__default["default"].bool, /** * Provide the description to be rendered inside of the Tooltip. The * description will use `aria-describedby` and will describe the child node * in addition to the text rendered inside of the child. This means that if you * have text in the child node, that it will be announced alongside the * description to the screen reader. * * Note: if label and description are both provided, label will be used and * description will not be used */ description: PropTypes__default["default"].node, /** * Specify whether a drop shadow should be rendered */ dropShadow: PropTypes__default["default"].bool, /** * Specify the duration in milliseconds to delay before displaying the tooltip */ enterDelayMs: PropTypes__default["default"].number, /** * Render the component using the high-contrast theme */ highContrast: PropTypes__default["default"].bool, /** * Provide the label to be rendered inside of the Tooltip. The label will use * `aria-labelledby` and will fully describe the child node that is provided. * This means that if you have text in the child node, that it will not be * announced to the screen reader. * * Note: if label and description are both provided, description will not be * used */ label: PropTypes__default["default"].node, /** * Specify the duration in milliseconds to delay before hiding the tooltip */ leaveDelayMs: PropTypes__default["default"].number }; exports.Tooltip = Tooltip;