Skip to content

Commit 79342ce

Browse files
committed
feat(tooltip): Add ability to hover on tooltip. Provide optional delay of updating so if the mouse p
issue 411
1 parent f6f8401 commit 79342ce

File tree

9 files changed

+364
-216
lines changed

9 files changed

+364
-216
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ className | data-class | String | | extra custom class, can use !importan
6767
html | data-html | Bool | true, false | `<p data-tip="<p>HTML tooltip</p>" data-html={true}></p>` or `<ReactTooltip html={true} />`
6868
delayHide | data-delay-hide | Number | | `<p data-tip="tooltip" data-delay-hide='1000'></p>` or `<ReactTooltip delayHide={1000} />`
6969
delayShow | data-delay-show | Number | | `<p data-tip="tooltip" data-delay-show='1000'></p>` or `<ReactTooltip delayShow={1000} />`
70+
delayUpdate | data-delay-update | Number | | `<p data-tip="tooltip" data-delay-update='1000'></p>` or `<ReactTooltip delayUpdate={1000} />` Sets a delay in calling getContent if the tooltip is already shown and you mouse over another target
7071
insecure | null | Bool | true, false | Whether to inject the style header into the page dynamically (violates CSP style-src but is a convenient default)
7172
border | data-border | Bool | true, false | Add one pixel white border
7273
getContent | null | Func or Array | (dataTip) => {}, [(dataTip) => {}, Interval] | Generate the tip content dynamically

example/src/index.js

+61
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,67 @@ class Test extends React.Component {
323323
</div>
324324
</pre>
325325
</div>
326+
<div className="section">
327+
<h4 className='title'>Demonstrate using mouse in tooltip. </h4>
328+
<p>Notice that the tooltip delays going away so you can get your mouse in it. You must set delayUpdate and delayHide for the tooltip to stay long enough to get your mouse over it.</p>
329+
<p className="sub-title"></p>
330+
<div className="example-jsx">
331+
<div className="block" >
332+
<a data-for='soclose' data-tip='1'>1 (❂‿❂)</a>
333+
</div>
334+
<div className="block">
335+
<a data-for='soclose' data-tip='2'>2 (❂‿❂)</a>
336+
</div>
337+
<div className="block" >
338+
<a data-for='soclose' data-tip='3'>3(❂‿❂)</a>
339+
</div>
340+
<div className="block">
341+
<a data-for='soclose' data-tip='4'>4(❂‿❂)</a>
342+
</div>
343+
<div className="block" >
344+
<a data-for='soclose' data-tip='5'>5(❂‿❂)</a>
345+
</div>
346+
<div className="block">
347+
<a data-for='soclose' data-tip='6'>6(❂‿❂)</a>
348+
</div>
349+
<div className="block" >
350+
<a data-for='soclose' data-tip='7'>7(❂‿❂)</a>
351+
</div>
352+
<div className="block">
353+
<a data-for='soclose' data-tip='8'>8(❂‿❂)</a>
354+
</div>
355+
356+
<ReactTooltip id='soclose'
357+
getContent={(dataTip) => <div><h3>This little buddy is {dataTip}</h3><p>Put mouse here</p></div> }
358+
effect='solid'
359+
delayHide={500}
360+
delayShow={500}
361+
delayUpdate={500}
362+
place={'right'}
363+
border={true}
364+
type={'light'}
365+
366+
/>
367+
</div>
368+
<br />
369+
<pre className='example-pre'>
370+
<div>
371+
<p>{"<a data-for='soclose' data-tip='sooooo cute'>(❂‿❂)</a>"}<p/>{"<a data-for='soclose' data-tip='2'>(❂‿❂)</a>..."}<p/>{
372+
"<a data-for='soclose' data-tip='really high'>(❂‿❂)</a>\n" +
373+
"<ReactTooltip id='soclose'\n" +
374+
" getContent={(dataTip) => \n"}{
375+
" <div><h3>This little buddy is {dataTip}</h3><p>Put mouse here</p></div> }\n" +
376+
" effect='solid'\n" +
377+
" delayHide={500}\n" +
378+
" delayShow={500}\n" +
379+
" delayUpdate={500}\n" +
380+
" place={'right'}\n" +
381+
" border={true}\n" +
382+
" type={'light'}"}</p>
383+
</div>
384+
</pre>
385+
</div>
386+
326387
</section>
327388
</div>
328389
)

example/src/index.scss

+12
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ html, body{
157157
height: 0;
158158
visibility: hidden;
159159
}
160+
.block {
161+
float: left;
162+
$width: 55px;
163+
164+
a {
165+
text-align: center;
166+
width: $width;
167+
height: $width;
168+
border: 1px solid #999;
169+
border-radius: 0px
170+
}
171+
}
160172
.side {
161173
width: 50%;
162174
float: left;

src/index.js

+108-43
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class ReactTooltip extends React.Component {
4444
id: PropTypes.string,
4545
html: PropTypes.bool,
4646
delayHide: PropTypes.number,
47+
delayUpdate: PropTypes.number,
4748
delayShow: PropTypes.number,
4849
event: PropTypes.string,
4950
eventOff: PropTypes.string,
@@ -101,12 +102,14 @@ class ReactTooltip extends React.Component {
101102
'globalRebuild',
102103
'globalShow',
103104
'globalHide',
104-
'onWindowResize'
105+
'onWindowResize',
106+
'mouseOnToolTip'
105107
])
106108

107109
this.mount = true
108110
this.delayShowLoop = null
109111
this.delayHideLoop = null
112+
this.delayReshow = null
110113
this.intervalUpdateContent = null
111114
}
112115

