@carbon/react
Version:
React components for the Carbon Design System
281 lines (267 loc) • 9.84 kB
JavaScript
/**
* 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.
*/
;
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;