UNPKG

@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
'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;