@mui/joy
Version:
Joy UI is an open-source React component library that implements MUI's own design principles. It's comprehensive and can be used in production out of the box.
696 lines (692 loc) • 23.9 kB
JavaScript
'use client';
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
import _extends from "@babel/runtime/helpers/esm/extends";
const _excluded = ["children", "className", "component", "arrow", "describeChild", "disableFocusListener", "disableHoverListener", "disableInteractive", "disableTouchListener", "enterDelay", "enterNextDelay", "enterTouchDelay", "followCursor", "id", "leaveDelay", "leaveTouchDelay", "onClose", "onOpen", "open", "disablePortal", "direction", "keepMounted", "modifiers", "placement", "title", "color", "variant", "size", "slots", "slotProps"];
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { unstable_capitalize as capitalize, unstable_useControlled as useControlled, unstable_useEventCallback as useEventCallback, unstable_useForkRef as useForkRef, unstable_useIsFocusVisible as useIsFocusVisible, unstable_useId as useId, unstable_useTimeout as useTimeout, unstable_Timeout as Timeout } from '@mui/utils';
import { Popper, unstable_composeClasses as composeClasses } from '@mui/base';
import styled from '../styles/styled';
import useThemeProps from '../styles/useThemeProps';
import useSlot from '../utils/useSlot';
import { getTooltipUtilityClass } from './tooltipClasses';
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
const useUtilityClasses = ownerState => {
const {
arrow,
variant,
color,
size,
placement,
touch
} = ownerState;
const slots = {
root: ['root', arrow && 'tooltipArrow', touch && 'touch', size && `size${capitalize(size)}`, color && `color${capitalize(color)}`, variant && `variant${capitalize(variant)}`, `tooltipPlacement${capitalize(placement.split('-')[0])}`],
arrow: ['arrow']
};
return composeClasses(slots, getTooltipUtilityClass, {});
};
const TooltipRoot = styled('div', {
name: 'JoyTooltip',
slot: 'Root',
overridesResolver: (props, styles) => styles.root
})(({
ownerState,
theme
}) => {
var _theme$variants, _ownerState$placement, _ownerState$placement2;
const variantStyle = (_theme$variants = theme.variants[ownerState.variant]) == null ? void 0 : _theme$variants[ownerState.color];
return _extends({}, ownerState.size === 'sm' && {
'--Icon-fontSize': theme.vars.fontSize.md,
'--Tooltip-arrowSize': '8px',
padding: theme.spacing(0.25, 0.625)
}, ownerState.size === 'md' && {
'--Icon-fontSize': theme.vars.fontSize.lg,
'--Tooltip-arrowSize': '10px',
padding: theme.spacing(0.5, 0.75)
}, ownerState.size === 'lg' && {
'--Icon-fontSize': theme.vars.fontSize.xl,
'--Tooltip-arrowSize': '12px',
padding: theme.spacing(0.75, 1)
}, {
zIndex: theme.vars.zIndex.tooltip,
borderRadius: theme.vars.radius.sm,
boxShadow: theme.shadow.sm,
wordWrap: 'break-word',
position: 'relative'
}, ownerState.disableInteractive && {
pointerEvents: 'none'
}, theme.typography[`body-${{
sm: 'xs',
md: 'sm',
lg: 'md'
}[ownerState.size]}`], variantStyle, !variantStyle.backgroundColor && {
backgroundColor: theme.vars.palette.background.surface
}, {
'&::before': {
// acts as a invisible connector between the element and the tooltip
// so that the cursor can move to the tooltip without losing focus.
content: '""',
display: 'block',
position: 'absolute',
width: (_ownerState$placement = ownerState.placement) != null && _ownerState$placement.match(/(top|bottom)/) ? '100%' :
// 10px equals the default offset popper config
'calc(10px + var(--variant-borderWidth, 0px))',
height: (_ownerState$placement2 = ownerState.placement) != null && _ownerState$placement2.match(/(top|bottom)/) ? 'calc(10px + var(--variant-borderWidth, 0px))' : '100%'
},
'&[data-popper-placement*="bottom"]::before': {
top: 0,
left: 0,
transform: 'translateY(-100%)'
},
'&[data-popper-placement*="left"]::before': {
top: 0,
right: 0,
transform: 'translateX(100%)'
},
'&[data-popper-placement*="right"]::before': {
top: 0,
left: 0,
transform: 'translateX(-100%)'
},
'&[data-popper-placement*="top"]::before': {
bottom: 0,
left: 0,
transform: 'translateY(100%)'
}
});
});
const TooltipArrow = styled('span', {
name: 'JoyTooltip',
slot: 'Arrow',
overridesResolver: (props, styles) => styles.arrow
})(({
theme,
ownerState
}) => {
var _theme$variants2, _variantStyle$backgro, _variantStyle$backgro2;
const variantStyle = (_theme$variants2 = theme.variants[ownerState.variant]) == null ? void 0 : _theme$variants2[ownerState.color];
return {
'--unstable_Tooltip-arrowRotation': 0,
width: 'var(--Tooltip-arrowSize)',
height: 'var(--Tooltip-arrowSize)',
boxSizing: 'border-box',
// use pseudo element because Popper controls the `transform` property of the arrow.
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
width: 0,
height: 0,
border: 'calc(var(--Tooltip-arrowSize) / 2) solid',
borderLeftColor: 'transparent',
borderBottomColor: 'transparent',
borderTopColor: (_variantStyle$backgro = variantStyle == null ? void 0 : variantStyle.backgroundColor) != null ? _variantStyle$backgro : theme.vars.palette.background.surface,
borderRightColor: (_variantStyle$backgro2 = variantStyle == null ? void 0 : variantStyle.backgroundColor) != null ? _variantStyle$backgro2 : theme.vars.palette.background.surface,
borderRadius: `0px 2px 0px 0px`,
boxShadow: `var(--variant-borderWidth, 0px) calc(-1 * var(--variant-borderWidth, 0px)) 0px 0px ${variantStyle.borderColor}`,
transformOrigin: 'center center',
transform: 'rotate(calc(-45deg + 90deg * var(--unstable_Tooltip-arrowRotation)))'
},
'[data-popper-placement*="bottom"] &': {
top: 'calc(0.5px + var(--Tooltip-arrowSize) * -1 / 2)' // 0.5px is for perfect overlap with the Tooltip
},
'[data-popper-placement*="top"] &': {
'--unstable_Tooltip-arrowRotation': 2,
bottom: 'calc(0.5px + var(--Tooltip-arrowSize) * -1 / 2)'
},
'[data-popper-placement*="left"] &': {
'--unstable_Tooltip-arrowRotation': 1,
right: 'calc(0.5px + var(--Tooltip-arrowSize) * -1 / 2)'
},
'[data-popper-placement*="right"] &': {
'--unstable_Tooltip-arrowRotation': 3,
left: 'calc(0.5px + var(--Tooltip-arrowSize) * -1 / 2)'
}
};
});
let hystersisOpen = false;
const hystersisTimer = new Timeout();
let cursorPosition = {
x: 0,
y: 0
};
export function testReset() {
hystersisOpen = false;
hystersisTimer.clear();
}
function composeMouseEventHandler(handler, eventHandler) {
return event => {
if (eventHandler) {
eventHandler(event);
}
handler(event);
};
}
function composeFocusEventHandler(handler, eventHandler) {
return (event, ...params) => {
if (eventHandler) {
eventHandler(event, ...params);
}
handler(event, ...params);
};
}
/**
*
* Demos:
*
* - [Tooltip](https://mui.com/joy-ui/react-tooltip/)
*
* API:
*
* - [Tooltip API](https://mui.com/joy-ui/api/tooltip/)
*/
const Tooltip = /*#__PURE__*/React.forwardRef(function Tooltip(inProps, ref) {
var _props$slots;
const props = useThemeProps({
props: inProps,
name: 'JoyTooltip'
});
const {
children,
className,
component,
arrow = false,
describeChild = false,
disableFocusListener = false,
disableHoverListener = false,
disableInteractive: disableInteractiveProp = false,
disableTouchListener = false,
enterDelay = 100,
enterNextDelay = 0,
enterTouchDelay = 700,
followCursor = false,
id: idProp,
leaveDelay = 0,
leaveTouchDelay = 1500,
onClose,
onOpen,
open: openProp,
disablePortal,
direction,
keepMounted,
modifiers: modifiersProp,
placement = 'bottom',
title,
color = 'neutral',
variant = 'solid',
size = 'md',
slots = {},
slotProps = {}
} = props,
other = _objectWithoutPropertiesLoose(props, _excluded);
const [childNode, setChildNode] = React.useState();
const [arrowRef, setArrowRef] = React.useState(null);
const ignoreNonTouchEvents = React.useRef(false);
const disableInteractive = disableInteractiveProp || followCursor;
const closeTimer = useTimeout();
const enterTimer = useTimeout();
const leaveTimer = useTimeout();
const touchTimer = useTimeout();
const [openState, setOpenState] = useControlled({
controlled: openProp,
default: false,
name: 'Tooltip',
state: 'open'
});
let open = openState;
const id = useId(idProp);
const prevUserSelect = React.useRef();
const stopTouchInteraction = useEventCallback(() => {
if (prevUserSelect.current !== undefined) {
document.body.style.WebkitUserSelect = prevUserSelect.current;
prevUserSelect.current = undefined;
}
touchTimer.clear();
});
React.useEffect(() => stopTouchInteraction, [stopTouchInteraction]);
const handleOpen = event => {
hystersisTimer.clear();
hystersisOpen = true;
// The mouseover event will trigger for every nested element in the tooltip.
// We can skip rerendering when the tooltip is already open.
// We are using the mouseover event instead of the mouseenter event to fix a hide/show issue.
setOpenState(true);
if (onOpen && !open) {
onOpen(event);
}
};
const handleClose = useEventCallback(event => {
hystersisTimer.start(800 + leaveDelay, () => {
hystersisOpen = false;
});
setOpenState(false);
if (onClose && open) {
onClose(event);
}
closeTimer.start(150, () => {
ignoreNonTouchEvents.current = false;
});
});
const handleMouseOver = event => {
if (ignoreNonTouchEvents.current && event.type !== 'touchstart') {
return;
}
// Remove the title ahead of time.
// We don't want to wait for the next render commit.
// We would risk displaying two tooltips at the same time (native + this one).
if (childNode) {
childNode.removeAttribute('title');
}
enterTimer.clear();
leaveTimer.clear();
if (enterDelay || hystersisOpen && enterNextDelay) {
enterTimer.start(hystersisOpen ? enterNextDelay : enterDelay, () => {
handleOpen(event);
});
} else {
handleOpen(event);
}
};
const handleMouseLeave = event => {
enterTimer.clear();
leaveTimer.start(leaveDelay, () => {
handleClose(event);
});
};
const {
isFocusVisibleRef,
onBlur: handleBlurVisible,
onFocus: handleFocusVisible,
ref: focusVisibleRef
} = useIsFocusVisible();
// We don't necessarily care about the focusVisible state (which is safe to access via ref anyway).
// We just need to re-render the Tooltip if the focus-visible state changes.
const [, setChildIsFocusVisible] = React.useState(false);
const handleBlur = event => {
handleBlurVisible(event);
if (isFocusVisibleRef.current === false) {
setChildIsFocusVisible(false);
handleMouseLeave(event);
}
};
const handleFocus = event => {
// Workaround for https://github.com/facebook/react/issues/7769
// The autoFocus of React might trigger the event before the componentDidMount.
// We need to account for this eventuality.
if (!childNode) {
setChildNode(event.currentTarget);
}
handleFocusVisible(event);
if (isFocusVisibleRef.current === true) {
setChildIsFocusVisible(true);
handleMouseOver(event);
}
};
const detectTouchStart = event => {
ignoreNonTouchEvents.current = true;
const childrenProps = children.props;
if (childrenProps.onTouchStart) {
childrenProps.onTouchStart(event);
}
};
const handleTouchStart = event => {
detectTouchStart(event);
leaveTimer.clear();
closeTimer.clear();
stopTouchInteraction();
prevUserSelect.current = document.body.style.WebkitUserSelect;
// Prevent iOS text selection on long-tap.
document.body.style.WebkitUserSelect = 'none';
touchTimer.start(enterTouchDelay, () => {
document.body.style.WebkitUserSelect = prevUserSelect.current;
handleMouseOver(event);
});
};
const handleTouchEnd = event => {
if (children.props.onTouchEnd) {
children.props.onTouchEnd(event);
}
stopTouchInteraction();
leaveTimer.start(leaveTouchDelay, () => {
handleClose(event);
});
};
React.useEffect(() => {
if (!open) {
return undefined;
}
function handleKeyDown(nativeEvent) {
// IE11, Edge (prior to using Bink?) use 'Esc'
if (nativeEvent.key === 'Escape' || nativeEvent.key === 'Esc') {
handleClose(nativeEvent);
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleClose, open]);
const handleUseRef = useForkRef(setChildNode, ref);
const handleFocusRef = useForkRef(focusVisibleRef, handleUseRef);
const handleRef = useForkRef(children.ref, handleFocusRef);
// There is no point in displaying an empty tooltip.
if (typeof title !== 'number' && !title) {
open = false;
}
const popperRef = React.useRef(null);
const handleMouseMove = event => {
const childrenProps = children.props;
if (childrenProps.onMouseMove) {
childrenProps.onMouseMove(event);
}
cursorPosition = {
x: event.clientX,
y: event.clientY
};
if (popperRef.current) {
popperRef.current.update();
}
};
const nameOrDescProps = {};
const titleIsString = typeof title === 'string';
if (describeChild) {
nameOrDescProps.title = !open && titleIsString && !disableHoverListener ? title : null;
nameOrDescProps['aria-describedby'] = open ? id : null;
} else {
nameOrDescProps['aria-label'] = titleIsString ? title : null;
nameOrDescProps['aria-labelledby'] = open && !titleIsString ? id : null;
}
const childrenProps = _extends({}, nameOrDescProps, other, {
component
}, children.props, {
className: clsx(className, children.props.className),
onTouchStart: detectTouchStart,
ref: handleRef
}, followCursor ? {
onMouseMove: handleMouseMove
} : {});
const interactiveWrapperListeners = {};
if (!disableTouchListener) {
childrenProps.onTouchStart = handleTouchStart;
childrenProps.onTouchEnd = handleTouchEnd;
}
if (!disableHoverListener) {
childrenProps.onMouseOver = composeMouseEventHandler(handleMouseOver, childrenProps.onMouseOver);
childrenProps.onMouseLeave = composeMouseEventHandler(handleMouseLeave, childrenProps.onMouseLeave);
if (!disableInteractive) {
interactiveWrapperListeners.onMouseOver = handleMouseOver;
interactiveWrapperListeners.onMouseLeave = handleMouseLeave;
}
}
if (!disableFocusListener) {
childrenProps.onFocus = composeFocusEventHandler(handleFocus, childrenProps.onFocus);
childrenProps.onBlur = composeFocusEventHandler(handleBlur, childrenProps.onBlur);
if (!disableInteractive) {
interactiveWrapperListeners.onFocus = handleFocus;
interactiveWrapperListeners.onBlur = handleBlur;
}
}
const ownerState = _extends({}, props, {
arrow,
disableInteractive,
placement,
touch: ignoreNonTouchEvents.current,
color,
variant,
size
});
const classes = useUtilityClasses(ownerState);
const externalForwardedProps = _extends({}, other, {
component,
slots,
slotProps
});
const modifiers = React.useMemo(() => [{
name: 'arrow',
enabled: Boolean(arrowRef),
options: {
element: arrowRef,
// https://popper.js.org/docs/v2/modifiers/arrow/#padding
// make the arrow looks nice with the Tooltip's border radius
padding: 6
}
}, {
name: 'offset',
options: {
offset: [0, 10]
}
}, ...(modifiersProp || [])], [arrowRef, modifiersProp]);
const [SlotRoot, rootProps] = useSlot('root', {
additionalProps: _extends({
id,
popperRef,
placement,
anchorEl: followCursor ? {
getBoundingClientRect: () => ({
top: cursorPosition.y,
left: cursorPosition.x,
right: cursorPosition.x,
bottom: cursorPosition.y,
width: 0,
height: 0
})
} : childNode,
open: childNode ? open : false,
disablePortal,
keepMounted,
direction,
modifiers
}, interactiveWrapperListeners),
ref: null,
className: classes.root,
elementType: TooltipRoot,
externalForwardedProps,
ownerState
});
const [SlotArrow, arrowProps] = useSlot('arrow', {
ref: setArrowRef,
className: classes.arrow,
elementType: TooltipArrow,
externalForwardedProps,
ownerState
});
return /*#__PURE__*/_jsxs(React.Fragment, {
children: [/*#__PURE__*/React.isValidElement(children) && /*#__PURE__*/React.cloneElement(children, childrenProps), /*#__PURE__*/_jsxs(SlotRoot, _extends({}, rootProps, !((_props$slots = props.slots) != null && _props$slots.root) && {
as: Popper,
slots: {
root: component || 'div'
}
}, {
children: [title, arrow ? /*#__PURE__*/_jsx(SlotArrow, _extends({}, arrowProps)) : null]
}))]
});
});
process.env.NODE_ENV !== "production" ? Tooltip.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* If `true`, adds an arrow to the tooltip.
* @default false
*/
arrow: PropTypes.bool,
/**
* Tooltip reference element.
*/
children: PropTypes.element.isRequired,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The color of the component. It supports those theme colors that make sense for this component.
* @default 'neutral'
*/
color: PropTypes.oneOf(['danger', 'neutral', 'primary', 'success', 'warning']),
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
*/
component: PropTypes.elementType,
/**
* Set to `true` if the `title` acts as an accessible description.
* By default the `title` acts as an accessible label for the child.
* @default false
*/
describeChild: PropTypes.bool,
/**
* Direction of the text.
* @default 'ltr'
*/
direction: PropTypes.oneOf(['ltr', 'rtl']),
/**
* Do not respond to focus-visible events.
* @default false
*/
disableFocusListener: PropTypes.bool,
/**
* Do not respond to hover events.
* @default false
*/
disableHoverListener: PropTypes.bool,
/**
* Makes a tooltip not interactive, i.e. it will close when the user
* hovers over the tooltip before the `leaveDelay` is expired.
* @default false
*/
disableInteractive: PropTypes.bool,
/**
* The `children` will be under the DOM hierarchy of the parent component.
* @default false
*/
disablePortal: PropTypes.bool,
/**
* Do not respond to long press touch events.
* @default false
*/
disableTouchListener: PropTypes.bool,
/**
* The number of milliseconds to wait before showing the tooltip.
* This prop won't impact the enter touch delay (`enterTouchDelay`).
* @default 100
*/
enterDelay: PropTypes.number,
/**
* The number of milliseconds to wait before showing the tooltip when one was already recently opened.
* @default 0
*/
enterNextDelay: PropTypes.number,
/**
* The number of milliseconds a user must touch the element before showing the tooltip.
* @default 700
*/
enterTouchDelay: PropTypes.number,
/**
* If `true`, the tooltip follow the cursor over the wrapped element.
* @default false
*/
followCursor: PropTypes.bool,
/**
* This prop is used to help implement the accessibility logic.
* If you don't provide this prop. It falls back to a randomly generated id.
*/
id: PropTypes.string,
/**
* Always keep the children in the DOM.
* This prop can be useful in SEO situation or
* when you want to maximize the responsiveness of the Popper.
* @default false
*/
keepMounted: PropTypes.bool,
/**
* The number of milliseconds to wait before hiding the tooltip.
* This prop won't impact the leave touch delay (`leaveTouchDelay`).
* @default 0
*/
leaveDelay: PropTypes.number,
/**
* The number of milliseconds after the user stops touching an element before hiding the tooltip.
* @default 1500
*/
leaveTouchDelay: PropTypes.number,
/**
* Popper.js is based on a "plugin-like" architecture,
* most of its features are fully encapsulated "modifiers".
*
* A modifier is a function that is called each time Popper.js needs to
* compute the position of the popper.
* For this reason, modifiers should be very performant to avoid bottlenecks.
* To learn how to create a modifier, [read the modifiers documentation](https://popper.js.org/docs/v2/modifiers/).
*/
modifiers: PropTypes.arrayOf(PropTypes.shape({
data: PropTypes.object,
effect: PropTypes.func,
enabled: PropTypes.bool,
fn: PropTypes.func,
name: PropTypes.any,
options: PropTypes.object,
phase: PropTypes.oneOf(['afterMain', 'afterRead', 'afterWrite', 'beforeMain', 'beforeRead', 'beforeWrite', 'main', 'read', 'write']),
requires: PropTypes.arrayOf(PropTypes.string),
requiresIfExists: PropTypes.arrayOf(PropTypes.string)
})),
/**
* Callback fired when the component requests to be closed.
*
* @param {React.SyntheticEvent} event The event source of the callback.
*/
onClose: PropTypes.func,
/**
* Callback fired when the component requests to be open.
*
* @param {React.SyntheticEvent} event The event source of the callback.
*/
onOpen: PropTypes.func,
/**
* If `true`, the component is shown.
*/
open: PropTypes.bool,
/**
* Tooltip placement.
* @default 'bottom'
*/
placement: PropTypes.oneOf(['bottom-end', 'bottom-start', 'bottom', 'left-end', 'left-start', 'left', 'right-end', 'right-start', 'right', 'top-end', 'top-start', 'top']),
/**
* The size of the component.
* @default 'md'
*/
size: PropTypes.oneOf(['sm', 'md', 'lg']),
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
arrow: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
arrow: PropTypes.elementType,
root: PropTypes.elementType
}),
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object]),
/**
* Tooltip title. Zero-length titles string, undefined, null and false are never displayed.
*/
title: PropTypes.node,
/**
* The [global variant](https://mui.com/joy-ui/main-features/global-variants/) to use.
* @default 'solid'
*/
variant: PropTypes.oneOf(['outlined', 'plain', 'soft', 'solid'])
} : void 0;
export default Tooltip;