@@ -150,6 +153,22 @@ class ReactTooltip extends React.Component {
150153
this.unbindWindowEvents()
151154
}
152155

156+
/**
157+
* Return if the mouse is on the tooltip.
158+
* @returns {boolean} true - mouse is on the tooltip
159+
*/
160+
mouseOnToolTip () {
161+
const {show} = this.state
162+
163+
if (show && this.tooltipRef) {
164+
/* old IE work around */
165+
if (!this.tooltipRef.matches) {
166+
this.tooltipRef.matches = this.tooltipRef.msMatchesSelector
167+
}
168+
return this.tooltipRef.matches(':hover')
169+
}
170+
return false
171+
}
153172
/**
154173
* Pick out corresponded target elements
155174
*/
@@ -280,57 +299,72 @@ class ReactTooltip extends React.Component {
280299
// To prevent previously created timers from triggering
281300
this.clearTimer()
282301

283-
this.setState({
284-
originTooltip: originTooltip,
285-
isMultiline: isMultiline,
286-
desiredPlace: e.currentTarget.getAttribute('data-place') || this.props.place || 'top',
287-
place: e.currentTarget.getAttribute('data-place') || this.props.place || 'top',
288-
type: e.currentTarget.getAttribute('data-type') || this.props.type || 'dark',
289-
effect: switchToSolid && 'solid' || this.getEffect(e.currentTarget),
290-
offset: e.currentTarget.getAttribute('data-offset') || this.props.offset || {},
291-
html: e.currentTarget.getAttribute('data-html')
292-
? e.currentTarget.getAttribute('data-html') === 'true'
293-
: (this.props.html || false),
294-
delayShow: e.currentTarget.getAttribute('data-delay-show') || this.props.delayShow || 0,
295-
delayHide: e.currentTarget.getAttribute('data-delay-hide') || this.props.delayHide || 0,
296-
border: e.currentTarget.getAttribute('data-border')
297-
? e.currentTarget.getAttribute('data-border') === 'true'
298-
: (this.props.border || false),
299-
extraClass: e.currentTarget.getAttribute('data-class') || this.props.class || this.props.className || '',
300-
disable: e.currentTarget.getAttribute('data-tip-disable')
301-
? e.currentTarget.getAttribute('data-tip-disable') === 'true'
302-
: (this.props.disable || false),
303-
currentTarget: e.currentTarget
304-
}, () => {
305-
if (scrollHide) this.addScrollListener(this.state.currentTarget)
306-
this.updateTooltip(e)
307-
308-
if (getContent && Array.isArray(getContent)) {
309-
this.intervalUpdateContent = setInterval(() => {
310-
if (this.mount) {
311-
const {getContent} = this.props
312-
const placeholder = getTipContent(originTooltip, '', getContent[0](), isMultiline)
313-
const isEmptyTip = this.isEmptyTip(placeholder)
314-
this.setState({
315-
isEmptyTip
316-
})
317-
this.updatePosition()
318-
}
319-
}, getContent[1])
320-
}
321-
})
302+
var target = e.currentTarget
303+
304+
var reshowDelay = this.state.show ? target.getAttribute('data-delay-update') || this.props.delayUpdate : 0
305+
306+
var self = this
307+
308+
var updateState = function updateState () {
309+
self.setState({
310+
originTooltip: originTooltip,
311+
isMultiline: isMultiline,
312+
desiredPlace: target.getAttribute('data-place') || self.props.place || 'top',
313+
place: target.getAttribute('data-place') || self.props.place || 'top',
314+
type: target.getAttribute('data-type') || self.props.type || 'dark',
315+
effect: switchToSolid && 'solid' || self.getEffect(target),
316+
offset: target.getAttribute('data-offset') || self.props.offset || {},
317+
html: target.getAttribute('data-html') ? target.getAttribute('data-html') === 'true' : self.props.html || false,
318+
delayShow: target.getAttribute('data-delay-show') || self.props.delayShow || 0,
319+
delayHide: target.getAttribute('data-delay-hide') || self.props.delayHide || 0,
320+
delayUpdate: target.getAttribute('data-delay-update') || self.props.delayUpdate || 0,
321+
border: target.getAttribute('data-border') ? target.getAttribute('data-border') === 'true' : self.props.border || false,
322+
extraClass: target.getAttribute('data-class') || self.props.class || self.props.className || '',
323+
disable: target.getAttribute('data-tip-disable') ? target.getAttribute('data-tip-disable') === 'true' : self.props.disable || false,
324+
currentTarget: target
325+
}, () => {
326+
if (scrollHide) self.addScrollListener(self.state.currentTarget)
327+
self.updateTooltip(e)
328+
329+
if (getContent && Array.isArray(getContent)) {
330+
this.intervalUpdateContent = setInterval(() => {
331+
if (self.mount) {
332+
const {getContent} = this.props
333+
const placeholder = getTipContent(originTooltip, '', getContent[0](), isMultiline)
334+
const isEmptyTip = this.isEmptyTip(placeholder)
335+
self.setState({
336+
isEmptyTip
337+
})
338+
self.updatePosition()
339+
}
340+
}, getContent[1])
341+
}
342+
})
343+
}
344+
345+
// If there is no delay call immediately, don't allow events to get in first.
346+
if (reshowDelay) {
347+
this.delayReshow = setTimeout(updateState, reshowDelay)
348+
} else {
349+
updateState()
350+
}
322351
}
323352

