diff --git a/docs/pages/api-docs/tooltip.json b/docs/pages/api-docs/tooltip.json index 5396fc2ffba5fc..82519ec2cfa8e9 100644 --- a/docs/pages/api-docs/tooltip.json +++ b/docs/pages/api-docs/tooltip.json @@ -28,6 +28,7 @@ }, "PopperComponent": { "type": { "name": "elementType" }, "default": "Popper" }, "PopperProps": { "type": { "name": "object" }, "default": "{}" }, + "sx": { "type": { "name": "object" } }, "TransitionComponent": { "type": { "name": "elementType" }, "default": "Grow" }, "TransitionProps": { "type": { "name": "object" } } }, @@ -54,6 +55,6 @@ "filename": "/packages/material-ui/src/Tooltip/Tooltip.js", "inheritance": null, "demos": "", - "styledComponent": false, + "styledComponent": true, "cssComponent": false } diff --git a/docs/translations/api-docs/tooltip/tooltip.json b/docs/translations/api-docs/tooltip/tooltip.json index 97a1810bb61c72..47a96ff44a6f61 100644 --- a/docs/translations/api-docs/tooltip/tooltip.json +++ b/docs/translations/api-docs/tooltip/tooltip.json @@ -22,6 +22,7 @@ "placement": "Tooltip placement.", "PopperComponent": "The component used for the popper.", "PopperProps": "Props applied to the Popper element.", + "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", "title": "Tooltip title. Zero-length titles string are never displayed.", "TransitionComponent": "The component used for the transition. Follow this guide to learn more about the requirements for this component.", "TransitionProps": "Props applied to the transition element. By default, the element is based on this Transition component." diff --git a/packages/material-ui/src/Tooltip/Tooltip.d.ts b/packages/material-ui/src/Tooltip/Tooltip.d.ts index f17846cf2028d3..4c605603cff377 100644 --- a/packages/material-ui/src/Tooltip/Tooltip.d.ts +++ b/packages/material-ui/src/Tooltip/Tooltip.d.ts @@ -1,5 +1,6 @@ import * as React from 'react'; -import { InternalStandardProps as StandardProps } from '..'; +import { SxProps } from '@material-ui/system'; +import { InternalStandardProps as StandardProps, Theme } from '..'; import { TransitionProps } from '../transitions/transition'; import { PopperProps } from '../Popper/Popper'; @@ -147,6 +148,10 @@ export interface TooltipProps extends StandardProps; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; /** * Tooltip title. Zero-length titles string are never displayed. */ diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js index a24c6c90bc382b..aac7d5d23165b0 100644 --- a/packages/material-ui/src/Tooltip/Tooltip.js +++ b/packages/material-ui/src/Tooltip/Tooltip.js @@ -1,9 +1,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { elementAcceptingRef } from '@material-ui/utils'; +import { deepmerge, elementAcceptingRef } from '@material-ui/utils'; +import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; import { alpha } from '../styles/colorManipulator'; -import withStyles from '../styles/withStyles'; +import experimentalStyled from '../styles/experimentalStyled'; +import useThemeProps from '../styles/useThemeProps'; import capitalize from '../utils/capitalize'; import Grow from '../Grow'; import Popper from '../Popper'; @@ -13,14 +15,64 @@ import useId from '../utils/useId'; import useIsFocusVisible from '../utils/useIsFocusVisible'; import useControlled from '../utils/useControlled'; import useTheme from '../styles/useTheme'; +import tooltipClasses, { getTooltipUtilityClass } from './tooltipClasses'; function round(value) { return Math.round(value * 1e5) / 1e5; } -function arrowGenerator() { - return { - '&[data-popper-placement*="bottom"] $arrow': { +const overridesResolver = (props, styles) => { + const { styleProps } = props; + + return deepmerge(styles.popper || {}, { + ...(!styleProps.disableInteractive && styles.popperInteractive), + ...(styleProps.arrow && styles.popperArrow), + [`& .${tooltipClasses.tooltip}`]: styles.tooltip, + ...(styleProps.arrow && { [`& .${tooltipClasses.tooltip}`]: styles.tooltipArrow }), + [`& .${tooltipClasses.arrow}`]: styles.arrow, + ...(styleProps.touch && { [`& .${tooltipClasses.tooltip}`]: styles.touch }), + [`& .${tooltipClasses.tooltip}`]: styles[ + `tooltipPlacement${capitalize(styleProps.placement.split('-')[0])}` + ], + }); +}; + +const useUtilityClasses = (styleProps) => { + const { classes, disableInteractive, arrow, touch, placement } = styleProps; + + 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 = experimentalStyled( + Popper, + {}, + { + name: 'MuiTooltip', + slot: 'Popper', + overridesResolver, + }, +)(({ theme, styleProps }) => ({ + /* Styles applied to the Popper element. */ + zIndex: theme.zIndex.tooltip, + pointerEvents: 'none', // disable jss-rtl plugin + /* Styles applied to the Popper component unless `disableInteractive={true}`. */ + ...(!styleProps.disableInteractive && { + pointerEvents: 'auto', + }), + /* Styles applied to the Popper element if `arrow={true}`. */ + ...(styleProps.arrow && { + [`&[data-popper-placement*="bottom"] .${tooltipClasses.arrow}`]: { top: 0, left: 0, marginTop: '-0.71em', @@ -28,7 +80,7 @@ function arrowGenerator() { transformOrigin: '0 100%', }, }, - '&[data-popper-placement*="top"] $arrow': { + [`&[data-popper-placement*="top"] .${tooltipClasses.arrow}`]: { bottom: 0, left: 0, marginBottom: '-0.71em', @@ -36,7 +88,7 @@ function arrowGenerator() { transformOrigin: '100% 0', }, }, - '&[data-popper-placement*="right"] $arrow': { + [`&[data-popper-placement*="right"] .${tooltipClasses.arrow}`]: { left: 0, marginLeft: '-0.71em', height: '1em', @@ -45,7 +97,7 @@ function arrowGenerator() { transformOrigin: '100% 100%', }, }, - '&[data-popper-placement*="left"] $arrow': { + [`&[data-popper-placement*="left"] .${tooltipClasses.arrow}`]: { right: 0, marginRight: '-0.71em', height: '1em', @@ -54,97 +106,101 @@ function arrowGenerator() { transformOrigin: '0 0', }, }, - }; -} - -export const styles = (theme) => ({ - /* Styles applied to the Popper component. */ - popper: { - zIndex: theme.zIndex.tooltip, - pointerEvents: 'none', // disable jss-rtl plugin - }, - /* Styles applied to the Popper component unless `disableInteractive={true}`. */ - popperInteractive: { - pointerEvents: 'auto', + }), +})); + +const TooltipLabel = experimentalStyled( + 'div', + {}, + { + name: 'MuiTooltip', + slot: 'Tooltip', }, - /* Styles applied to the Popper component if `arrow={true}`. */ - popperArrow: arrowGenerator(), +)(({ theme, styleProps }) => ({ /* Styles applied to the tooltip (label wrapper) element. */ - tooltip: { - 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, - }, + 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, /* Styles applied to the tooltip (label wrapper) element if `arrow={true}`. */ - tooltipArrow: { + ...(styleProps.arrow && { position: 'relative', margin: 0, - }, - /* Styles applied to the arrow element. */ - arrow: { - 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)', - }, - }, + }), /* Styles applied to the tooltip (label wrapper) element if the tooltip is opened by touch. */ - touch: { + ...(styleProps.touch && { padding: '8px 16px', fontSize: theme.typography.pxToRem(14), lineHeight: `${round(16 / 14)}em`, fontWeight: theme.typography.fontWeightRegular, - }, + }), /* Styles applied to the tooltip (label wrapper) element if `placement` contains "left". */ - tooltipPlacementLeft: { + ...(styleProps.placement.split('-')[0] === 'left' && { transformOrigin: 'right center', marginRight: '24px', [theme.breakpoints.up('sm')]: { marginRight: '14px', }, - }, + }), /* Styles applied to the tooltip (label wrapper) element if `placement` contains "right". */ - tooltipPlacementRight: { + ...(styleProps.placement.split('-')[0] === 'right' && { transformOrigin: 'left center', marginLeft: '24px', [theme.breakpoints.up('sm')]: { marginLeft: '14px', }, - }, + }), /* Styles applied to the tooltip (label wrapper) element if `placement` contains "top". */ - tooltipPlacementTop: { + ...(styleProps.placement.split('-')[0] === 'top' && { transformOrigin: 'center bottom', marginBottom: '24px', [theme.breakpoints.up('sm')]: { marginBottom: '14px', }, - }, + }), /* Styles applied to the tooltip (label wrapper) element if `placement` contains "bottom". */ - tooltipPlacementBottom: { + ...(styleProps.placement.split('-')[0] === 'bottom' && { transformOrigin: 'center top', marginTop: '24px', [theme.breakpoints.up('sm')]: { marginTop: '14px', }, + }), +})); + +const TooltipArrow = experimentalStyled( + 'span', + {}, + { + name: 'MuiTooltip', + slot: 'Arrow', }, -}); +)(({ theme }) => ({ + /* Styles applied to the arrow element. */ + arrow: { + 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; @@ -163,11 +219,11 @@ function composeEventHandler(handler, eventHandler) { }; } -const Tooltip = React.forwardRef(function Tooltip(props, ref) { +const Tooltip = React.forwardRef(function Tooltip(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiTooltip' }); const { arrow = false, children, - classes, describeChild = false, disableFocusListener = false, disableHoverListener = false, @@ -548,14 +604,23 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) { }; }, [arrowRef, PopperProps]); + const styleProps = { + ...props, + arrow, + disableInteractive, + PopperComponent, + placement, + touch: ignoreNonTouchEvents.current, + }; + + const classes = useUtilityClasses(styleProps); + return ( {React.cloneElement(children, childrenProps)} - - {({ placement: placementInner, TransitionProps: TransitionPropsInner }) => ( + {({ TransitionProps: TransitionPropsInner }) => ( -
+ {title} - {arrow ? : null} -
+ {arrow ? ( + + ) : null} +
)} -
+
); }); @@ -735,6 +794,10 @@ Tooltip.propTypes = { * @default {} */ PopperProps: PropTypes.object, + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.object, /** * Tooltip title. Zero-length titles string are never displayed. */ @@ -752,4 +815,4 @@ Tooltip.propTypes = { TransitionProps: PropTypes.object, }; -export default withStyles(styles, { name: 'MuiTooltip', flip: false })(Tooltip); +export default Tooltip; diff --git a/packages/material-ui/src/Tooltip/Tooltip.test.js b/packages/material-ui/src/Tooltip/Tooltip.test.js index 552e91e9d56b0d..db0d2617e17211 100644 --- a/packages/material-ui/src/Tooltip/Tooltip.test.js +++ b/packages/material-ui/src/Tooltip/Tooltip.test.js @@ -2,9 +2,8 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy, useFakeTimers } from 'sinon'; import { - getClasses, createMount, - describeConformance, + describeConformanceV5, act, createClientRender, fireEvent, @@ -16,6 +15,7 @@ import { import { camelCase } from 'lodash/string'; import Tooltip, { testReset } from './Tooltip'; import Input from '../Input'; +import classes from './tooltipClasses'; async function raf() { return new Promise((resolve) => { @@ -47,18 +47,9 @@ describe('', () => { }); const mount = createMount({ strict: true }); - let classes; const render = createClientRender(); - before(() => { - classes = getClasses( - - - , - ); - }); - - describeConformance( + describeConformanceV5( , @@ -66,7 +57,10 @@ describe('', () => { classes, inheritComponent: 'button', mount, + muiName: 'MuiTooltip', refInstanceof: window.HTMLButtonElement, + testVariantProps: { arrow: true }, + testDeepOverrides: { slotName: 'tooltip', slotClassName: classes.tooltip }, skip: [ 'componentProp', // react-transition-group issue diff --git a/packages/material-ui/src/Tooltip/index.d.ts b/packages/material-ui/src/Tooltip/index.d.ts index de73a9e2b6514e..c44b819bdc9bff 100644 --- a/packages/material-ui/src/Tooltip/index.d.ts +++ b/packages/material-ui/src/Tooltip/index.d.ts @@ -1,2 +1,5 @@ export { default } from './Tooltip'; export * from './Tooltip'; + +export { default as tooltipClasses } from './tooltipClasses'; +export * from './tooltipClasses'; diff --git a/packages/material-ui/src/Tooltip/index.js b/packages/material-ui/src/Tooltip/index.js index cdc0fab160f86c..41217639d8cc24 100644 --- a/packages/material-ui/src/Tooltip/index.js +++ b/packages/material-ui/src/Tooltip/index.js @@ -1 +1,4 @@ export { default } from './Tooltip'; + +export { default as tooltipClasses } from './tooltipClasses'; +export * from './tooltipClasses'; diff --git a/packages/material-ui/src/Tooltip/tooltipClasses.d.ts b/packages/material-ui/src/Tooltip/tooltipClasses.d.ts new file mode 100644 index 00000000000000..b463645331760c --- /dev/null +++ b/packages/material-ui/src/Tooltip/tooltipClasses.d.ts @@ -0,0 +1,19 @@ +export interface TooltipClasses { + popper: string; + popperInteractive: string; + popperArrow: string; + tooltip: string; + tooltipArrow: string; + touch: string; + tooltipPlacementLeft: string; + tooltipPlacementRight: string; + tooltipPlacementTop: string; + tooltipPlacementBottom: string; + arrow: string; +} + +declare const tooltipClasses: TooltipClasses; + +export function getTooltipUtilityClass(slot: string): string; + +export default tooltipClasses; diff --git a/packages/material-ui/src/Tooltip/tooltipClasses.js b/packages/material-ui/src/Tooltip/tooltipClasses.js new file mode 100644 index 00000000000000..a381f9daa76b52 --- /dev/null +++ b/packages/material-ui/src/Tooltip/tooltipClasses.js @@ -0,0 +1,21 @@ +import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; + +export function getTooltipUtilityClass(slot) { + return generateUtilityClass('MuiTooltip', slot); +} + +const tooltipClasses = generateUtilityClasses('MuiTooltip', [ + 'popper', + 'popperInteractive', + 'popperArrow', + 'tooltip', + 'tooltipArrow', + 'touch', + 'tooltipPlacementLeft', + 'tooltipPlacementRight', + 'tooltipPlacementTop', + 'tooltipPlacementBottom', + 'arrow', +]); + +export default tooltipClasses;