Skip to content

Commit

Permalink
[Tooltip] Add placement followCursor (#22876)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Silbermann <[email protected]>
Co-authored-by: Olivier Tassinari <[email protected]>
  • Loading branch information
3 people authored Oct 7, 2020
1 parent cb6b69b commit 4084299
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/pages/api-docs/tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The `MuiTooltip` name can be used for providing [default props](/customization/g
| <span class="prop-name">enterDelay</span> | <span class="prop-type">number</span> | <span class="prop-default">100</span> | The number of milliseconds to wait before showing the tooltip. This prop won't impact the enter touch delay (`enterTouchDelay`). |
| <span class="prop-name">enterNextDelay</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The number of milliseconds to wait before showing the tooltip when one was already recently opened. |
| <span class="prop-name">enterTouchDelay</span> | <span class="prop-type">number</span> | <span class="prop-default">700</span> | The number of milliseconds a user must touch the element before showing the tooltip. |
| <span class="prop-name">followCursor</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the tooltip follow the cursor over the wrapped element. |
| <span class="prop-name">id</span> | <span class="prop-type">string</span> | | 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. |
| <span class="prop-name">leaveDelay</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The number of milliseconds to wait before hiding the tooltip. This prop won't impact the leave touch delay (`leaveTouchDelay`). |
| <span class="prop-name">leaveTouchDelay</span> | <span class="prop-type">number</span> | <span class="prop-default">1500</span> | The number of milliseconds after the user stops touching an element before hiding the tooltip. |
Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/components/popper/popper.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
55 changes: 55 additions & 0 deletions docs/src/pages/components/tooltips/AnchorElTooltips.js
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip
title="Add"
placement="top"
arrow
PopperProps={{
popperRef,
anchorEl: {
clientHeight: 0,
clientWidth: 0,
getBoundingClientRect: () => ({
top: areaRef.current?.getBoundingClientRect().top ?? 0,
left: positionRef.current.x,
right: positionRef.current.x,
bottom: areaRef.current?.getBoundingClientRect().bottom ?? 0,
width: 0,
height: 0,
}),
},
}}
>
<Box
/* @ts-expect-error need to fix #17010 */
ref={areaRef}
bgcolor="primary.main"
color="primary.contrastText"
onMouseMove={handleMouseMove}
p={2}
>
Hover
</Box>
</Tooltip>
);
}
55 changes: 55 additions & 0 deletions docs/src/pages/components/tooltips/AnchorElTooltips.tsx
Original file line number Diff line number Diff line change
@@ -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<PopperJs>(null);
const areaRef = React.useRef<HTMLDivElement>(null);

const handleMouseMove = (event: React.MouseEvent) => {
positionRef.current = { x: event.clientX, y: event.clientY };

if (popperRef.current != null) {
popperRef.current.scheduleUpdate();
}
};

return (
<Tooltip
title="Add"
placement="top"
arrow
PopperProps={{
popperRef,
anchorEl: {
clientHeight: 0,
clientWidth: 0,
getBoundingClientRect: () => ({
top: areaRef.current?.getBoundingClientRect().top ?? 0,
left: positionRef.current.x,
right: positionRef.current.x,
bottom: areaRef.current?.getBoundingClientRect().bottom ?? 0,
width: 0,
height: 0,
}),
},
}}
>
<Box
/* @ts-expect-error need to fix #17010 */
ref={areaRef}
bgcolor="primary.main"
color="primary.contrastText"
onMouseMove={handleMouseMove}
p={2}
>
Hover
</Box>
</Tooltip>
);
}
13 changes: 13 additions & 0 deletions docs/src/pages/components/tooltips/FollowCursorTooltips.js
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip title="You don't have permission to do this" followCursor>
<Box bgcolor="text.disabled" color="background.paper" p={2}>
Disabled Action
</Box>
</Tooltip>
);
}
13 changes: 13 additions & 0 deletions docs/src/pages/components/tooltips/FollowCursorTooltips.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip title="You don't have permission to do this" followCursor>
<Box bgcolor="text.disabled" color="background.paper" p={2}>
Disabled Action
</Box>
</Tooltip>
);
}
14 changes: 14 additions & 0 deletions docs/src/pages/components/tooltips/tooltips.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions packages/material-ui/src/Tooltip/Tooltip.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export interface TooltipProps extends StandardProps<React.HTMLAttributes<HTMLDiv
* @default 700
*/
enterTouchDelay?: number;
/**
* If `true`, the tooltip follow the cursor over the wrapped element.
* @default false
*/
followCursor?: boolean;
/**
* 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.
Expand Down
41 changes: 40 additions & 1 deletion packages/material-ui/src/Tooltip/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
enterDelay = 100,
enterNextDelay = 0,
enterTouchDelay = 700,
followCursor = false,
id: idProp,
disableInteractive = false,
leaveDelay = 0,
Expand Down Expand Up @@ -440,6 +441,22 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
open = false;
}

const positionRef = React.useRef({ x: 0, y: 0 });
const popperRef = React.useRef();

const handleMouseMove = (event) => {
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) {
Expand All @@ -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') {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
51 changes: 51 additions & 0 deletions packages/material-ui/src/Tooltip/Tooltip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<Tooltip />', () => {
/**
* @type {ReturnType<typeof useFakeTimers>}
Expand Down Expand Up @@ -967,4 +980,42 @@ describe('<Tooltip />', () => {
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(
<Tooltip title="Hello World" open followCursor PopperProps={{ 'data-testid': 'popper' }}>
<button data-testid="target" type="submit">
Hello World
</button>
</Tooltip>,
);
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);
});
});
});

0 comments on commit 4084299

Please sign in to comment.