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); + }); + }); });