Skip to content

Commit

Permalink
[Tooltip] Allow overriding internal components and their props (#28692)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaldudak committed Oct 4, 2021
1 parent 1ed2c4a commit f98d52b
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 52 deletions.
8 changes: 8 additions & 0 deletions docs/pages/api-docs/tooltip.json
Expand Up @@ -4,6 +4,14 @@
"title": { "type": { "name": "node" }, "required": true },
"arrow": { "type": { "name": "bool" } },
"classes": { "type": { "name": "object" } },
"components": {
"type": {
"name": "shape",
"description": "{ Arrow?: elementType, Popper?: elementType, Tooltip?: elementType, Transition?: elementType }"
},
"default": "{}"
},
"componentsProps": { "type": { "name": "object" }, "default": "{}" },
"describeChild": { "type": { "name": "bool" } },
"disableFocusListener": { "type": { "name": "bool" } },
"disableHoverListener": { "type": { "name": "bool" } },
Expand Down
2 changes: 2 additions & 0 deletions docs/translations/api-docs/tooltip/tooltip.json
Expand Up @@ -4,6 +4,8 @@
"arrow": "If <code>true</code>, adds an arrow to the tooltip.",
"children": "Tooltip reference element.<br>⚠️ <a href=\"/guides/composition/#caveat-with-refs\">Needs to be able to hold a ref</a>.",
"classes": "Override or extend the styles applied to the component. See <a href=\"#css\">CSS API</a> below for more details.",
"components": "The components used for each slot inside the Tooltip. Either a string to use a HTML element or a component.",
"componentsProps": "The props used for each slot inside the Tooltip.",
"describeChild": "Set to <code>true</code> if the <code>title</code> acts as an accessible description. By default the <code>title</code> acts as an accessible label for the child.",
"disableFocusListener": "Do not respond to focus-visible events.",
"disableHoverListener": "Do not respond to hover events.",
Expand Down
29 changes: 28 additions & 1 deletion packages/mui-material/src/Tooltip/Tooltip.d.ts
@@ -1,10 +1,12 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { MUIStyledCommonProps, SxProps } from '@mui/system';
import { InternalStandardProps as StandardProps, Theme } from '..';
import { TransitionProps } from '../transitions/transition';
import { PopperProps } from '../Popper/Popper';
import { TooltipClasses } from './tooltipClasses';

export interface TooltipComponentsPropsOverrides {}

export interface TooltipProps extends StandardProps<React.HTMLAttributes<HTMLDivElement>, 'title'> {
/**
* If `true`, adds an arrow to the tooltip.
Expand All @@ -19,6 +21,31 @@ export interface TooltipProps extends StandardProps<React.HTMLAttributes<HTMLDiv
* Override or extend the styles applied to the component.
*/
classes?: Partial<TooltipClasses>;
/**
* The components used for each slot inside the Tooltip.
* Either a string to use a HTML element or a component.
* @default {}
*/
components?: {
Popper?: React.ElementType;
Transition?: React.ElementType;
Tooltip?: React.ElementType;
Arrow?: React.ElementType;
};
/**
* The props used for each slot inside the Tooltip.
* @default {}
*/
componentsProps?: {
popper?: PopperProps & TooltipComponentsPropsOverrides;
transition?: TransitionProps & TooltipComponentsPropsOverrides;
tooltip?: React.HTMLProps<HTMLDivElement> &
MUIStyledCommonProps &
TooltipComponentsPropsOverrides;
arrow?: React.HTMLProps<HTMLSpanElement> &
MUIStyledCommonProps &
TooltipComponentsPropsOverrides;
};
/**
* Set to `true` if the `title` acts as an accessible description.
* By default the `title` acts as an accessible label for the child.
Expand Down
82 changes: 68 additions & 14 deletions packages/mui-material/src/Tooltip/Tooltip.js
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { elementAcceptingRef } from '@mui/utils';
import { unstable_composeClasses as composeClasses } from '@mui/core';
import { unstable_composeClasses as composeClasses, appendOwnerState } from '@mui/core';
import { alpha } from '@mui/system';
import styled from '../styles/styled';
import useTheme from '../styles/useTheme';
Expand Down Expand Up @@ -225,12 +225,15 @@ function composeEventHandler(handler, eventHandler) {
};
}

// TODO (v6) Remove PopperComponent, PopperProps, TransitionComponent and TransitionProps.
const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiTooltip' });
const {
arrow = false,
children,
classes: classesProp,
components = {},
componentsProps = {},
describeChild = false,
disableFocusListener = false,
disableHoverListener = false,
Expand All @@ -247,10 +250,10 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
onOpen,
open: openProp,
placement = 'bottom',
PopperComponent = Popper,
PopperComponent: PopperComponentProp,
PopperProps = {},
title,
TransitionComponent = Grow,
TransitionComponent: TransitionComponentProp = Grow,
TransitionProps,
...other
} = props;
Expand Down Expand Up @@ -615,18 +618,46 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
arrow,
disableInteractive,
placement,
PopperComponent,
PopperComponentProp,
touch: ignoreNonTouchEvents.current,
};

const classes = useUtilityClasses(ownerState);

const PopperComponent = components.Popper ?? TooltipPopper;
const TransitionComponent = TransitionComponentProp ?? components.Transition ?? Grow;
const TooltipComponent = components.Tooltip ?? TooltipTooltip;
const ArrowComponent = components.Arrow ?? TooltipArrow;

const popperProps = appendOwnerState(
PopperComponent,
{ ...PopperProps, ...componentsProps.popper },
ownerState,
);

const transitionProps = appendOwnerState(
TransitionComponent,
{ ...TransitionProps, ...componentsProps.transition },
ownerState,
);

const tooltipProps = appendOwnerState(
TooltipComponent,
{ ...componentsProps.tooltip },
ownerState,
);

const tooltipArrowProps = appendOwnerState(
ArrowComponent,
{ ...componentsProps.arrow },
ownerState,
);

return (
<React.Fragment>
{React.cloneElement(children, childrenProps)}
<TooltipPopper
as={PopperComponent}
className={classes.popper}
<PopperComponent
as={PopperComponentProp ?? Popper}
placement={placement}
anchorEl={
followCursor
Expand All @@ -647,25 +678,32 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
id={id}
transition
{...interactiveWrapperListeners}
{...PopperProps}
{...popperProps}
className={clsx(classes.popper, componentsProps.popper?.className)}
popperOptions={popperOptions}
ownerState={ownerState}
>
{({ TransitionProps: TransitionPropsInner }) => (
<TransitionComponent
timeout={theme.transitions.duration.shorter}
{...TransitionPropsInner}
{...TransitionProps}
{...transitionProps}
>
<TooltipTooltip className={classes.tooltip} ownerState={ownerState}>
<TooltipComponent
{...tooltipProps}
className={clsx(classes.tooltip, componentsProps.tooltip?.className)}
>
{title}
{arrow ? (
<TooltipArrow className={classes.arrow} ref={setArrowRef} ownerState={ownerState} />
<ArrowComponent
{...tooltipArrowProps}
className={clsx(classes.arrow, componentsProps.arrow?.className)}
ref={setArrowRef}
/>
) : null}
</TooltipTooltip>
</TooltipComponent>
</TransitionComponent>
)}
</TooltipPopper>
</PopperComponent>
</React.Fragment>
);
});
Expand All @@ -692,6 +730,22 @@ Tooltip.propTypes /* remove-proptypes */ = {
* @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.
* @default {}
*/
componentsProps: 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.
Expand Down
90 changes: 90 additions & 0 deletions packages/mui-material/src/Tooltip/Tooltip.test.js
Expand Up @@ -1092,6 +1092,96 @@ describe('<Tooltip />', () => {
});
});

describe('prop: components', () => {
it('can render a different Popper component', () => {
const CustomPopper = () => <div data-testid="CustomPopper" />;
const { getByTestId } = render(
<Tooltip title="Hello World" open components={{ Popper: CustomPopper }}>
<button id="testChild" type="submit">
Hello World
</button>
</Tooltip>,
);
expect(getByTestId('CustomPopper')).toBeVisible();
});

it('can render a different Tooltip component', () => {
const CustomTooltip = React.forwardRef((props, ref) => (
<div data-testid="CustomTooltip" ref={ref} />
));
const { getByTestId } = render(
<Tooltip title="Hello World" open components={{ Tooltip: CustomTooltip }}>
<button id="testChild" type="submit">
Hello World
</button>
</Tooltip>,
);
expect(getByTestId('CustomTooltip')).toBeVisible();
});

it('can render a different Arrow component', () => {
const CustomArrow = React.forwardRef((props, ref) => (
<div data-testid="CustomArrow" ref={ref} />
));
const { getByTestId } = render(
<Tooltip title="Hello World" open arrow components={{ Arrow: CustomArrow }}>
<button id="testChild" type="submit">
Hello World
</button>
</Tooltip>,
);
expect(getByTestId('CustomArrow')).toBeVisible();
});
});

describe('prop: componentsProps', () => {
it('can provide custom props for the inner Popper component', () => {
const { getByTestId } = render(
<Tooltip
title="Hello World"
open
componentsProps={{ popper: { 'data-testid': 'CustomPopper' } }}
>
<button id="testChild" type="submit">
Hello World
</button>
</Tooltip>,
);
expect(getByTestId('CustomPopper')).toBeVisible();
});

it('can provide custom props for the inner Tooltip component', () => {
const { getByTestId } = render(
<Tooltip
title="Hello World"
open
componentsProps={{ tooltip: { 'data-testid': 'CustomTooltip' } }}
>
<button id="testChild" type="submit">
Hello World
</button>
</Tooltip>,
);
expect(getByTestId('CustomTooltip')).toBeVisible();
});

it('can provide custom props for the inner Arrow component', () => {
const { getByTestId } = render(
<Tooltip
title="Hello World"
open
arrow
componentsProps={{ arrow: { 'data-testid': 'CustomArrow' } }}
>
<button id="testChild" type="submit">
Hello World
</button>
</Tooltip>,
);
expect(getByTestId('CustomArrow')).toBeVisible();
});
});

describe('user-select state', () => {
let prevWebkitUserSelect;
beforeEach(() => {
Expand Down

0 comments on commit f98d52b

Please sign in to comment.