UNPKG

@mui/material

Version:

Quickly build beautiful React apps. MUI is a simple and customizable component library to build faster, beautiful, and more accessible React applications. Follow your own design system, or start with Material Design.

821 lines (727 loc) 25.2 kB
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; import _extends from "@babel/runtime/helpers/esm/extends"; const _excluded = ["arrow", "children", "classes", "components", "componentsProps", "describeChild", "disableFocusListener", "disableHoverListener", "disableInteractive", "disableTouchListener", "enterDelay", "enterNextDelay", "enterTouchDelay", "followCursor", "id", "leaveDelay", "leaveTouchDelay", "onClose", "onOpen", "open", "placement", "PopperComponent", "PopperProps", "title", "TransitionComponent", "TransitionProps"]; import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { elementAcceptingRef } from '@mui/utils'; import { unstable_composeClasses as composeClasses, appendOwnerState } from '@mui/base'; import { alpha } from '@mui/system'; import styled from '../styles/styled'; import useTheme from '../styles/useTheme'; import useThemeProps from '../styles/useThemeProps'; import capitalize from '../utils/capitalize'; import Grow from '../Grow'; import Popper from '../Popper'; import useEventCallback from '../utils/useEventCallback'; import useForkRef from '../utils/useForkRef'; import useId from '../utils/useId'; import useIsFocusVisible from '../utils/useIsFocusVisible'; import useControlled from '../utils/useControlled'; import tooltipClasses, { getTooltipUtilityClass } from './tooltipClasses'; import { jsx as _jsx } from "react/jsx-runtime"; import { jsxs as _jsxs } from "react/jsx-runtime"; function round(value) { return Math.round(value * 1e5) / 1e5; } const useUtilityClasses = ownerState => { const { classes, disableInteractive, arrow, touch, placement } = ownerState; const slots = { popper: ['popper', !disableInteractive && 'popperInteractive', arrow && 'popperArrow'], tooltip: ['tooltip', arrow && 'tooltipArrow', touch && 'touch', `tooltipPlacement${capitalize(placement.split('-')[0])}`], arrow: ['arrow'] }; return composeClasses(slots, getTooltipUtilityClass, classes); }; const TooltipPopper = styled(Popper, { name: 'MuiTooltip', slot: 'Popper', overridesResolver: (props, styles) => { const { ownerState } = props; return [styles.popper, !ownerState.disableInteractive && styles.popperInteractive, ownerState.arrow && styles.popperArrow, !ownerState.open && styles.popperClose]; } })(({ theme, ownerState, open }) => _extends({ zIndex: theme.zIndex.tooltip, pointerEvents: 'none' }, !ownerState.disableInteractive && { pointerEvents: 'auto' }, !open && { pointerEvents: 'none' }, ownerState.arrow && { [`&[data-popper-placement*="bottom"] .${tooltipClasses.arrow}`]: { top: 0, marginTop: '-0.71em', '&::before': { transformOrigin: '0 100%' } }, [`&[data-popper-placement*="top"] .${tooltipClasses.arrow}`]: { bottom: 0, marginBottom: '-0.71em', '&::before': { transformOrigin: '100% 0' } }, [`&[data-popper-placement*="right"] .${tooltipClasses.arrow}`]: _extends({}, !ownerState.isRtl ? { left: 0, marginLeft: '-0.71em' } : { right: 0, marginRight: '-0.71em' }, { height: '1em', width: '0.71em', '&::before': { transformOrigin: '100% 100%' } }), [`&[data-popper-placement*="left"] .${tooltipClasses.arrow}`]: _extends({}, !ownerState.isRtl ? { right: 0, marginRight: '-0.71em' } : { left: 0, marginLeft: '-0.71em' }, { height: '1em', width: '0.71em', '&::before': { transformOrigin: '0 0' } }) })); const TooltipTooltip = styled('div', { name: 'MuiTooltip', slot: 'Tooltip', overridesResolver: (props, styles) => { const { ownerState } = props; return [styles.tooltip, ownerState.touch && styles.touch, ownerState.arrow && styles.tooltipArrow, styles[`tooltipPlacement${capitalize(ownerState.placement.split('-')[0])}`]]; } })(({ theme, ownerState }) => _extends({ backgroundColor: alpha(theme.palette.grey[700], 0.92), borderRadius: theme.shape.borderRadius, color: theme.palette.common.white, fontFamily: theme.typography.fontFamily, padding: '4px 8px', fontSize: theme.typography.pxToRem(11), maxWidth: 300, margin: 2, wordWrap: 'break-word', fontWeight: theme.typography.fontWeightMedium }, ownerState.arrow && { position: 'relative', margin: 0 }, ownerState.touch && { padding: '8px 16px', fontSize: theme.typography.pxToRem(14), lineHeight: `${round(16 / 14)}em`, fontWeight: theme.typography.fontWeightRegular }, { [`.${tooltipClasses.popper}[data-popper-placement*="left"] &`]: _extends({ transformOrigin: 'right center' }, !ownerState.isRtl ? _extends({ marginRight: '14px' }, ownerState.touch && { marginRight: '24px' }) : _extends({ marginLeft: '14px' }, ownerState.touch && { marginLeft: '24px' })), [`.${tooltipClasses.popper}[data-popper-placement*="right"] &`]: _extends({ transformOrigin: 'left center' }, !ownerState.isRtl ? _extends({ marginLeft: '14px' }, ownerState.touch && { marginLeft: '24px' }) : _extends({ marginRight: '14px' }, ownerState.touch && { marginRight: '24px' })), [`.${tooltipClasses.popper}[data-popper-placement*="top"] &`]: _extends({ transformOrigin: 'center bottom', marginBottom: '14px' }, ownerState.touch && { marginBottom: '24px' }), [`.${tooltipClasses.popper}[data-popper-placement*="bottom"] &`]: _extends({ transformOrigin: 'center top', marginTop: '14px' }, ownerState.touch && { marginTop: '24px' }) })); const TooltipArrow = styled('span', { name: 'MuiTooltip', slot: 'Arrow', overridesResolver: (props, styles) => styles.arrow })(({ theme }) => ({ overflow: 'hidden', position: 'absolute', width: '1em', height: '0.71em' /* = width / sqrt(2) = (length of the hypotenuse) */ , boxSizing: 'border-box', color: alpha(theme.palette.grey[700], 0.9), '&::before': { content: '""', margin: 'auto', display: 'block', width: '100%', height: '100%', backgroundColor: 'currentColor', transform: 'rotate(45deg)' } })); let hystersisOpen = false; let hystersisTimer = null; export function testReset() { hystersisOpen = false; clearTimeout(hystersisTimer); } function composeEventHandler(handler, eventHandler) { return event => { if (eventHandler) { eventHandler(event); } handler(event); }; } // TODO v6: Remove PopperComponent, PopperProps, TransitionComponent and TransitionProps. const Tooltip = /*#__PURE__*/React.forwardRef(function Tooltip(inProps, ref) { const props = useThemeProps({ props: inProps, name: 'MuiTooltip' }); const { arrow = false, children, components = {}, componentsProps = {}, 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, placement = 'bottom', PopperComponent: PopperComponentProp, PopperProps = {}, title, TransitionComponent: TransitionComponentProp = Grow, TransitionProps } = props, other = _objectWithoutPropertiesLoose(props, _excluded); const theme = useTheme(); const isRtl = theme.direction === 'rtl'; const [childNode, setChildNode] = React.useState(); const [arrowRef, setArrowRef] = React.useState(null); const ignoreNonTouchEvents = React.useRef(false); const disableInteractive = disableInteractiveProp || followCursor; const closeTimer = React.useRef(); const enterTimer = React.useRef(); const leaveTimer = React.useRef(); const touchTimer = React.useRef(); const [openState, setOpenState] = useControlled({ controlled: openProp, default: false, name: 'Tooltip', state: 'open' }); let open = openState; if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line react-hooks/rules-of-hooks const { current: isControlled } = React.useRef(openProp !== undefined); // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(() => { if (childNode && childNode.disabled && !isControlled && title !== '' && childNode.tagName.toLowerCase() === 'button') { console.error(['MUI: You are providing a disabled `button` child to the Tooltip component.', 'A disabled element does not fire events.', "Tooltip needs to listen to the child element's events to display the title.", '', 'Add a simple wrapper element, such as a `span`.'].join('\n')); } }, [title, childNode, isControlled]); } const id = useId(idProp); const prevUserSelect = React.useRef(); const stopTouchInteraction = React.useCallback(() => { if (prevUserSelect.current !== undefined) { document.body.style.WebkitUserSelect = prevUserSelect.current; prevUserSelect.current = undefined; } clearTimeout(touchTimer.current); }, []); React.useEffect(() => { return () => { clearTimeout(closeTimer.current); clearTimeout(enterTimer.current); clearTimeout(leaveTimer.current); stopTouchInteraction(); }; }, [stopTouchInteraction]); const handleOpen = event => { clearTimeout(hystersisTimer); 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( /** * @param {React.SyntheticEvent | Event} event */ event => { clearTimeout(hystersisTimer); hystersisTimer = setTimeout(() => { hystersisOpen = false; }, 800 + leaveDelay); setOpenState(false); if (onClose && open) { onClose(event); } clearTimeout(closeTimer.current); closeTimer.current = setTimeout(() => { ignoreNonTouchEvents.current = false; }, theme.transitions.duration.shortest); }); const handleEnter = 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'); } clearTimeout(enterTimer.current); clearTimeout(leaveTimer.current); if (enterDelay || hystersisOpen && enterNextDelay) { enterTimer.current = setTimeout(() => { handleOpen(event); }, hystersisOpen ? enterNextDelay : enterDelay); } else { handleOpen(event); } }; const handleLeave = event => { clearTimeout(enterTimer.current); clearTimeout(leaveTimer.current); leaveTimer.current = setTimeout(() => { handleClose(event); }, leaveDelay); }; 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); handleLeave(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); handleEnter(event); } }; const detectTouchStart = event => { ignoreNonTouchEvents.current = true; const childrenProps = children.props; if (childrenProps.onTouchStart) { childrenProps.onTouchStart(event); } }; const handleMouseOver = handleEnter; const handleMouseLeave = handleLeave; const handleTouchStart = event => { detectTouchStart(event); clearTimeout(leaveTimer.current); clearTimeout(closeTimer.current); stopTouchInteraction(); prevUserSelect.current = document.body.style.WebkitUserSelect; // Prevent iOS text selection on long-tap. document.body.style.WebkitUserSelect = 'none'; touchTimer.current = setTimeout(() => { document.body.style.WebkitUserSelect = prevUserSelect.current; handleEnter(event); }, enterTouchDelay); }; const handleTouchEnd = event => { if (children.props.onTouchEnd) { children.props.onTouchEnd(event); } stopTouchInteraction(); clearTimeout(leaveTimer.current); leaveTimer.current = setTimeout(() => { handleClose(event); }, leaveTouchDelay); }; React.useEffect(() => { if (!open) { return undefined; } /** * @param {KeyboardEvent} nativeEvent */ 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 (title === '') { open = false; } const positionRef = React.useRef({ x: 0, y: 0 }); const popperRef = React.useRef(); const handleMouseMove = event => { const childrenProps = children.props; if (childrenProps.onMouseMove) { childrenProps.onMouseMove(event); } positionRef.current = { 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, children.props, { className: clsx(other.className, children.props.className), onTouchStart: detectTouchStart, ref: handleRef }, followCursor ? { onMouseMove: handleMouseMove } : {}); if (process.env.NODE_ENV !== 'production') { childrenProps['data-mui-internal-clone-element'] = true; // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(() => { if (childNode && !childNode.getAttribute('data-mui-internal-clone-element')) { console.error(['MUI: The `children` component of the Tooltip is not forwarding its props correctly.', 'Please make sure that props are spread on the same element that the ref is applied to.'].join('\n')); } }, [childNode]); } const interactiveWrapperListeners = {}; if (!disableTouchListener) { childrenProps.onTouchStart = handleTouchStart; childrenProps.onTouchEnd = handleTouchEnd; } if (!disableHoverListener) { childrenProps.onMouseOver = composeEventHandler(handleMouseOver, childrenProps.onMouseOver); childrenProps.onMouseLeave = composeEventHandler(handleMouseLeave, childrenProps.onMouseLeave); if (!disableInteractive) { interactiveWrapperListeners.onMouseOver = handleMouseOver; interactiveWrapperListeners.onMouseLeave = handleMouseLeave; } } if (!disableFocusListener) { childrenProps.onFocus = composeEventHandler(handleFocus, childrenProps.onFocus); childrenProps.onBlur = composeEventHandler(handleBlur, childrenProps.onBlur); if (!disableInteractive) { interactiveWrapperListeners.onFocus = handleFocus; interactiveWrapperListeners.onBlur = handleBlur; } } if (process.env.NODE_ENV !== 'production') { if (children.props.title) { console.error(['MUI: You have provided a `title` prop to the child of <Tooltip />.', `Remove this title prop \`${children.props.title}\` or the Tooltip component.`].join('\n')); } } const popperOptions = React.useMemo(() => { let tooltipModifiers = [{ name: 'arrow', enabled: Boolean(arrowRef), options: { element: arrowRef, padding: 4 } }]; if (PopperProps.popperOptions?.modifiers) { tooltipModifiers = tooltipModifiers.concat(PopperProps.popperOptions.modifiers); } return _extends({}, PopperProps.popperOptions, { modifiers: tooltipModifiers }); }, [arrowRef, PopperProps]); const ownerState = _extends({}, props, { isRtl, arrow, disableInteractive, placement, PopperComponentProp, touch: ignoreNonTouchEvents.current }); const classes = useUtilityClasses(ownerState); const PopperComponent = components.Popper ?? TooltipPopper; const TransitionComponent = components.Transition ?? TransitionComponentProp ?? Grow; const TooltipComponent = components.Tooltip ?? TooltipTooltip; const ArrowComponent = components.Arrow ?? TooltipArrow; const popperProps = appendOwnerState(PopperComponent, _extends({}, PopperProps, componentsProps.popper), ownerState); const transitionProps = appendOwnerState(TransitionComponent, _extends({}, TransitionProps, componentsProps.transition), ownerState); const tooltipProps = appendOwnerState(TooltipComponent, _extends({}, componentsProps.tooltip), ownerState); const tooltipArrowProps = appendOwnerState(ArrowComponent, _extends({}, componentsProps.arrow), ownerState); return /*#__PURE__*/_jsxs(React.Fragment, { children: [/*#__PURE__*/React.cloneElement(children, childrenProps), /*#__PURE__*/_jsx(PopperComponent, _extends({ as: PopperComponentProp ?? Popper, placement: placement, anchorEl: followCursor ? { getBoundingClientRect: () => ({ top: positionRef.current.y, left: positionRef.current.x, right: positionRef.current.x, bottom: positionRef.current.y, width: 0, height: 0 }) } : childNode, popperRef: popperRef, open: childNode ? open : false, id: id, transition: true }, interactiveWrapperListeners, popperProps, { className: clsx(classes.popper, PopperProps?.className, componentsProps.popper?.className), popperOptions: popperOptions, children: ({ TransitionProps: TransitionPropsInner }) => /*#__PURE__*/_jsx(TransitionComponent, _extends({ timeout: theme.transitions.duration.shorter }, TransitionPropsInner, transitionProps, { children: /*#__PURE__*/_jsxs(TooltipComponent, _extends({}, tooltipProps, { className: clsx(classes.tooltip, componentsProps.tooltip?.className), children: [title, arrow ? /*#__PURE__*/_jsx(ArrowComponent, _extends({}, tooltipArrowProps, { className: clsx(classes.arrow, componentsProps.arrow?.className), ref: setArrowRef })) : 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 d.ts file and run "yarn proptypes" | // ---------------------------------------------------------------------- /** * If `true`, adds an arrow to the tooltip. * @default false */ arrow: PropTypes.bool, /** * Tooltip reference element. */ children: elementAcceptingRef.isRequired, /** * Override or extend the styles applied to the component. */ classes: PropTypes.object, /** * @ignore */ className: PropTypes.string, /** * The components used for each slot inside the Tooltip. * Either a string to use a HTML element or a component. * @default {} */ components: PropTypes.shape({ Arrow: PropTypes.elementType, Popper: PropTypes.elementType, Tooltip: PropTypes.elementType, Transition: PropTypes.elementType }), /** * The props used for each slot inside the Tooltip. * Note that `componentsProps.popper` prop values win over `PopperProps` * and `componentsProps.transition` prop values win over `TransitionProps` if both are applied. * @default {} */ componentsProps: PropTypes.shape({ arrow: PropTypes.object, popper: PropTypes.object, tooltip: PropTypes.object, transition: PropTypes.object }), /** * 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, /** * 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, /** * 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, /** * 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, /** * 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 component used for the popper. * @default Popper */ PopperComponent: PropTypes.elementType, /** * Props applied to the [`Popper`](/api/popper/) element. * @default {} */ PopperProps: PropTypes.object, /** * 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 are never displayed. */ title: PropTypes /* @typescript-to-proptypes-ignore */ .node.isRequired, /** * The component used for the transition. * [Follow this guide](/components/transitions/#transitioncomponent-prop) to learn more about the requirements for this component. * @default Grow */ TransitionComponent: PropTypes.elementType, /** * Props applied to the transition element. * By default, the element is based on this [`Transition`](http://reactcommunity.org/react-transition-group/transition/) component. */ TransitionProps: PropTypes.object } : void 0; export default Tooltip;