@@ -83,71 +83,6 @@ const Tooltip = ({
83
83
const [ anchorElements , setAnchorElements ] = useState < HTMLElement [ ] > ( [ ] )
84
84
const mounted = useRef ( false )
85
85
86
- const hasClickEvent =
87
- openOnClick || openEvents ?. click || openEvents ?. dblclick || openEvents ?. mousedown
88
- const actualOpenEvents : AnchorOpenEvents = openEvents
89
- ? { ...openEvents }
90
- : {
91
- mouseenter : true ,
92
- focus : true ,
93
- click : false ,
94
- dblclick : false ,
95
- mousedown : false ,
96
- }
97
- if ( ! openEvents && openOnClick ) {
98
- Object . assign ( actualOpenEvents , {
99
- mouseenter : false ,
100
- focus : false ,
101
- click : true ,
102
- } )
103
- }
104
- const actualCloseEvents : AnchorCloseEvents = closeEvents
105
- ? { ...closeEvents }
106
- : {
107
- mouseleave : true ,
108
- blur : true ,
109
- click : false ,
110
- dblclick : false ,
111
- mouseup : false ,
112
- }
113
- if ( ! closeEvents && openOnClick ) {
114
- Object . assign ( actualCloseEvents , {
115
- mouseleave : false ,
116
- blur : false ,
117
- } )
118
- }
119
- const actualGlobalCloseEvents : GlobalCloseEvents = globalCloseEvents
120
- ? { ...globalCloseEvents }
121
- : {
122
- escape : false ,
123
- scroll : false ,
124
- resize : false ,
125
- clickOutsideAnchor : hasClickEvent || false ,
126
- }
127
-
128
- if ( imperativeModeOnly ) {
129
- Object . assign ( actualOpenEvents , {
130
- mouseenter : false ,
131
- focus : false ,
132
- click : false ,
133
- dblclick : false ,
134
- mousedown : false ,
135
- } )
136
- Object . assign ( actualCloseEvents , {
137
- mouseleave : false ,
138
- blur : false ,
139
- click : false ,
140
- dblclick : false ,
141
- mouseup : false ,
142
- } )
143
- Object . assign ( actualGlobalCloseEvents , {
144
- escape : false ,
145
- scroll : false ,
146
- resize : false ,
147
- clickOutsideAnchor : false ,
148
- } )
149
- }
150
-
151
86
/**
152
87
* useLayoutEffect runs before useEffect,
153
88
* but should be used carefully because of caveats
@@ -160,27 +95,30 @@ const Tooltip = ({
160
95
}
161
96
} , [ ] )
162
97
163
- const handleShow = ( value : boolean ) => {
164
- if ( ! mounted . current ) {
165
- return
166
- }
167
- if ( value ) {
168
- setRendered ( true )
169
- }
170
- /**
171
- * wait for the component to render and calculate position
172
- * before actually showing
173
- */
174
- setTimeout ( ( ) => {
98
+ const handleShow = useCallback (
99
+ ( value : boolean ) => {
175
100
if ( ! mounted . current ) {
176
101
return
177
102
}
178
- setIsOpen ?.( value )
179
- if ( isOpen === undefined ) {
180
- setShow ( value )
103
+ if ( value ) {
104
+ setRendered ( true )
181
105
}
182
- } , 10 )
183
- }
106
+ /**
107
+ * wait for the component to render and calculate position
108
+ * before actually showing
109
+ */
110
+ setTimeout ( ( ) => {
111
+ if ( ! mounted . current ) {
112
+ return
113
+ }
114
+ setIsOpen ?.( value )
115
+ if ( isOpen === undefined ) {
116
+ setShow ( value )
117
+ }
118
+ } , 10 )
119
+ } ,
120
+ [ isOpen , setIsOpen ] ,
121
+ )
184
122
185
123
/**
186
124
* this replicates the effect from `handleShow()`
@@ -228,7 +166,7 @@ const Tooltip = ({
228
166
// +25ms just to make sure `onTransitionEnd` (if it gets fired) has time to run
229
167
} , transitionShowDelay + 25 )
230
168
}
231
- } , [ show ] )
169
+ } , [ afterHide , afterShow , show ] )
232
170
233
171
const handleComputedPosition = ( newComputedPosition : IComputedPosition ) => {
234
172
setComputedPosition ( ( oldComputedPosition ) =>
@@ -238,154 +176,72 @@ const Tooltip = ({
238
176
)
239
177
}
240
178
241
- const handleShowTooltipDelayed = ( delay = delayShow ) => {
242
- if ( tooltipShowDelayTimerRef . current ) {
243
- clearTimeout ( tooltipShowDelayTimerRef . current )
244
- }
245
-
246
- if ( rendered ) {
247
- // if the tooltip is already rendered, ignore delay
248
- handleShow ( true )
249
- return
250
- }
251
-
252
- tooltipShowDelayTimerRef . current = setTimeout ( ( ) => {
253
- handleShow ( true )
254
- } , delay )
255
- }
256
-
257
- const handleHideTooltipDelayed = ( delay = delayHide ) => {
258
- if ( tooltipHideDelayTimerRef . current ) {
259
- clearTimeout ( tooltipHideDelayTimerRef . current )
260
- }
179
+ const handleShowTooltipDelayed = useCallback (
180
+ ( delay = delayShow ) => {
181
+ if ( tooltipShowDelayTimerRef . current ) {
182
+ clearTimeout ( tooltipShowDelayTimerRef . current )
183
+ }
261
184
262
- tooltipHideDelayTimerRef . current = setTimeout ( ( ) => {
263
- if ( hoveringTooltip . current ) {
185
+ if ( rendered ) {
186
+ // if the tooltip is already rendered, ignore delay
187
+ handleShow ( true )
264
188
return
265
189
}
266
- handleShow ( false )
267
- } , delay )
268
- }
269
-
270
- const handleShowTooltip = ( event ?: Event ) => {
271
- if ( ! event ) {
272
- return
273
- }
274
- const target = ( event . currentTarget ?? event . target ) as HTMLElement | null
275
- if ( ! target ?. isConnected ) {
276
- /**
277
- * this happens when the target is removed from the DOM
278
- * at the same time the tooltip gets triggered
279
- */
280
- setActiveAnchor ( null )
281
- return
282
- }
283
- if ( delayShow ) {
284
- handleShowTooltipDelayed ( )
285
- } else {
286
- handleShow ( true )
287
- }
288
- setActiveAnchor ( target )
289
190
290
- if ( tooltipHideDelayTimerRef . current ) {
291
- clearTimeout ( tooltipHideDelayTimerRef . current )
292
- }
293
- }
294
-
295
- const handleHideTooltip = ( ) => {
296
- if ( clickable ) {
297
- // allow time for the mouse to reach the tooltip, in case there's a gap
298
- handleHideTooltipDelayed ( delayHide || 100 )
299
- } else if ( delayHide ) {
300
- handleHideTooltipDelayed ( )
301
- } else {
302
- handleShow ( false )
303
- }
191
+ tooltipShowDelayTimerRef . current = setTimeout ( ( ) => {
192
+ handleShow ( true )
193
+ } , delay )
194
+ } ,
195
+ [ delayShow , handleShow , rendered ] ,
196
+ )
304
197
305
- if ( tooltipShowDelayTimerRef . current ) {
306
- clearTimeout ( tooltipShowDelayTimerRef . current )
307
- }
308
- }
198
+ const handleHideTooltipDelayed = useCallback (
199
+ ( delay = delayHide ) => {
200
+ if ( tooltipHideDelayTimerRef . current ) {
201
+ clearTimeout ( tooltipHideDelayTimerRef . current )
202
+ }
309
203
310
- const handleTooltipPosition = ( { x, y } : IPosition ) => {
311
- const virtualElement = {
312
- getBoundingClientRect ( ) {
313
- return {
314
- x,
315
- y,
316
- width : 0 ,
317
- height : 0 ,
318
- top : y ,
319
- left : x ,
320
- right : x ,
321
- bottom : y ,
204
+ tooltipHideDelayTimerRef . current = setTimeout ( ( ) => {
205
+ if ( hoveringTooltip . current ) {
206
+ return
322
207
}
323
- } ,
324
- } as Element
325
- computeTooltipPosition ( {
326
- place : imperativeOptions ?. place ?? place ,
327
- offset,
328
- elementReference : virtualElement ,
329
- tooltipReference : tooltipRef . current ,
330
- tooltipArrowReference : tooltipArrowRef . current ,
331
- strategy : positionStrategy ,
332
- middlewares,
333
- border,
334
- } ) . then ( ( computedStylesData ) => {
335
- handleComputedPosition ( computedStylesData )
336
- } )
337
- }
338
-
339
- const handlePointerMove = ( event ?: Event ) => {
340
- if ( ! event ) {
341
- return
342
- }
343
- const mouseEvent = event as MouseEvent
344
- const mousePosition = {
345
- x : mouseEvent . clientX ,
346
- y : mouseEvent . clientY ,
347
- }
348
- handleTooltipPosition ( mousePosition )
349
- lastFloatPosition . current = mousePosition
350
- }
351
-
352
- const handleClickOutsideAnchors = ( event : MouseEvent ) => {
353
- if ( ! show ) {
354
- return
355
- }
356
- const target = event . target as HTMLElement
357
- if ( ! target . isConnected ) {
358
- return
359
- }
360
- if ( tooltipRef . current ?. contains ( target ) ) {
361
- return
362
- }
363
- if ( anchorElements . some ( ( anchor ) => anchor ?. contains ( target ) ) ) {
364
- return
365
- }
366
- handleShow ( false )
367
- if ( tooltipShowDelayTimerRef . current ) {
368
- clearTimeout ( tooltipShowDelayTimerRef . current )
369
- }
370
- }
208
+ handleShow ( false )
209
+ } , delay )
210
+ } ,
211
+ [ delayHide , handleShow ] ,
212
+ )
371
213
372
- // debounce handler to prevent call twice when
373
- // mouse enter and focus events being triggered toggether
374
- const internalDebouncedHandleShowTooltip = debounce ( handleShowTooltip , 50 , true )
375
- const internalDebouncedHandleHideTooltip = debounce ( handleHideTooltip , 50 , true )
376
- // If either of the functions is called while the other is still debounced,
377
- // reset the timeout. Otherwise if there is a sub-50ms (leave A, enter B, leave B)
378
- // sequence of events, the tooltip will stay open because the hide debounce
379
- // from leave A prevented the leave B event from calling it, leaving the
380
- // tooltip visible.
381
- const debouncedHandleShowTooltip = ( e ?: Event ) => {
382
- internalDebouncedHandleHideTooltip . cancel ( )
383
- internalDebouncedHandleShowTooltip ( e )
384
- }
385
- const debouncedHandleHideTooltip = ( ) => {
386
- internalDebouncedHandleShowTooltip . cancel ( )
387
- internalDebouncedHandleHideTooltip ( )
388
- }
214
+ const handleTooltipPosition = useCallback (
215
+ ( { x, y } : IPosition ) => {
216
+ const virtualElement = {
217
+ getBoundingClientRect ( ) {
218
+ return {
219
+ x,
220
+ y,
221
+ width : 0 ,
222
+ height : 0 ,
223
+ top : y ,
224
+ left : x ,
225
+ right : x ,
226
+ bottom : y ,
227
+ }
228
+ } ,
229
+ } as Element
230
+ computeTooltipPosition ( {
231
+ place : imperativeOptions ?. place ?? place ,
232
+ offset,
233
+ elementReference : virtualElement ,
234
+ tooltipReference : tooltipRef . current ,
235
+ tooltipArrowReference : tooltipArrowRef . current ,
236
+ strategy : positionStrategy ,
237
+ middlewares,
238
+ border,
239
+ } ) . then ( ( computedStylesData ) => {
240
+ handleComputedPosition ( computedStylesData )
241
+ } )
242
+ } ,
243
+ [ imperativeOptions ?. place , place , offset , positionStrategy , middlewares , border ] ,
244
+ )
389
245
390
246
const updateTooltipPosition = useCallback ( ( ) => {
391
247
const actualPosition = imperativeOptions ?. position ?? position
@@ -431,17 +287,17 @@ const Tooltip = ({
431
287
handleComputedPosition ( computedStylesData )
432
288
} )
433
289
} , [
434
- show ,
290
+ imperativeOptions ?. position ,
291
+ imperativeOptions ?. place ,
292
+ position ,
293
+ float ,
435
294
activeAnchor ,
436
- content ,
437
- externalStyles ,
438
295
place ,
439
- imperativeOptions ?. place ,
440
296
offset ,
441
297
positionStrategy ,
442
- position ,
443
- imperativeOptions ?. position ,
444
- float ,
298
+ middlewares ,
299
+ border ,
300
+ handleTooltipPosition ,
445
301
] )
446
302
447
303
useEffect ( ( ) => {
@@ -451,15 +307,172 @@ const Tooltip = ({
451
307
* - `handleMouseEvents()`
452
308
* - `handleGlobalCloseEvents()`
453
309
* - `handleAnchorEvents()`
454
- * - ?
310
+ * - ...
455
311
*/
456
312
313
+ const handlePointerMove = ( event ?: Event ) => {
314
+ if ( ! event ) {
315
+ return
316
+ }
317
+ const mouseEvent = event as MouseEvent
318
+ const mousePosition = {
319
+ x : mouseEvent . clientX ,
320
+ y : mouseEvent . clientY ,
321
+ }
322
+ handleTooltipPosition ( mousePosition )
323
+ lastFloatPosition . current = mousePosition
324
+ }
325
+
326
+ const handleClickOutsideAnchors = ( event : MouseEvent ) => {
327
+ if ( ! show ) {
328
+ return
329
+ }
330
+ const target = event . target as HTMLElement
331
+ if ( ! target . isConnected ) {
332
+ return
333
+ }
334
+ if ( tooltipRef . current ?. contains ( target ) ) {
335
+ return
336
+ }
337
+ if ( anchorElements . some ( ( anchor ) => anchor ?. contains ( target ) ) ) {
338
+ return
339
+ }
340
+ handleShow ( false )
341
+ if ( tooltipShowDelayTimerRef . current ) {
342
+ clearTimeout ( tooltipShowDelayTimerRef . current )
343
+ }
344
+ }
345
+
346
+ const handleShowTooltip = ( event ?: Event ) => {
347
+ if ( ! event ) {
348
+ return
349
+ }
350
+ const target = ( event . currentTarget ?? event . target ) as HTMLElement | null
351
+ if ( ! target ?. isConnected ) {
352
+ /**
353
+ * this happens when the target is removed from the DOM
354
+ * at the same time the tooltip gets triggered
355
+ */
356
+ setActiveAnchor ( null )
357
+ return
358
+ }
359
+ if ( delayShow ) {
360
+ handleShowTooltipDelayed ( )
361
+ } else {
362
+ handleShow ( true )
363
+ }
364
+ setActiveAnchor ( target )
365
+
366
+ if ( tooltipHideDelayTimerRef . current ) {
367
+ clearTimeout ( tooltipHideDelayTimerRef . current )
368
+ }
369
+ }
370
+
371
+ const handleHideTooltip = ( ) => {
372
+ if ( clickable ) {
373
+ // allow time for the mouse to reach the tooltip, in case there's a gap
374
+ handleHideTooltipDelayed ( delayHide || 100 )
375
+ } else if ( delayHide ) {
376
+ handleHideTooltipDelayed ( )
377
+ } else {
378
+ handleShow ( false )
379
+ }
380
+
381
+ if ( tooltipShowDelayTimerRef . current ) {
382
+ clearTimeout ( tooltipShowDelayTimerRef . current )
383
+ }
384
+ }
385
+
386
+ // debounce handler to prevent call twice when
387
+ // mouse enter and focus events being triggered toggether
388
+ const internalDebouncedHandleShowTooltip = debounce ( handleShowTooltip , 50 , true )
389
+ const internalDebouncedHandleHideTooltip = debounce ( handleHideTooltip , 50 , true )
390
+ // If either of the functions is called while the other is still debounced,
391
+ // reset the timeout. Otherwise if there is a sub-50ms (leave A, enter B, leave B)
392
+ // sequence of events, the tooltip will stay open because the hide debounce
393
+ // from leave A prevented the leave B event from calling it, leaving the
394
+ // tooltip visible.
395
+ const debouncedHandleShowTooltip = ( e ?: Event ) => {
396
+ internalDebouncedHandleHideTooltip . cancel ( )
397
+ internalDebouncedHandleShowTooltip ( e )
398
+ }
399
+ const debouncedHandleHideTooltip = ( ) => {
400
+ internalDebouncedHandleShowTooltip . cancel ( )
401
+ internalDebouncedHandleHideTooltip ( )
402
+ }
403
+
457
404
const handleScrollResize = ( ) => {
458
405
handleShow ( false )
459
406
}
460
407
461
- const anchorScrollParent = getScrollParent ( activeAnchor )
408
+ const hasClickEvent =
409
+ openOnClick || openEvents ?. click || openEvents ?. dblclick || openEvents ?. mousedown
410
+ const actualOpenEvents : AnchorOpenEvents = openEvents
411
+ ? { ...openEvents }
412
+ : {
413
+ mouseenter : true ,
414
+ focus : true ,
415
+ click : false ,
416
+ dblclick : false ,
417
+ mousedown : false ,
418
+ }
419
+ if ( ! openEvents && openOnClick ) {
420
+ Object . assign ( actualOpenEvents , {
421
+ mouseenter : false ,
422
+ focus : false ,
423
+ click : true ,
424
+ } )
425
+ }
426
+ const actualCloseEvents : AnchorCloseEvents = closeEvents
427
+ ? { ...closeEvents }
428
+ : {
429
+ mouseleave : true ,
430
+ blur : true ,
431
+ click : false ,
432
+ dblclick : false ,
433
+ mouseup : false ,
434
+ }
435
+ if ( ! closeEvents && openOnClick ) {
436
+ Object . assign ( actualCloseEvents , {
437
+ mouseleave : false ,
438
+ blur : false ,
439
+ } )
440
+ }
441
+ const actualGlobalCloseEvents : GlobalCloseEvents = globalCloseEvents
442
+ ? { ...globalCloseEvents }
443
+ : {
444
+ escape : false ,
445
+ scroll : false ,
446
+ resize : false ,
447
+ clickOutsideAnchor : hasClickEvent || false ,
448
+ }
449
+
450
+ if ( imperativeModeOnly ) {
451
+ Object . assign ( actualOpenEvents , {
452
+ mouseenter : false ,
453
+ focus : false ,
454
+ click : false ,
455
+ dblclick : false ,
456
+ mousedown : false ,
457
+ } )
458
+ Object . assign ( actualCloseEvents , {
459
+ mouseleave : false ,
460
+ blur : false ,
461
+ click : false ,
462
+ dblclick : false ,
463
+ mouseup : false ,
464
+ } )
465
+ Object . assign ( actualGlobalCloseEvents , {
466
+ escape : false ,
467
+ scroll : false ,
468
+ resize : false ,
469
+ clickOutsideAnchor : false ,
470
+ } )
471
+ }
472
+
473
+ const tooltipElement = tooltipRef . current
462
474
const tooltipScrollParent = getScrollParent ( tooltipRef . current )
475
+ const anchorScrollParent = getScrollParent ( activeAnchor )
463
476
464
477
if ( actualGlobalCloseEvents . scroll ) {
465
478
window . addEventListener ( 'scroll' , handleScrollResize )
@@ -567,8 +580,8 @@ const Tooltip = ({
567
580
if ( clickable && ! hasClickEvent ) {
568
581
// used to keep the tooltip open when hovering content.
569
582
// not needed if using click events.
570
- tooltipRef . current ?. addEventListener ( 'mouseenter' , handleMouseEnterTooltip )
571
- tooltipRef . current ?. addEventListener ( 'mouseleave' , handleMouseLeaveTooltip )
583
+ tooltipElement ?. addEventListener ( 'mouseenter' , handleMouseEnterTooltip )
584
+ tooltipElement ?. addEventListener ( 'mouseleave' , handleMouseLeaveTooltip )
572
585
}
573
586
574
587
enabledEvents . forEach ( ( { event, listener } ) => {
@@ -595,8 +608,8 @@ const Tooltip = ({
595
608
window . removeEventListener ( 'keydown' , handleEsc )
596
609
}
597
610
if ( clickable && ! hasClickEvent ) {
598
- tooltipRef . current ?. removeEventListener ( 'mouseenter' , handleMouseEnterTooltip )
599
- tooltipRef . current ?. removeEventListener ( 'mouseleave' , handleMouseLeaveTooltip )
611
+ tooltipElement ?. removeEventListener ( 'mouseenter' , handleMouseEnterTooltip )
612
+ tooltipElement ?. removeEventListener ( 'mouseleave' , handleMouseLeaveTooltip )
600
613
}
601
614
enabledEvents . forEach ( ( { event, listener } ) => {
602
615
anchorElements . forEach ( ( anchor ) => {
@@ -610,16 +623,23 @@ const Tooltip = ({
610
623
*/
611
624
} , [
612
625
activeAnchor ,
613
- updateTooltipPosition ,
614
- rendered ,
615
626
anchorElements ,
616
- // the effect uses the `actual*Events` objects, but this should work
617
- openEvents ,
627
+ clickable ,
618
628
closeEvents ,
619
- globalCloseEvents ,
620
- hasClickEvent ,
621
- delayShow ,
622
629
delayHide ,
630
+ delayShow ,
631
+ float ,
632
+ globalCloseEvents ,
633
+ handleHideTooltipDelayed ,
634
+ handleShow ,
635
+ handleShowTooltipDelayed ,
636
+ handleTooltipPosition ,
637
+ imperativeModeOnly ,
638
+ openEvents ,
639
+ openOnClick ,
640
+ setActiveAnchor ,
641
+ show ,
642
+ updateTooltipPosition ,
623
643
] )
624
644
625
645
useEffect ( ( ) => {
@@ -749,7 +769,7 @@ const Tooltip = ({
749
769
return ( ) => {
750
770
documentObserver . disconnect ( )
751
771
}
752
- } , [ id , anchorSelect , imperativeOptions ?. anchorSelect , activeAnchor ] )
772
+ } , [ id , anchorSelect , imperativeOptions ?. anchorSelect , activeAnchor , handleShow , setActiveAnchor ] )
753
773
754
774
useEffect ( ( ) => {
755
775
updateTooltipPosition ( )
@@ -766,7 +786,7 @@ const Tooltip = ({
766
786
return ( ) => {
767
787
contentObserver . disconnect ( )
768
788
}
769
- } , [ content , contentWrapperRef ?. current ] )
789
+ } , [ content , contentWrapperRef , updateTooltipPosition ] )
770
790
771
791
useEffect ( ( ) => {
772
792
if ( ! activeAnchor || ! anchorElements . includes ( activeAnchor ) ) {
@@ -777,7 +797,7 @@ const Tooltip = ({
777
797
*/
778
798
setActiveAnchor ( anchorElements [ 0 ] ?? null )
779
799
}
780
- } , [ anchorElements , activeAnchor ] )
800
+ } , [ anchorElements , activeAnchor , setActiveAnchor ] )
781
801
782
802
useEffect ( ( ) => {
783
803
if ( defaultIsOpen ) {
@@ -791,7 +811,7 @@ const Tooltip = ({
791
811
clearTimeout ( tooltipHideDelayTimerRef . current )
792
812
}
793
813
}
794
- } , [ ] )
814
+ } , [ defaultIsOpen , handleShow ] )
795
815
796
816
useEffect ( ( ) => {
797
817
let selector = imperativeOptions ?. anchorSelect ?? anchorSelect
@@ -815,7 +835,7 @@ const Tooltip = ({
815
835
clearTimeout ( tooltipShowDelayTimerRef . current )
816
836
handleShowTooltipDelayed ( delayShow )
817
837
}
818
- } , [ delayShow ] )
838
+ } , [ delayShow , handleShowTooltipDelayed ] )
819
839
820
840
const actualContent = imperativeOptions ?. content ?? content
821
841
const canShow = show && Object . keys ( computedPosition . tooltipStyles ) . length > 0
0 commit comments