diff --git a/docs/pages/api-docs/tooltip.md b/docs/pages/api-docs/tooltip.md
index 61bf6d6989b389..723b0fb8c65b0a 100644
--- a/docs/pages/api-docs/tooltip.md
+++ b/docs/pages/api-docs/tooltip.md
@@ -39,6 +39,7 @@ The `MuiTooltip` name can be used for providing [default props](/customization/g
| enterDelay | number | 100 | The number of milliseconds to wait before showing the tooltip. This prop won't impact the enter touch delay (`enterTouchDelay`). |
| enterNextDelay | number | 0 | The number of milliseconds to wait before showing the tooltip when one was already recently opened. |
| enterTouchDelay | number | 700 | The number of milliseconds a user must touch the element before showing the tooltip. |
+| followCursor | bool | false | If `true`, the tooltip follow the cursor over the wrapped element. |
| id | string | | 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. |
| leaveDelay | number | 0 | The number of milliseconds to wait before hiding the tooltip. This prop won't impact the leave touch delay (`leaveTouchDelay`). |
| leaveTouchDelay | number | 1500 | The number of milliseconds after the user stops touching an element before hiding the tooltip. |
diff --git a/docs/src/pages/components/popper/popper.md b/docs/src/pages/components/popper/popper.md
index ada7c1fa37f77b..a12e63e2777846 100644
--- a/docs/src/pages/components/popper/popper.md
+++ b/docs/src/pages/components/popper/popper.md
@@ -56,7 +56,7 @@ Alternatively, you can use [react-spring](https://github.com/react-spring/react-
## Faked reference object
The value of the `anchorEl` prop can be a reference to a fake DOM element.
-You just need to create an object shaped like the [`ReferenceObject`](https://github.com/FezVrasta/popper.js/blob/0642ce0ddeffe3c7c033a412d4d60ce7ec8193c3/packages/popper/index.d.ts#L118-L123).
+You need to create an object shaped like the [`ReferenceObject`](https://github.com/FezVrasta/popper.js/blob/0642ce0ddeffe3c7c033a412d4d60ce7ec8193c3/packages/popper/index.d.ts#L118-L123).
Highlight part of the text to see the popper:
diff --git a/docs/src/pages/components/tooltips/AnchorElTooltips.js b/docs/src/pages/components/tooltips/AnchorElTooltips.js
new file mode 100644
index 00000000000000..408be5d1779507
--- /dev/null
+++ b/docs/src/pages/components/tooltips/AnchorElTooltips.js
@@ -0,0 +1,55 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+import Tooltip from '@material-ui/core/Tooltip';
+
+export default function AnchorElTooltips() {
+ const positionRef = React.useRef({
+ x: 0,
+ y: 0,
+ });
+
+ const popperRef = React.useRef(null);
+ const areaRef = React.useRef(null);
+
+ const handleMouseMove = (event) => {
+ positionRef.current = { x: event.clientX, y: event.clientY };
+
+ if (popperRef.current != null) {
+ popperRef.current.scheduleUpdate();
+ }
+ };
+
+ return (
+ ({
+ top: areaRef.current?.getBoundingClientRect().top ?? 0,
+ left: positionRef.current.x,
+ right: positionRef.current.x,
+ bottom: areaRef.current?.getBoundingClientRect().bottom ?? 0,
+ width: 0,
+ height: 0,
+ }),
+ },
+ }}
+ >
+
+ Hover
+
+
+ );
+}
diff --git a/docs/src/pages/components/tooltips/AnchorElTooltips.tsx b/docs/src/pages/components/tooltips/AnchorElTooltips.tsx
new file mode 100644
index 00000000000000..2b9d8b286b226f
--- /dev/null
+++ b/docs/src/pages/components/tooltips/AnchorElTooltips.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+import Tooltip from '@material-ui/core/Tooltip';
+import PopperJs from 'popper.js';
+
+export default function AnchorElTooltips() {
+ const positionRef = React.useRef<{ x: number; y: number }>({
+ x: 0,
+ y: 0,
+ });
+ const popperRef = React.useRef(null);
+ const areaRef = React.useRef(null);
+
+ const handleMouseMove = (event: React.MouseEvent) => {
+ positionRef.current = { x: event.clientX, y: event.clientY };
+
+ if (popperRef.current != null) {
+ popperRef.current.scheduleUpdate();
+ }
+ };
+
+ return (
+ ({
+ top: areaRef.current?.getBoundingClientRect().top ?? 0,
+ left: positionRef.current.x,
+ right: positionRef.current.x,
+ bottom: areaRef.current?.getBoundingClientRect().bottom ?? 0,
+ width: 0,
+ height: 0,
+ }),
+ },
+ }}
+ >
+
+ Hover
+
+
+ );
+}
diff --git a/docs/src/pages/components/tooltips/FollowCursorTooltips.js b/docs/src/pages/components/tooltips/FollowCursorTooltips.js
new file mode 100644
index 00000000000000..51c73725722326
--- /dev/null
+++ b/docs/src/pages/components/tooltips/FollowCursorTooltips.js
@@ -0,0 +1,13 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+import Tooltip from '@material-ui/core/Tooltip';
+
+export default function FollowCursorTooltips() {
+ return (
+
+
+ Disabled Action
+
+
+ );
+}
diff --git a/docs/src/pages/components/tooltips/FollowCursorTooltips.tsx b/docs/src/pages/components/tooltips/FollowCursorTooltips.tsx
new file mode 100644
index 00000000000000..51c73725722326
--- /dev/null
+++ b/docs/src/pages/components/tooltips/FollowCursorTooltips.tsx
@@ -0,0 +1,13 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+import Tooltip from '@material-ui/core/Tooltip';
+
+export default function FollowCursorTooltips() {
+ return (
+
+
+ Disabled Action
+
+
+ );
+}
diff --git a/docs/src/pages/components/tooltips/tooltips.md b/docs/src/pages/components/tooltips/tooltips.md
index e338574b3428dc..e459e53d443cdd 100644
--- a/docs/src/pages/components/tooltips/tooltips.md
+++ b/docs/src/pages/components/tooltips/tooltips.md
@@ -113,6 +113,20 @@ Use a different transition.
{{"demo": "pages/components/tooltips/TransitionsTooltips.js"}}
+## Follow cursor
+
+You can enable the tooltip to follow the cursor by setting `followCursor={true}`.
+
+{{"demo": "pages/components/tooltips/FollowCursorTooltips.js"}}
+
+## Faked reference object
+
+In the event you need to implement a custom placement, you can use the `anchorEl` prop:
+The value of the `anchorEl` prop can be a reference to a fake DOM element.
+You need to create an object shaped like the [`ReferenceObject`](https://github.com/FezVrasta/popper.js/blob/0642ce0ddeffe3c7c033a412d4d60ce7ec8193c3/packages/popper/index.d.ts#L118-L123).
+
+{{"demo": "pages/components/tooltips/AnchorElTooltips.js"}}
+
## Showing and hiding
The tooltip is normally shown immediately when the user's mouse hovers over the element, and hides immediately when the user's mouse leaves. A delay in showing or hiding the tooltip can be added through the `enterDelay` and `leaveDelay` props, as shown in the Controlled Tooltips demo above.
diff --git a/packages/material-ui/src/Tooltip/Tooltip.d.ts b/packages/material-ui/src/Tooltip/Tooltip.d.ts
index bed17e51f14b2c..d9eadaffeeeb7c 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.d.ts
+++ b/packages/material-ui/src/Tooltip/Tooltip.d.ts
@@ -83,6 +83,11 @@ export interface TooltipProps extends StandardProps {
+ const childrenProps = children.props;
+ if (childrenProps.handleMouseMove) {
+ childrenProps.handleMouseMove(event);
+ }
+
+ positionRef.current = { x: event.clientX, y: event.clientY };
+
+ if (popperRef.current) {
+ popperRef.current.scheduleUpdate();
+ }
+ };
+
const nameOrDescProps = {};
const titleIsString = typeof title === 'string';
if (describeChild) {
@@ -457,6 +474,7 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
className: clsx(other.className, children.props.className),
onTouchStart: detectTouchStart,
ref: handleRef,
+ ...(followCursor ? { onMouseMove: handleMouseMove } : {}),
};
if (process.env.NODE_ENV !== 'production') {
@@ -538,7 +556,23 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
[classes.popperArrow]: arrow,
})}
placement={placement}
- anchorEl={childNode}
+ anchorEl={
+ followCursor
+ ? {
+ clientHeight: 0,
+ clientWidth: 0,
+ 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
@@ -636,6 +670,11 @@ Tooltip.propTypes = {
* @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.
diff --git a/packages/material-ui/src/Tooltip/Tooltip.test.js b/packages/material-ui/src/Tooltip/Tooltip.test.js
index 011bdba116c7be..9495e8852059b9 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.test.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.test.js
@@ -17,6 +17,19 @@ import { camelCase } from 'lodash/string';
import Tooltip, { testReset } from './Tooltip';
import Input from '../Input';
+async function raf() {
+ return new Promise((resolve) => {
+ // Chrome and Safari have a bug where calling rAF once returns the current
+ // frame instead of the next frame, so we need to call a double rAF here.
+ // See crbug.com/675795 for more.
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ });
+}
+
describe('', () => {
/**
* @type {ReturnType}
@@ -967,4 +980,42 @@ describe('', () => {
expect(getByTestId('CustomPopper')).toBeVisible();
});
});
+
+ describe('prop: followCursor', () => {
+ it('should use the position of the mouse', async function test() {
+ // Only callig render() outputs:
+ // An update to ForwardRef(Popper) inside a test was not wrapped in act(...).
+ // Somethings is wrong in JSDOM and strict mode.
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+
+ const x = 5;
+ const y = 10;
+
+ // Avoid mock of raf
+ clock.restore();
+ render(
+
+
+ ,
+ );
+ const tooltipElement = screen.getByTestId('popper');
+ const targetElement = screen.getByTestId('target');
+
+ fireEvent.mouseMove(targetElement, {
+ clientX: x,
+ clientY: y,
+ });
+
+ // Wait for the scheduleUpdate() call to resolve.
+ await raf();
+
+ expect(tooltipElement).toBeVisible();
+ expect(tooltipElement.getBoundingClientRect()).to.have.property('top', y);
+ expect(tooltipElement.getBoundingClientRect()).to.have.property('left', x);
+ });
+ });
});