Skip to content

Commit 68a0ed7

Browse files
authored
feat(Tooltip): add ability to disable autohide on tooltip content hover (#149)
* feat(Tooltip): add ability to disable autohide on tooltip content hover * docs(Tooltip): add example of disabling autohide
1 parent 0de52de commit 68a0ed7

5 files changed

Lines changed: 165 additions & 5 deletions

File tree

docs/lib/Components/TooltipsPage.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { PrismCode } from 'react-prism';
44
import Helmet from 'react-helmet';
55
import TooltipExample from '../examples/Tooltip';
66
const TooltipExampleSource = require('!!raw!../examples/Tooltip');
7+
import TooltipAutoHideExample from '../examples/TooltipAutoHide';
8+
const TooltipExampleAutoHideSource = require('!!raw!../examples/TooltipAutoHide');
79
import TooltipExampleMulti from '../examples/TooltipMulti';
810
const TooltipExampleMultiSource = require('!!raw!../examples/TooltipMulti');
911

@@ -39,6 +41,8 @@ export default class TooltipsPage extends React.Component {
3941
PropTypes.number
4042
]),
4143
// optionally override show/hide delays - default { show: 0, hide: 250 }
44+
autohide: PropTypes.bool,
45+
// optionally hide tooltip when hovering over tooltip content - default true
4246
placement: PropTypes.oneOf([
4347
'top',
4448
'bottom',
@@ -62,6 +66,15 @@ export default class TooltipsPage extends React.Component {
6266
}`}
6367
</PrismCode>
6468
</pre>
69+
<h3>Tooltip Disable Autohide</h3>
70+
<div className="docs-example">
71+
<TooltipAutoHideExample />
72+
</div>
73+
<pre>
74+
<PrismCode className="language-jsx">
75+
{TooltipExampleAutoHideSource}
76+
</PrismCode>
77+
</pre>
6578
<h3>Tooltips List</h3>
6679
<div className="docs-example">
6780
<TooltipExampleMulti />

docs/lib/examples/Tooltip.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
22
import React from 'react';
3-
import { Button, Tooltip } from 'reactstrap';
3+
import { Tooltip } from 'reactstrap';
44

55
export default class Example extends React.Component {
66
constructor(props) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2+
import React from 'react';
3+
import { Tooltip } from 'reactstrap';
4+
5+
export default class Example extends React.Component {
6+
constructor(props) {
7+
super(props);
8+
9+
this.toggle = this.toggle.bind(this);
10+
this.state = {
11+
tooltipOpen: false
12+
};
13+
}
14+
15+
toggle() {
16+
this.setState({
17+
tooltipOpen: !this.state.tooltipOpen
18+
});
19+
}
20+
21+
render() {
22+
return (
23+
<div>
24+
<p>Sometimes you need to allow users to select text within a <a href="#" id="DisabledAutoHideExample">tooltip</a>.</p>
25+
<Tooltip placement="top" isOpen={this.state.tooltipOpen} autohide={false} target="DisabledAutoHideExample" toggle={this.toggle}>
26+
Try to select this text!
27+
</Tooltip>
28+
</div>
29+
);
30+
}
31+
}

src/Tooltip.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const propTypes = {
1010
tether: PropTypes.object,
1111
toggle: PropTypes.func,
1212
children: PropTypes.node,
13+
autohide: PropTypes.bool,
1314
delay: PropTypes.oneOfType([
1415
PropTypes.shape({ show: PropTypes.number, hide: PropTypes.number }),
1516
PropTypes.number
@@ -24,7 +25,8 @@ const DEFAULT_DELAYS = {
2425
const defaultProps = {
2526
isOpen: false,
2627
placement: 'bottom',
27-
delay: DEFAULT_DELAYS
28+
delay: DEFAULT_DELAYS,
29+
autohide: true
2830
};
2931

3032
const defaultTetherConfig = {
@@ -47,6 +49,8 @@ class Tooltip extends React.Component {
4749
this.toggle = this.toggle.bind(this);
4850
this.onMouseOverTooltip = this.onMouseOverTooltip.bind(this);
4951
this.onMouseLeaveTooltip = this.onMouseLeaveTooltip.bind(this);
52+
this.onMouseOverTooltipContent = this.onMouseOverTooltipContent.bind(this);
53+
this.onMouseLeaveTooltipContent = this.onMouseLeaveTooltipContent.bind(this);
5054
this.show = this.show.bind(this);
5155
this.hide = this.hide.bind(this);
5256
}
@@ -74,6 +78,25 @@ class Tooltip extends React.Component {
7478
this._hideTimeout = setTimeout(this.hide, this.getDelay('hide'));
7579
}
7680

81+
onMouseOverTooltipContent() {
82+
if (this.props.autohide) {
83+
return;
84+
}
85+
if (this._hideTimeout) {
86+
this.clearHideTimeout();
87+
}
88+
}
89+
90+
onMouseLeaveTooltipContent() {
91+
if (this.props.autohide) {
92+
return;
93+
}
94+
if (this._showTimeout) {
95+
this.clearShowTimeout();
96+
}
97+
this._hideTimeout = setTimeout(this.hide, this.getDelay('hide'));
98+
}
99+
77100
getDelay(key) {
78101
const { delay } = this.props;
79102
if (typeof delay === 'object') {
@@ -94,11 +117,14 @@ class Tooltip extends React.Component {
94117

95118
show() {
96119
if (!this.props.isOpen) {
120+
this.clearShowTimeout();
97121
this.toggle();
98122
}
99123
}
124+
100125
hide() {
101126
if (this.props.isOpen) {
127+
this.clearHideTimeout();
102128
this.toggle();
103129
}
104130
}
@@ -154,14 +180,16 @@ class Tooltip extends React.Component {
154180

155181
return (
156182
<TetherContent
157-
onMouseOver={this.onMouseOverTooltip}
158-
onMouseLeave={this.onMouseLeaveTooltip}
159183
arrow="tooltip"
160184
tether={tetherConfig}
161185
isOpen={this.props.isOpen}
162186
toggle={this.toggle}
163187
>
164-
<div className="tooltip-inner">
188+
<div
189+
className="tooltip-inner"
190+
onMouseOver={this.onMouseOverTooltipContent}
191+
onMouseLeave={this.onMouseLeaveTooltipContent}
192+
>
165193
{this.props.children}
166194
</div>
167195
</TetherContent>

test/Tooltip.spec.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,4 +398,92 @@ describe('Tooltip', () => {
398398
wrapper.detach();
399399
});
400400
});
401+
402+
describe('autohide', () => {
403+
it('should keep tooltip around when false and onmouseleave from tooltip content', () => {
404+
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
405+
isOpen = true;
406+
const wrapper = mount(
407+
<Tooltip target="target" autohide={false} isOpen={isOpen} toggle={toggle} delay={200}>
408+
Tooltip Content
409+
</Tooltip>,
410+
{ attachTo: container }
411+
);
412+
const instance = wrapper.instance();
413+
414+
expect(isOpen).toBe(true);
415+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
416+
417+
instance.onMouseLeaveTooltipContent();
418+
jasmine.clock().tick(100);
419+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
420+
jasmine.clock().tick(200);
421+
expect(Tooltip.prototype.toggle).toHaveBeenCalled();
422+
423+
wrapper.detach();
424+
});
425+
426+
it('clears showTimeout in onMouseLeaveTooltipContent', () => {
427+
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
428+
isOpen = true;
429+
const wrapper = mount(
430+
<Tooltip target="target" autohide={false} isOpen={isOpen} toggle={toggle} delay={200}>
431+
Tooltip Content
432+
</Tooltip>,
433+
{ attachTo: container }
434+
);
435+
const instance = wrapper.instance();
436+
437+
instance.onMouseOverTooltip();
438+
expect(instance._showTimeout).toBeTruthy();
439+
instance.onMouseLeaveTooltipContent();
440+
jasmine.clock().tick(300);
441+
expect(instance._showTimeout).toBeFalsy();
442+
wrapper.detach();
443+
});
444+
445+
it('clears hide timeout in onMouseOverTooltipContent', () => {
446+
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
447+
isOpen = true;
448+
const wrapper = mount(
449+
<Tooltip target="target" autohide={false} isOpen={isOpen} toggle={toggle} delay={200}>
450+
Tooltip Content
451+
</Tooltip>,
452+
{ attachTo: container }
453+
);
454+
const instance = wrapper.instance();
455+
456+
expect(isOpen).toBe(true);
457+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
458+
instance.onMouseLeaveTooltipContent();
459+
jasmine.clock().tick(100);
460+
expect(instance._hideTimeout).toBeTruthy();
461+
instance.onMouseOverTooltipContent();
462+
expect(instance._hideTimeout).toBeFalsy();
463+
instance.onMouseOverTooltipContent();
464+
wrapper.detach();
465+
});
466+
467+
it('should not keep tooltip around when autohide is true and tooltip content is hovered over', () => {
468+
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
469+
isOpen = true;
470+
const wrapper = mount(
471+
<Tooltip target="target" autohide isOpen={isOpen} toggle={toggle} delay={200}>
472+
Tooltip Content
473+
</Tooltip>,
474+
{ attachTo: container }
475+
);
476+
const instance = wrapper.instance();
477+
expect(isOpen).toBe(true);
478+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
479+
instance.onMouseLeaveTooltip();
480+
jasmine.clock().tick(100);
481+
instance.onMouseOverTooltipContent();
482+
jasmine.clock().tick(200);
483+
expect(Tooltip.prototype.toggle).toHaveBeenCalled();
484+
instance.onMouseLeaveTooltipContent();
485+
expect(instance._hideTimeout).toBeFalsy();
486+
wrapper.detach();
487+
});
488+
});
401489
});

0 commit comments

Comments
 (0)