324353
/**
325354
* When mouse hover, updatetooltip
326355
*/
327356
updateTooltip (e) {
328-
const {delayShow, show, disable} = this.state
357+
const {delayShow, disable} = this.state
329358
const {afterShow} = this.props
330359
const placeholder = this.getTooltipContent()
331-
const delayTime = show ? 0 : parseInt(delayShow, 10)
360+
const delayTime = parseInt(delayShow, 10)
332361
const eventTarget = e.currentTarget || e.target
333362

363+
// Check if the mouse is actually over the tooltip, if so don't hide the tooltip
364+
if (this.mouseOnToolTip()) {
365+
return
366+
}
367+
334368
if (this.isEmptyTip(placeholder) || disable) return // if the tooltip is empty, disable the tooltip
335369
const updateState = () => {
336370
if (Array.isArray(placeholder) && placeholder.length > 0 || placeholder) {
@@ -354,6 +388,25 @@ class ReactTooltip extends React.Component {
354388
}
355389
}
356390

391+
/*
392+
* If we're mousing over the tooltip remove it when we leave.
393+
*/
394+
listenForTooltipExit () {
395+
const {show} = this.state
396+
397+
if (show && this.tooltipRef) {
398+
this.tooltipRef.addEventListener('mouseleave', this.hideTooltip)
399+
}
400+
}
401+
402+
removeListenerForTooltipExit () {
403+
const {show} = this.state
404+
405+
if (show && this.tooltipRef) {
406+
this.tooltipRef.removeEventListener('mouseleave', this.hideTooltip)
407+
}
408+
}
409+
357410
/**
358411
* When mouse leave, hide tooltip
359412
*/
@@ -369,8 +422,16 @@ class ReactTooltip extends React.Component {
369422
const isMyElement = targetArray.some(ele => ele === e.currentTarget)
370423
if (!isMyElement || !this.state.show) return
371424
}
425+
372426
const resetState = () => {
373427
const isVisible = this.state.show
428+
// Check if the mouse is actually over the tooltip, if so don't hide the tooltip
429+
if (this.mouseOnToolTip()) {
430+
this.listenForTooltipExit()
431+
return
432+
}
433+
this.removeListenerForTooltipExit()
434+
374435
this.setState({
375436
show: false
376437
}, () => {
@@ -437,6 +498,7 @@ class ReactTooltip extends React.Component {
437498
clearTimer () {
438499
clearTimeout(this.delayShowLoop)
439500
clearTimeout(this.delayHideLoop)
501+
clearTimeout(this.delayReshow)
440502
clearInterval(this.intervalUpdateContent)
441503
}
442504

@@ -457,7 +519,8 @@ class ReactTooltip extends React.Component {
457519
{'type-warning': this.state.type === 'warning'},
458520
{'type-error': this.state.type === 'error'},
459521
{'type-info': this.state.type === 'info'},
460-
{'type-light': this.state.type === 'light'}
522+
{'type-light': this.state.type === 'light'},
523+
{'allow_hover': this.props.delayUpdate}
461524
)
462525

463526
let Wrapper = this.props.wrapper
@@ -469,6 +532,7 @@ class ReactTooltip extends React.Component {
469532
return (
470533
<Wrapper className={`${tooltipClass} ${extraClass}`}
471534
id={this.props.id}
535+
ref={ref => this.tooltipRef = ref}
472536
{...ariaProps}
473537
data-id='tooltip'
474538
dangerouslySetInnerHTML={{__html: placeholder}}/>
@@ -478,6 +542,7 @@ class ReactTooltip extends React.Component {
478542
<Wrapper className={`${tooltipClass} ${extraClass}`}
479543
id={this.props.id}
480544
{...ariaProps}
545+
ref={ref => this.tooltipRef = ref}
481546
data-id='tooltip'>{placeholder}</Wrapper>
482547
)
483548
}

src/index.scss

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
top: -999em;
6868
visibility: hidden;
6969
z-index: 999;
70+
&.allow_hover {
71+
pointer-events:auto;
72+
}
7073
&:before,
7174
&:after {
7275
content: "";

0 commit comments

Comments
 (0)