react-tooltip 6.0.0-beta.1179.rc.8 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,11 +43,9 @@
43
43
  return;
44
44
  }
45
45
  if (type === 'core') {
46
- // eslint-disable-next-line no-param-reassign
47
46
  id = REACT_TOOLTIP_CORE_STYLES_ID;
48
47
  }
49
48
  if (!ref) {
50
- // eslint-disable-next-line no-param-reassign
51
49
  ref = {};
52
50
  }
53
51
  const { insertAt } = ref;
@@ -78,7 +76,6 @@
78
76
  style.appendChild(document.createTextNode(css));
79
77
  }
80
78
  if (typeof state[type] !== 'undefined') {
81
- // eslint-disable-next-line no-param-reassign
82
79
  state[type] = true;
83
80
  }
84
81
  else {
@@ -86,16 +83,12 @@
86
83
  }
87
84
  }
88
85
 
89
- const computeTooltipPosition = async ({ elementReference = null, tooltipReference = null, tooltipArrowReference = null, place = 'top', offset: offsetValue = 10, strategy = 'absolute', middlewares = [
90
- dom.offset(Number(offsetValue)),
91
- dom.flip({
92
- fallbackAxisSideDirection: 'start',
93
- }),
94
- dom.shift({ padding: 5 }),
95
- ], border, arrowSize = 8, }) => {
86
+ // Hoisted constant middlewares these configs never change
87
+ const defaultFlip = dom.flip({ fallbackAxisSideDirection: 'start' });
88
+ const defaultShift = dom.shift({ padding: 5 });
89
+ const computeTooltipPosition = async ({ elementReference = null, tooltipReference = null, tooltipArrowReference = null, place = 'top', offset: offsetValue = 10, strategy = 'absolute', middlewares = [dom.offset(Number(offsetValue)), defaultFlip, defaultShift], border, arrowSize = 8, }) => {
96
90
  if (!elementReference) {
97
91
  // elementReference can be null or undefined and we will not compute the position
98
- // eslint-disable-next-line no-console
99
92
  // console.error('The reference element for tooltip was not defined: ', elementReference)
100
93
  return { tooltipStyles: {}, tooltipArrowStyles: {}, place };
101
94
  }
@@ -180,6 +173,7 @@
180
173
  */
181
174
  const debounce = (func, wait, immediate) => {
182
175
  let timeout = null;
176
+ let currentFunc = func;
183
177
  const debounced = function debounced(...args) {
184
178
  const later = () => {
185
179
  timeout = null;
@@ -189,7 +183,7 @@
189
183
  * there's no need to clear the timeout
190
184
  * since we expect it to resolve and set `timeout = null`
191
185
  */
192
- func.apply(this, args);
186
+ currentFunc.apply(this, args);
193
187
  timeout = setTimeout(later, wait);
194
188
  }
195
189
  };
@@ -202,36 +196,12 @@
202
196
  clearTimeout(timeout);
203
197
  timeout = null;
204
198
  };
199
+ debounced.setCallback = (newFunc) => {
200
+ currentFunc = newFunc;
201
+ };
205
202
  return debounced;
206
203
  };
207
204
 
208
- const isObject = (object) => {
209
- return object !== null && !Array.isArray(object) && typeof object === 'object';
210
- };
211
- const deepEqual = (object1, object2) => {
212
- if (object1 === object2) {
213
- return true;
214
- }
215
- if (Array.isArray(object1) && Array.isArray(object2)) {
216
- if (object1.length !== object2.length) {
217
- return false;
218
- }
219
- return object1.every((val, index) => deepEqual(val, object2[index]));
220
- }
221
- if (Array.isArray(object1) !== Array.isArray(object2)) {
222
- return false;
223
- }
224
- if (!isObject(object1) || !isObject(object2)) {
225
- return object1 === object2;
226
- }
227
- const keys1 = Object.keys(object1);
228
- const keys2 = Object.keys(object2);
229
- if (keys1.length !== keys2.length) {
230
- return false;
231
- }
232
- return keys1.every((key) => deepEqual(object1[key], object2[key]));
233
- };
234
-
235
205
  const isScrollable = (node) => {
236
206
  if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
237
207
  return false;
@@ -272,17 +242,43 @@
272
242
  const clearTimeoutRef = (ref) => {
273
243
  if (ref.current) {
274
244
  clearTimeout(ref.current);
275
- // eslint-disable-next-line no-param-reassign
276
245
  ref.current = null;
277
246
  }
278
247
  };
279
248
 
249
+ function parseDataTooltipIdSelector(selector) {
250
+ const match = selector.match(/^\[data-tooltip-id=(['"])((?:\\.|(?!\1).)*)\1\]$/);
251
+ if (!match) {
252
+ return null;
253
+ }
254
+ return match[2].replace(/\\(['"])/g, '$1');
255
+ }
256
+
257
+ function resolveDataTooltipAnchor(targetElement, tooltipId) {
258
+ let currentElement = targetElement;
259
+ while (currentElement) {
260
+ if (currentElement.dataset.tooltipId === tooltipId) {
261
+ return currentElement;
262
+ }
263
+ currentElement = currentElement.parentElement;
264
+ }
265
+ return null;
266
+ }
267
+
280
268
  var coreStyles = {"tooltip":"core-styles-module_tooltip__3vRRp","fixed":"core-styles-module_fixed__pcSol","arrow":"core-styles-module_arrow__cvMwQ","content":"core-styles-module_content__BRKdB","noArrow":"core-styles-module_noArrow__xock6","clickable":"core-styles-module_clickable__ZuTTB","show":"core-styles-module_show__Nt9eE","closing":"core-styles-module_closing__sGnxF"};
281
269
 
282
270
  var styles = {"tooltip":"styles-module_tooltip__mnnfp","content":"styles-module_content__ydYdI","arrow":"styles-module_arrow__K0L3T","dark":"styles-module_dark__xNqje","light":"styles-module_light__Z6W-X","success":"styles-module_success__A2AKt","warning":"styles-module_warning__SCK0X","error":"styles-module_error__JvumD","info":"styles-module_info__BWdHW"};
283
271
 
284
272
  const registry = new Map();
285
273
  let documentObserver = null;
274
+ /**
275
+ * Extract a tooltip ID from a simple `[data-tooltip-id='value']` selector.
276
+ * Returns null for complex or custom selectors.
277
+ */
278
+ function extractTooltipId(selector) {
279
+ const match = selector.match(/^\[data-tooltip-id=(['"])((?:\\.|(?!\1).)*)\1\]$/);
280
+ return match ? match[2].replace(/\\(['"])/g, '$1') : null;
281
+ }
286
282
  function areAnchorListsEqual(left, right) {
287
283
  if (left.length !== right.length) {
288
284
  return false;
@@ -304,8 +300,7 @@
304
300
  }
305
301
  }
306
302
  function notifySubscribers(entry) {
307
- const anchors = [...entry.anchors];
308
- entry.subscribers.forEach((subscriber) => subscriber(anchors, entry.error));
303
+ entry.subscribers.forEach((subscriber) => subscriber(entry.anchors, entry.error));
309
304
  }
310
305
  function refreshEntry(selector, entry) {
311
306
  var _a, _b, _c, _d;
@@ -329,12 +324,117 @@
329
324
  refreshEntry(selector, entry);
330
325
  });
331
326
  }
327
+ let refreshScheduled = false;
328
+ let pendingTooltipIds = null;
329
+ let pendingFullRefresh = false;
330
+ function scheduleRefresh(affectedTooltipIds) {
331
+ if (affectedTooltipIds) {
332
+ if (!pendingTooltipIds) {
333
+ pendingTooltipIds = new Set();
334
+ }
335
+ affectedTooltipIds.forEach((id) => pendingTooltipIds.add(id));
336
+ }
337
+ else {
338
+ pendingFullRefresh = true;
339
+ }
340
+ if (refreshScheduled) {
341
+ return;
342
+ }
343
+ refreshScheduled = true;
344
+ const flush = () => {
345
+ refreshScheduled = false;
346
+ const fullRefresh = pendingFullRefresh;
347
+ const ids = pendingTooltipIds;
348
+ pendingFullRefresh = false;
349
+ pendingTooltipIds = null;
350
+ if (fullRefresh) {
351
+ refreshAllEntries();
352
+ }
353
+ else if (ids && ids.size > 0) {
354
+ refreshEntriesForTooltipIds(ids);
355
+ }
356
+ };
357
+ if (typeof requestAnimationFrame === 'function') {
358
+ requestAnimationFrame(flush);
359
+ }
360
+ else {
361
+ Promise.resolve().then(flush);
362
+ }
363
+ }
364
+ /**
365
+ * Only refresh entries whose tooltipId is in the affected set,
366
+ * plus any entries with custom (non-tooltipId) selectors.
367
+ */
368
+ function refreshEntriesForTooltipIds(affectedIds) {
369
+ registry.forEach((entry, selector) => {
370
+ if (entry.tooltipId === null || affectedIds.has(entry.tooltipId)) {
371
+ refreshEntry(selector, entry);
372
+ }
373
+ });
374
+ }
375
+ /**
376
+ * Collect tooltip IDs from mutation records. Returns null when targeted
377
+ * analysis is not worthwhile (few registry entries, or too many nodes to scan).
378
+ */
379
+ function collectAffectedTooltipIds(records) {
380
+ var _a;
381
+ // Targeted refresh only pays off when there are many distinct selectors.
382
+ // With few entries, full refresh is already cheap — skip the analysis overhead.
383
+ if (registry.size <= 4) {
384
+ return null;
385
+ }
386
+ const ids = new Set();
387
+ for (const record of records) {
388
+ if (record.type === 'attributes') {
389
+ const target = record.target;
390
+ const currentId = (_a = target.getAttribute) === null || _a === void 0 ? void 0 : _a.call(target, 'data-tooltip-id');
391
+ if (currentId)
392
+ ids.add(currentId);
393
+ if (record.oldValue)
394
+ ids.add(record.oldValue);
395
+ continue;
396
+ }
397
+ if (record.type === 'childList') {
398
+ const gatherIds = (nodes) => {
399
+ var _a, _b;
400
+ for (let i = 0; i < nodes.length; i++) {
401
+ const node = nodes[i];
402
+ if (node.nodeType !== Node.ELEMENT_NODE)
403
+ continue;
404
+ const el = node;
405
+ const id = (_a = el.getAttribute) === null || _a === void 0 ? void 0 : _a.call(el, 'data-tooltip-id');
406
+ if (id)
407
+ ids.add(id);
408
+ // For large subtrees, bail out to full refresh to avoid double-scanning
409
+ const descendants = (_b = el.querySelectorAll) === null || _b === void 0 ? void 0 : _b.call(el, '[data-tooltip-id]');
410
+ if (descendants) {
411
+ if (descendants.length > 50) {
412
+ return true; // signal bail-out
413
+ }
414
+ for (let j = 0; j < descendants.length; j++) {
415
+ const descId = descendants[j].getAttribute('data-tooltip-id');
416
+ if (descId)
417
+ ids.add(descId);
418
+ }
419
+ }
420
+ }
421
+ return false;
422
+ };
423
+ if (gatherIds(record.addedNodes) || gatherIds(record.removedNodes)) {
424
+ return null; // large mutation — full refresh is cheaper
425
+ }
426
+ continue;
427
+ }
428
+ }
429
+ return ids;
430
+ }
332
431
  function ensureDocumentObserver() {
333
432
  if (documentObserver || typeof MutationObserver === 'undefined') {
334
433
  return;
335
434
  }
336
- documentObserver = new MutationObserver(() => {
337
- refreshAllEntries();
435
+ documentObserver = new MutationObserver((records) => {
436
+ const affectedIds = collectAffectedTooltipIds(records);
437
+ scheduleRefresh(affectedIds);
338
438
  });
339
439
  documentObserver.observe(document.body, {
340
440
  childList: true,
@@ -359,6 +459,7 @@
359
459
  anchors: initialState.anchors,
360
460
  error: initialState.error,
361
461
  subscribers: new Set(),
462
+ tooltipId: extractTooltipId(selector),
362
463
  };
363
464
  registry.set(selector, entry);
364
465
  }
@@ -386,7 +487,7 @@
386
487
  }
387
488
  return selector;
388
489
  };
389
- const useTooltipAnchors = ({ id, anchorSelect, imperativeAnchorSelect, activeAnchor, disableTooltip, onActiveAnchorRemoved, }) => {
490
+ const useTooltipAnchors = ({ id, anchorSelect, imperativeAnchorSelect, activeAnchor, disableTooltip, onActiveAnchorRemoved, trackAnchors, }) => {
390
491
  const [rawAnchorElements, setRawAnchorElements] = React.useState([]);
391
492
  const [selectorError, setSelectorError] = React.useState(null);
392
493
  const warnedSelectorRef = React.useRef(null);
@@ -402,9 +503,10 @@
402
503
  catch (_a) {
403
504
  return false;
404
505
  }
405
- }, [activeAnchor, selector]);
506
+ // eslint-disable-next-line react-hooks/exhaustive-deps
507
+ }, [activeAnchor, selector, anchorElements]);
406
508
  React.useEffect(() => {
407
- if (!selector) {
509
+ if (!selector || !trackAnchors) {
408
510
  setRawAnchorElements([]);
409
511
  setSelectorError(null);
410
512
  return undefined;
@@ -413,7 +515,7 @@
413
515
  setRawAnchorElements(anchors);
414
516
  setSelectorError(error);
415
517
  });
416
- }, [selector]);
518
+ }, [selector, trackAnchors]);
417
519
  React.useEffect(() => {
418
520
  if (!selectorError || warnedSelectorRef.current === selector) {
419
521
  return;
@@ -433,111 +535,81 @@
433
535
  onActiveAnchorRemoved();
434
536
  }
435
537
  }, [activeAnchor, anchorElements, activeAnchorMatchesSelector, onActiveAnchorRemoved]);
436
- return anchorElements;
538
+ return {
539
+ anchorElements,
540
+ selector,
541
+ };
437
542
  };
438
543
 
439
- const useTooltipEvents = ({ activeAnchor, anchorElements, clickable, closeEvents, delayHide, delayShow, disableTooltip, float, globalCloseEvents, handleHideTooltipDelayed, handleShow, handleShowTooltipDelayed, handleTooltipPosition, hoveringTooltip, imperativeModeOnly, lastFloatPosition, openEvents, openOnClick, setActiveAnchor, show, tooltipHideDelayTimerRef, tooltipRef, tooltipShowDelayTimerRef, updateTooltipPosition, }) => {
440
- React.useEffect(() => {
441
- const resolveAnchorElement = (target) => {
442
- var _a;
443
- const targetElement = target;
444
- if (!(targetElement === null || targetElement === void 0 ? void 0 : targetElement.isConnected)) {
445
- return null;
446
- }
447
- return ((_a = anchorElements.find((anchor) => anchor === targetElement || anchor.contains(targetElement))) !== null && _a !== void 0 ? _a : null);
448
- };
449
- const handlePointerMove = (event) => {
450
- if (!event) {
451
- return;
452
- }
453
- if (!activeAnchor) {
454
- return;
455
- }
456
- const targetAnchor = resolveAnchorElement(event.target);
457
- if (targetAnchor !== activeAnchor) {
458
- return;
459
- }
460
- const mouseEvent = event;
461
- const mousePosition = {
462
- x: mouseEvent.clientX,
463
- y: mouseEvent.clientY,
464
- };
465
- handleTooltipPosition(mousePosition);
466
- // eslint-disable-next-line no-param-reassign
467
- lastFloatPosition.current = mousePosition;
468
- };
469
- const handleClickOutsideAnchors = (event) => {
470
- var _a;
471
- if (!show) {
472
- return;
473
- }
474
- const target = event.target;
475
- if (!target.isConnected) {
476
- return;
477
- }
478
- if ((_a = tooltipRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) {
479
- return;
480
- }
481
- if (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.contains(target)) {
482
- return;
483
- }
484
- if (anchorElements.some((anchor) => anchor === null || anchor === void 0 ? void 0 : anchor.contains(target))) {
485
- return;
486
- }
487
- handleShow(false);
488
- clearTimeoutRef(tooltipShowDelayTimerRef);
489
- };
490
- const handleShowTooltip = (anchor) => {
491
- if (!anchor) {
492
- return;
493
- }
494
- if (!anchor.isConnected) {
495
- setActiveAnchor(null);
496
- return;
497
- }
498
- if (disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(anchor)) {
499
- return;
500
- }
501
- if (delayShow) {
502
- handleShowTooltipDelayed();
503
- }
504
- else {
505
- handleShow(true);
506
- }
507
- setActiveAnchor(anchor);
508
- if (tooltipHideDelayTimerRef.current) {
509
- clearTimeout(tooltipHideDelayTimerRef.current);
510
- }
511
- };
512
- const handleHideTooltip = () => {
513
- if (clickable) {
514
- handleHideTooltipDelayed(delayHide || 100);
515
- }
516
- else if (delayHide) {
517
- handleHideTooltipDelayed();
518
- }
519
- else {
520
- handleShow(false);
521
- }
522
- if (tooltipShowDelayTimerRef.current) {
523
- clearTimeout(tooltipShowDelayTimerRef.current);
524
- }
525
- };
526
- const internalDebouncedHandleShowTooltip = debounce(handleShowTooltip, 50);
527
- const internalDebouncedHandleHideTooltip = debounce(handleHideTooltip, 50);
528
- const debouncedHandleShowTooltip = (anchor) => {
529
- internalDebouncedHandleHideTooltip.cancel();
530
- internalDebouncedHandleShowTooltip(anchor);
531
- };
532
- const debouncedHandleHideTooltip = () => {
533
- internalDebouncedHandleShowTooltip.cancel();
534
- internalDebouncedHandleHideTooltip();
535
- };
536
- const handleScrollResize = () => {
537
- handleShow(false);
538
- };
539
- const hasClickEvent = openOnClick || (openEvents === null || openEvents === void 0 ? void 0 : openEvents.click) || (openEvents === null || openEvents === void 0 ? void 0 : openEvents.dblclick) || (openEvents === null || openEvents === void 0 ? void 0 : openEvents.mousedown);
540
- const actualOpenEvents = openEvents
544
+ /**
545
+ * Shared document event delegation.
546
+ *
547
+ * Instead of N tooltips each calling document.addEventListener(type, handler),
548
+ * we maintain ONE document listener per event type. When the event fires,
549
+ * we iterate through all registered handlers for that type.
550
+ *
551
+ * This reduces document-level listeners from O(N × eventTypes) to O(eventTypes).
552
+ */
553
+ const handlersByType = new Map();
554
+ function getOrCreateSet(eventType) {
555
+ let set = handlersByType.get(eventType);
556
+ if (!set) {
557
+ set = new Set();
558
+ handlersByType.set(eventType, set);
559
+ document.addEventListener(eventType, dispatch);
560
+ }
561
+ return set;
562
+ }
563
+ function dispatch(event) {
564
+ const handlers = handlersByType.get(event.type);
565
+ if (handlers) {
566
+ // Safe to iterate directly — mutations (add/remove) only happen in
567
+ // setup/cleanup, not during dispatch. Set iteration is stable for
568
+ // entries that existed when iteration began.
569
+ handlers.forEach((handler) => {
570
+ handler(event);
571
+ });
572
+ }
573
+ }
574
+ /**
575
+ * Register a handler for a document-level event type.
576
+ * Returns an unsubscribe function.
577
+ */
578
+ function addDelegatedEventListener(eventType, handler) {
579
+ const set = getOrCreateSet(eventType);
580
+ set.add(handler);
581
+ return () => {
582
+ set.delete(handler);
583
+ if (set.size === 0) {
584
+ handlersByType.delete(eventType);
585
+ document.removeEventListener(eventType, dispatch);
586
+ }
587
+ };
588
+ }
589
+
590
+ const useTooltipEvents = ({ activeAnchor, anchorElements, anchorSelector, clickable, closeEvents, delayHide, delayShow, disableTooltip, float, globalCloseEvents, handleHideTooltipDelayed, handleShow, handleShowTooltipDelayed, handleTooltipPosition, hoveringTooltip, imperativeModeOnly, lastFloatPosition, openEvents, openOnClick, setActiveAnchor, show, tooltipHideDelayTimerRef, tooltipRef, tooltipShowDelayTimerRef, updateTooltipPosition, }) => {
591
+ // Ref-stable debounced handlers — avoids recreating debounce instances on every effect run
592
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
593
+ const debouncedShowRef = React.useRef(debounce((_anchor) => { }, 50));
594
+ const debouncedHideRef = React.useRef(debounce(() => { }, 50));
595
+ // Cache scroll parents only recompute when the element actually changes
596
+ const anchorScrollParentRef = React.useRef(null);
597
+ const tooltipScrollParentRef = React.useRef(null);
598
+ const prevAnchorRef = React.useRef(null);
599
+ const prevTooltipRef = React.useRef(null);
600
+ if (activeAnchor !== prevAnchorRef.current) {
601
+ prevAnchorRef.current = activeAnchor;
602
+ anchorScrollParentRef.current = getScrollParent(activeAnchor);
603
+ }
604
+ const currentTooltipEl = tooltipRef.current;
605
+ if (currentTooltipEl !== prevTooltipRef.current) {
606
+ prevTooltipRef.current = currentTooltipEl;
607
+ tooltipScrollParentRef.current = getScrollParent(currentTooltipEl);
608
+ }
609
+ // Memoize event config objects — only rebuild when the relevant props change
610
+ const hasClickEvent = openOnClick || (openEvents === null || openEvents === void 0 ? void 0 : openEvents.click) || (openEvents === null || openEvents === void 0 ? void 0 : openEvents.dblclick) || (openEvents === null || openEvents === void 0 ? void 0 : openEvents.mousedown);
611
+ const actualOpenEvents = React.useMemo(() => {
612
+ const events = openEvents
541
613
  ? { ...openEvents }
542
614
  : {
543
615
  mouseenter: true,
@@ -547,13 +619,25 @@
547
619
  mousedown: false,
548
620
  };
549
621
  if (!openEvents && openOnClick) {
550
- Object.assign(actualOpenEvents, {
622
+ Object.assign(events, {
551
623
  mouseenter: false,
552
624
  focus: false,
553
625
  click: true,
554
626
  });
555
627
  }
556
- const actualCloseEvents = closeEvents
628
+ if (imperativeModeOnly) {
629
+ Object.assign(events, {
630
+ mouseenter: false,
631
+ focus: false,
632
+ click: false,
633
+ dblclick: false,
634
+ mousedown: false,
635
+ });
636
+ }
637
+ return events;
638
+ }, [openEvents, openOnClick, imperativeModeOnly]);
639
+ const actualCloseEvents = React.useMemo(() => {
640
+ const events = closeEvents
557
641
  ? { ...closeEvents }
558
642
  : {
559
643
  mouseleave: true,
@@ -563,12 +647,24 @@
563
647
  mouseup: false,
564
648
  };
565
649
  if (!closeEvents && openOnClick) {
566
- Object.assign(actualCloseEvents, {
650
+ Object.assign(events, {
567
651
  mouseleave: false,
568
652
  blur: false,
569
653
  });
570
654
  }
571
- const actualGlobalCloseEvents = globalCloseEvents
655
+ if (imperativeModeOnly) {
656
+ Object.assign(events, {
657
+ mouseleave: false,
658
+ blur: false,
659
+ click: false,
660
+ dblclick: false,
661
+ mouseup: false,
662
+ });
663
+ }
664
+ return events;
665
+ }, [closeEvents, openOnClick, imperativeModeOnly]);
666
+ const actualGlobalCloseEvents = React.useMemo(() => {
667
+ const events = globalCloseEvents
572
668
  ? { ...globalCloseEvents }
573
669
  : {
574
670
  escape: false,
@@ -577,108 +673,157 @@
577
673
  clickOutsideAnchor: hasClickEvent || false,
578
674
  };
579
675
  if (imperativeModeOnly) {
580
- Object.assign(actualOpenEvents, {
581
- mouseenter: false,
582
- focus: false,
583
- click: false,
584
- dblclick: false,
585
- mousedown: false,
586
- });
587
- Object.assign(actualCloseEvents, {
588
- mouseleave: false,
589
- blur: false,
590
- click: false,
591
- dblclick: false,
592
- mouseup: false,
593
- });
594
- Object.assign(actualGlobalCloseEvents, {
676
+ Object.assign(events, {
595
677
  escape: false,
596
678
  scroll: false,
597
679
  resize: false,
598
680
  clickOutsideAnchor: false,
599
681
  });
600
682
  }
601
- const tooltipElement = tooltipRef.current;
602
- const tooltipScrollParent = getScrollParent(tooltipRef.current);
603
- const anchorScrollParent = getScrollParent(activeAnchor);
604
- if (actualGlobalCloseEvents.scroll) {
605
- window.addEventListener('scroll', handleScrollResize);
606
- anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.addEventListener('scroll', handleScrollResize);
607
- tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.addEventListener('scroll', handleScrollResize);
683
+ return events;
684
+ }, [globalCloseEvents, hasClickEvent, imperativeModeOnly]);
685
+ // --- Refs for values read inside event handlers (avoids effect deps) ---
686
+ const activeAnchorRef = React.useRef(activeAnchor);
687
+ activeAnchorRef.current = activeAnchor;
688
+ const showRef = React.useRef(show);
689
+ showRef.current = show;
690
+ const anchorElementsRef = React.useRef(anchorElements);
691
+ anchorElementsRef.current = anchorElements;
692
+ const handleShowRef = React.useRef(handleShow);
693
+ handleShowRef.current = handleShow;
694
+ const handleTooltipPositionRef = React.useRef(handleTooltipPosition);
695
+ handleTooltipPositionRef.current = handleTooltipPosition;
696
+ const updateTooltipPositionRef = React.useRef(updateTooltipPosition);
697
+ updateTooltipPositionRef.current = updateTooltipPosition;
698
+ // --- Handler refs (updated every render, read via ref indirection in effects) ---
699
+ const resolveAnchorElementRef = React.useRef(() => null);
700
+ const handleShowTooltipRef = React.useRef(() => { });
701
+ const handleHideTooltipRef = React.useRef(() => { });
702
+ const dataTooltipId = anchorSelector ? parseDataTooltipIdSelector(anchorSelector) : null;
703
+ resolveAnchorElementRef.current = (target) => {
704
+ var _a, _b;
705
+ const targetElement = target;
706
+ if (!(targetElement === null || targetElement === void 0 ? void 0 : targetElement.isConnected)) {
707
+ return null;
608
708
  }
609
- let updateTooltipCleanup = null;
610
- if (actualGlobalCloseEvents.resize) {
611
- window.addEventListener('resize', handleScrollResize);
709
+ if (dataTooltipId) {
710
+ const matchedAnchor = resolveDataTooltipAnchor(targetElement, dataTooltipId);
711
+ if (matchedAnchor && !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(matchedAnchor))) {
712
+ return matchedAnchor;
713
+ }
612
714
  }
613
- else if (activeAnchor && tooltipRef.current) {
614
- updateTooltipCleanup = dom.autoUpdate(activeAnchor, tooltipRef.current, updateTooltipPosition, {
615
- ancestorResize: true,
616
- elementResize: true,
617
- layoutShift: true,
618
- });
715
+ else if (anchorSelector) {
716
+ try {
717
+ const matchedAnchor = (_a = (targetElement.matches(anchorSelector)
718
+ ? targetElement
719
+ : targetElement.closest(anchorSelector))) !== null && _a !== void 0 ? _a : null;
720
+ if (matchedAnchor && !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(matchedAnchor))) {
721
+ return matchedAnchor;
722
+ }
723
+ }
724
+ catch (_c) {
725
+ return null;
726
+ }
619
727
  }
620
- const handleEsc = (event) => {
621
- if (event.key !== 'Escape') {
622
- return;
728
+ return ((_b = anchorElementsRef.current.find((anchor) => anchor === targetElement || anchor.contains(targetElement))) !== null && _b !== void 0 ? _b : null);
729
+ };
730
+ handleShowTooltipRef.current = (anchor) => {
731
+ if (!anchor) {
732
+ return;
733
+ }
734
+ if (!anchor.isConnected) {
735
+ setActiveAnchor(null);
736
+ return;
737
+ }
738
+ if (disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(anchor)) {
739
+ return;
740
+ }
741
+ if (delayShow) {
742
+ handleShowTooltipDelayed();
743
+ }
744
+ else {
745
+ handleShow(true);
746
+ }
747
+ if (delayShow && activeAnchorRef.current && anchor !== activeAnchorRef.current) {
748
+ // Moving to a different anchor while one is already active — defer the anchor
749
+ // switch until the show delay fires to prevent content/position from updating
750
+ // before visibility transitions complete.
751
+ if (tooltipShowDelayTimerRef.current) {
752
+ clearTimeout(tooltipShowDelayTimerRef.current);
623
753
  }
754
+ tooltipShowDelayTimerRef.current = setTimeout(() => {
755
+ setActiveAnchor(anchor);
756
+ handleShow(true);
757
+ }, delayShow);
758
+ }
759
+ else {
760
+ setActiveAnchor(anchor);
761
+ }
762
+ if (tooltipHideDelayTimerRef.current) {
763
+ clearTimeout(tooltipHideDelayTimerRef.current);
764
+ }
765
+ };
766
+ handleHideTooltipRef.current = () => {
767
+ if (clickable) {
768
+ handleHideTooltipDelayed(delayHide || 100);
769
+ }
770
+ else if (delayHide) {
771
+ handleHideTooltipDelayed();
772
+ }
773
+ else {
624
774
  handleShow(false);
625
- };
626
- if (actualGlobalCloseEvents.escape) {
627
- window.addEventListener('keydown', handleEsc);
628
775
  }
629
- if (actualGlobalCloseEvents.clickOutsideAnchor) {
630
- window.addEventListener('click', handleClickOutsideAnchors);
776
+ if (tooltipShowDelayTimerRef.current) {
777
+ clearTimeout(tooltipShowDelayTimerRef.current);
631
778
  }
632
- const activeAnchorContainsTarget = (event) => Boolean((event === null || event === void 0 ? void 0 : event.target) && (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.contains(event.target)));
633
- const handleClickOpenTooltipAnchor = (event) => {
634
- var _a;
635
- const anchor = resolveAnchorElement((_a = event === null || event === void 0 ? void 0 : event.target) !== null && _a !== void 0 ? _a : null);
636
- if (!anchor) {
637
- return;
638
- }
639
- if (show && activeAnchor === anchor) {
640
- return;
641
- }
642
- handleShowTooltip(anchor);
779
+ };
780
+ // Update debounced callbacks to always delegate to latest handler refs
781
+ const debouncedShow = debouncedShowRef.current;
782
+ const debouncedHide = debouncedHideRef.current;
783
+ debouncedShow.setCallback((anchor) => handleShowTooltipRef.current(anchor));
784
+ debouncedHide.setCallback(() => handleHideTooltipRef.current());
785
+ // --- Effect 1: Delegated anchor events + tooltip hover ---
786
+ // Only re-runs when the set of active event types or interaction mode changes.
787
+ // Handlers read reactive values (activeAnchor, show, etc.) from refs at invocation
788
+ // time, so this effect is decoupled from show/hide state changes.
789
+ React.useEffect(() => {
790
+ const cleanupFns = [];
791
+ const addDelegatedListener = (eventType, listener) => {
792
+ cleanupFns.push(addDelegatedEventListener(eventType, listener));
643
793
  };
644
- const handleClickCloseTooltipAnchor = (event) => {
645
- if (!show || !activeAnchorContainsTarget(event)) {
646
- return;
647
- }
648
- handleHideTooltip();
794
+ const activeAnchorContainsTarget = (event) => { var _a; return Boolean((event === null || event === void 0 ? void 0 : event.target) && ((_a = activeAnchorRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target))); };
795
+ const debouncedHandleShowTooltip = (anchor) => {
796
+ debouncedHide.cancel();
797
+ debouncedShow(anchor);
798
+ };
799
+ const debouncedHandleHideTooltip = () => {
800
+ debouncedShow.cancel();
801
+ debouncedHide();
649
802
  };
650
- const regularEvents = ['mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'focus', 'blur'];
651
- const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup'];
652
- const delegatedEvents = [];
653
803
  const addDelegatedHoverOpenListener = () => {
654
- delegatedEvents.push({
655
- event: 'mouseover',
656
- listener: (event) => {
657
- const anchor = resolveAnchorElement(event.target);
658
- if (!anchor) {
659
- return;
660
- }
661
- const relatedAnchor = resolveAnchorElement(event.relatedTarget);
662
- if (relatedAnchor === anchor) {
663
- return;
664
- }
665
- debouncedHandleShowTooltip(anchor);
666
- },
804
+ addDelegatedListener('mouseover', (event) => {
805
+ const anchor = resolveAnchorElementRef.current(event.target);
806
+ if (!anchor) {
807
+ return;
808
+ }
809
+ const relatedAnchor = resolveAnchorElementRef.current(event.relatedTarget);
810
+ if (relatedAnchor === anchor) {
811
+ return;
812
+ }
813
+ debouncedHandleShowTooltip(anchor);
667
814
  });
668
815
  };
669
816
  const addDelegatedHoverCloseListener = () => {
670
- delegatedEvents.push({
671
- event: 'mouseout',
672
- listener: (event) => {
673
- if (!activeAnchorContainsTarget(event)) {
674
- return;
675
- }
676
- const relatedTarget = event.relatedTarget;
677
- if (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.contains(relatedTarget)) {
678
- return;
679
- }
680
- debouncedHandleHideTooltip();
681
- },
817
+ addDelegatedListener('mouseout', (event) => {
818
+ var _a;
819
+ if (!activeAnchorContainsTarget(event)) {
820
+ return;
821
+ }
822
+ const relatedTarget = event.relatedTarget;
823
+ if ((_a = activeAnchorRef.current) === null || _a === void 0 ? void 0 : _a.contains(relatedTarget)) {
824
+ return;
825
+ }
826
+ debouncedHandleHideTooltip();
682
827
  });
683
828
  };
684
829
  if (actualOpenEvents.mouseenter) {
@@ -694,37 +839,48 @@
694
839
  addDelegatedHoverCloseListener();
695
840
  }
696
841
  if (actualOpenEvents.focus) {
697
- delegatedEvents.push({
698
- event: 'focusin',
699
- listener: (event) => {
700
- debouncedHandleShowTooltip(resolveAnchorElement(event.target));
701
- },
842
+ addDelegatedListener('focusin', (event) => {
843
+ debouncedHandleShowTooltip(resolveAnchorElementRef.current(event.target));
702
844
  });
703
845
  }
704
846
  if (actualCloseEvents.blur) {
705
- delegatedEvents.push({
706
- event: 'focusout',
707
- listener: (event) => {
708
- if (!activeAnchorContainsTarget(event)) {
709
- return;
710
- }
711
- const relatedTarget = event.relatedTarget;
712
- if (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.contains(relatedTarget)) {
713
- return;
714
- }
715
- debouncedHandleHideTooltip();
716
- },
847
+ addDelegatedListener('focusout', (event) => {
848
+ var _a;
849
+ if (!activeAnchorContainsTarget(event)) {
850
+ return;
851
+ }
852
+ const relatedTarget = event.relatedTarget;
853
+ if ((_a = activeAnchorRef.current) === null || _a === void 0 ? void 0 : _a.contains(relatedTarget)) {
854
+ return;
855
+ }
856
+ debouncedHandleHideTooltip();
717
857
  });
718
858
  }
859
+ const regularEvents = ['mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'focus', 'blur'];
860
+ const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup'];
861
+ const handleClickOpenTooltipAnchor = (event) => {
862
+ var _a;
863
+ const anchor = resolveAnchorElementRef.current((_a = event === null || event === void 0 ? void 0 : event.target) !== null && _a !== void 0 ? _a : null);
864
+ if (!anchor) {
865
+ return;
866
+ }
867
+ if (showRef.current && activeAnchorRef.current === anchor) {
868
+ return;
869
+ }
870
+ handleShowTooltipRef.current(anchor);
871
+ };
872
+ const handleClickCloseTooltipAnchor = (event) => {
873
+ if (!showRef.current || !activeAnchorContainsTarget(event)) {
874
+ return;
875
+ }
876
+ handleHideTooltipRef.current();
877
+ };
719
878
  Object.entries(actualOpenEvents).forEach(([event, enabled]) => {
720
879
  if (!enabled || regularEvents.includes(event)) {
721
880
  return;
722
881
  }
723
882
  if (clickEvents.includes(event)) {
724
- delegatedEvents.push({
725
- event,
726
- listener: handleClickOpenTooltipAnchor,
727
- });
883
+ addDelegatedListener(event, handleClickOpenTooltipAnchor);
728
884
  }
729
885
  });
730
886
  Object.entries(actualCloseEvents).forEach(([event, enabled]) => {
@@ -732,38 +888,111 @@
732
888
  return;
733
889
  }
734
890
  if (clickEvents.includes(event)) {
735
- delegatedEvents.push({
736
- event,
737
- listener: handleClickCloseTooltipAnchor,
738
- });
891
+ addDelegatedListener(event, handleClickCloseTooltipAnchor);
739
892
  }
740
893
  });
741
894
  if (float) {
742
- delegatedEvents.push({
743
- event: 'pointermove',
744
- listener: handlePointerMove,
895
+ addDelegatedListener('pointermove', (event) => {
896
+ const currentActiveAnchor = activeAnchorRef.current;
897
+ if (!currentActiveAnchor) {
898
+ return;
899
+ }
900
+ const targetAnchor = resolveAnchorElementRef.current(event.target);
901
+ if (targetAnchor !== currentActiveAnchor) {
902
+ return;
903
+ }
904
+ const mouseEvent = event;
905
+ const mousePosition = {
906
+ x: mouseEvent.clientX,
907
+ y: mouseEvent.clientY,
908
+ };
909
+ handleTooltipPositionRef.current(mousePosition);
910
+ lastFloatPosition.current = mousePosition;
745
911
  });
746
912
  }
913
+ const tooltipElement = tooltipRef.current;
747
914
  const handleMouseOverTooltip = () => {
748
- // eslint-disable-next-line no-param-reassign
749
915
  hoveringTooltip.current = true;
750
916
  };
751
917
  const handleMouseOutTooltip = () => {
752
- // eslint-disable-next-line no-param-reassign
753
918
  hoveringTooltip.current = false;
754
- handleHideTooltip();
919
+ handleHideTooltipRef.current();
755
920
  };
756
921
  const addHoveringTooltipListeners = clickable && (actualCloseEvents.mouseout || actualCloseEvents.mouseleave);
757
922
  if (addHoveringTooltipListeners) {
758
923
  tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseover', handleMouseOverTooltip);
759
924
  tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseout', handleMouseOutTooltip);
760
925
  }
761
- delegatedEvents.forEach(({ event, listener }) => {
762
- document.addEventListener(event, listener);
763
- });
764
926
  return () => {
927
+ cleanupFns.forEach((fn) => fn());
928
+ if (addHoveringTooltipListeners) {
929
+ tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseover', handleMouseOverTooltip);
930
+ tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseout', handleMouseOutTooltip);
931
+ }
932
+ debouncedShow.cancel();
933
+ debouncedHide.cancel();
934
+ };
935
+ // eslint-disable-next-line react-hooks/exhaustive-deps
936
+ }, [actualOpenEvents, actualCloseEvents, float, clickable]);
937
+ // --- Effect 2: Global close events + auto-update ---
938
+ // Re-runs when the global close config changes, or when the active anchor changes
939
+ // (for scroll parent listeners and floating-ui autoUpdate).
940
+ React.useEffect(() => {
941
+ const handleScrollResize = () => {
942
+ handleShowRef.current(false);
943
+ };
944
+ const tooltipScrollParent = tooltipScrollParentRef.current;
945
+ const anchorScrollParent = anchorScrollParentRef.current;
946
+ if (actualGlobalCloseEvents.scroll) {
947
+ window.addEventListener('scroll', handleScrollResize);
948
+ anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.addEventListener('scroll', handleScrollResize);
949
+ tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.addEventListener('scroll', handleScrollResize);
950
+ }
951
+ let updateTooltipCleanup = null;
952
+ if (actualGlobalCloseEvents.resize) {
953
+ window.addEventListener('resize', handleScrollResize);
954
+ }
955
+ else if (activeAnchor && tooltipRef.current) {
956
+ updateTooltipCleanup = dom.autoUpdate(activeAnchor, tooltipRef.current, () => updateTooltipPositionRef.current(), {
957
+ ancestorResize: true,
958
+ elementResize: true,
959
+ layoutShift: true,
960
+ });
961
+ }
962
+ const handleEsc = (event) => {
963
+ if (event.key !== 'Escape') {
964
+ return;
965
+ }
966
+ handleShowRef.current(false);
967
+ };
968
+ if (actualGlobalCloseEvents.escape) {
969
+ window.addEventListener('keydown', handleEsc);
970
+ }
971
+ const handleClickOutsideAnchors = (event) => {
972
+ var _a, _b;
973
+ if (!showRef.current) {
974
+ return;
975
+ }
976
+ const target = event.target;
977
+ if (!(target === null || target === void 0 ? void 0 : target.isConnected)) {
978
+ return;
979
+ }
980
+ if ((_a = tooltipRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) {
981
+ return;
982
+ }
983
+ if ((_b = activeAnchorRef.current) === null || _b === void 0 ? void 0 : _b.contains(target)) {
984
+ return;
985
+ }
986
+ if (anchorElementsRef.current.some((anchor) => anchor === null || anchor === void 0 ? void 0 : anchor.contains(target))) {
987
+ return;
988
+ }
989
+ handleShowRef.current(false);
765
990
  clearTimeoutRef(tooltipShowDelayTimerRef);
766
- clearTimeoutRef(tooltipHideDelayTimerRef);
991
+ };
992
+ if (actualGlobalCloseEvents.clickOutsideAnchor) {
993
+ window.addEventListener('click', handleClickOutsideAnchors);
994
+ }
995
+ return () => {
767
996
  if (actualGlobalCloseEvents.scroll) {
768
997
  window.removeEventListener('scroll', handleScrollResize);
769
998
  anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.removeEventListener('scroll', handleScrollResize);
@@ -775,53 +1004,22 @@
775
1004
  if (updateTooltipCleanup) {
776
1005
  updateTooltipCleanup();
777
1006
  }
778
- if (actualGlobalCloseEvents.clickOutsideAnchor) {
779
- window.removeEventListener('click', handleClickOutsideAnchors);
780
- }
781
1007
  if (actualGlobalCloseEvents.escape) {
782
1008
  window.removeEventListener('keydown', handleEsc);
783
1009
  }
784
- if (addHoveringTooltipListeners) {
785
- tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseover', handleMouseOverTooltip);
786
- tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseout', handleMouseOutTooltip);
1010
+ if (actualGlobalCloseEvents.clickOutsideAnchor) {
1011
+ window.removeEventListener('click', handleClickOutsideAnchors);
787
1012
  }
788
- delegatedEvents.forEach(({ event, listener }) => {
789
- document.removeEventListener(event, listener);
790
- });
791
- internalDebouncedHandleShowTooltip.cancel();
792
- internalDebouncedHandleHideTooltip.cancel();
793
1013
  };
794
- }, [
795
- activeAnchor,
796
- anchorElements,
797
- clickable,
798
- closeEvents,
799
- delayHide,
800
- delayShow,
801
- disableTooltip,
802
- float,
803
- globalCloseEvents,
804
- handleHideTooltipDelayed,
805
- handleShow,
806
- handleShowTooltipDelayed,
807
- handleTooltipPosition,
808
- imperativeModeOnly,
809
- lastFloatPosition,
810
- openEvents,
811
- openOnClick,
812
- setActiveAnchor,
813
- show,
814
- tooltipHideDelayTimerRef,
815
- tooltipRef,
816
- tooltipShowDelayTimerRef,
817
- updateTooltipPosition,
818
- hoveringTooltip,
819
- ]);
1014
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1015
+ }, [actualGlobalCloseEvents, activeAnchor]);
820
1016
  };
821
1017
 
1018
+ // Shared across all tooltip instances — the CSS variable is on :root and never changes per-instance
1019
+ let globalTransitionShowDelay = null;
822
1020
  const Tooltip = ({
823
1021
  // props
824
- forwardRef, id, className, classNameArrow, variant = 'dark', portalRoot, anchorSelect, place = 'top', offset = 10, openOnClick = false, positionStrategy = 'absolute', middlewares, wrapper: WrapperElement, delayShow = 0, delayHide = 0, float = false, hidden = false, noArrow = false, clickable = false, openEvents, closeEvents, globalCloseEvents, imperativeModeOnly, style: externalStyles, position, afterShow, afterHide, disableTooltip,
1022
+ forwardRef, id, className, classNameArrow, variant = 'dark', portalRoot, anchorSelect, place = 'top', offset = 10, openOnClick = false, positionStrategy = 'absolute', middlewares, wrapper: WrapperElement, delayShow = 0, delayHide = 0, autoClose, float = false, hidden = false, noArrow = false, clickable = false, openEvents, closeEvents, globalCloseEvents, imperativeModeOnly, style: externalStyles, position, afterShow, afterHide, disableTooltip,
825
1023
  // props handled by controller
826
1024
  content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousActiveAnchor, activeAnchor, setActiveAnchor, border, opacity, arrowColor, arrowSize = 8, role = 'tooltip', }) => {
827
1025
  var _a;
@@ -829,6 +1027,7 @@
829
1027
  const tooltipArrowRef = React.useRef(null);
830
1028
  const tooltipShowDelayTimerRef = React.useRef(null);
831
1029
  const tooltipHideDelayTimerRef = React.useRef(null);
1030
+ const tooltipAutoCloseTimerRef = React.useRef(null);
832
1031
  const missedTransitionTimerRef = React.useRef(null);
833
1032
  const [computedPosition, setComputedPosition] = React.useState({
834
1033
  tooltipStyles: {},
@@ -842,6 +1041,18 @@
842
1041
  const lastFloatPosition = React.useRef(null);
843
1042
  const hoveringTooltip = React.useRef(false);
844
1043
  const mounted = React.useRef(false);
1044
+ const virtualElementRef = React.useRef({
1045
+ getBoundingClientRect: () => ({
1046
+ x: 0,
1047
+ y: 0,
1048
+ width: 0,
1049
+ height: 0,
1050
+ top: 0,
1051
+ left: 0,
1052
+ right: 0,
1053
+ bottom: 0,
1054
+ }),
1055
+ });
845
1056
  /**
846
1057
  * useLayoutEffect runs before useEffect,
847
1058
  * but should be used carefully because of caveats
@@ -902,7 +1113,6 @@
902
1113
  else {
903
1114
  removeAriaDescribedBy(activeAnchor);
904
1115
  }
905
- // eslint-disable-next-line consistent-return
906
1116
  return () => {
907
1117
  // cleanup aria-describedby when the tooltip is closed
908
1118
  removeAriaDescribedBy(activeAnchor);
@@ -940,8 +1150,11 @@
940
1150
  /**
941
1151
  * see `onTransitionEnd` on tooltip wrapper
942
1152
  */
943
- const style = getComputedStyle(document.body);
944
- const transitionShowDelay = cssTimeToMs(style.getPropertyValue('--rt-transition-show-delay'));
1153
+ if (globalTransitionShowDelay === null) {
1154
+ const style = getComputedStyle(document.body);
1155
+ globalTransitionShowDelay = cssTimeToMs(style.getPropertyValue('--rt-transition-show-delay'));
1156
+ }
1157
+ const transitionShowDelay = globalTransitionShowDelay;
945
1158
  missedTransitionTimerRef.current = setTimeout(() => {
946
1159
  /**
947
1160
  * if the tooltip switches from `show === true` to `show === false` too fast
@@ -954,13 +1167,44 @@
954
1167
  }, transitionShowDelay + 25);
955
1168
  }
956
1169
  }, [afterHide, afterShow, show]);
1170
+ React.useEffect(() => {
1171
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1172
+ if (!show || !autoClose || autoClose <= 0) {
1173
+ return () => {
1174
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1175
+ };
1176
+ }
1177
+ tooltipAutoCloseTimerRef.current = setTimeout(() => {
1178
+ handleShow(false);
1179
+ }, autoClose);
1180
+ return () => {
1181
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1182
+ };
1183
+ }, [activeAnchor, autoClose, handleShow, show]);
957
1184
  const handleComputedPosition = React.useCallback((newComputedPosition) => {
958
1185
  if (!mounted.current) {
959
1186
  return;
960
1187
  }
961
- setComputedPosition((oldComputedPosition) => deepEqual(oldComputedPosition, newComputedPosition)
962
- ? oldComputedPosition
963
- : newComputedPosition);
1188
+ setComputedPosition((oldComputedPosition) => {
1189
+ if (oldComputedPosition.place === newComputedPosition.place &&
1190
+ oldComputedPosition.tooltipStyles.left === newComputedPosition.tooltipStyles.left &&
1191
+ oldComputedPosition.tooltipStyles.top === newComputedPosition.tooltipStyles.top &&
1192
+ oldComputedPosition.tooltipStyles.border === newComputedPosition.tooltipStyles.border &&
1193
+ oldComputedPosition.tooltipArrowStyles.left ===
1194
+ newComputedPosition.tooltipArrowStyles.left &&
1195
+ oldComputedPosition.tooltipArrowStyles.top === newComputedPosition.tooltipArrowStyles.top &&
1196
+ oldComputedPosition.tooltipArrowStyles.right ===
1197
+ newComputedPosition.tooltipArrowStyles.right &&
1198
+ oldComputedPosition.tooltipArrowStyles.bottom ===
1199
+ newComputedPosition.tooltipArrowStyles.bottom &&
1200
+ oldComputedPosition.tooltipArrowStyles.borderBottom ===
1201
+ newComputedPosition.tooltipArrowStyles.borderBottom &&
1202
+ oldComputedPosition.tooltipArrowStyles.borderRight ===
1203
+ newComputedPosition.tooltipArrowStyles.borderRight) {
1204
+ return oldComputedPosition;
1205
+ }
1206
+ return newComputedPosition;
1207
+ });
964
1208
  }, []);
965
1209
  const handleShowTooltipDelayed = React.useCallback((delay = delayShow) => {
966
1210
  if (tooltipShowDelayTimerRef.current) {
@@ -988,24 +1232,20 @@
988
1232
  }, [delayHide, handleShow]);
989
1233
  const handleTooltipPosition = React.useCallback(({ x, y }) => {
990
1234
  var _a;
991
- const virtualElement = {
992
- getBoundingClientRect() {
993
- return {
994
- x,
995
- y,
996
- width: 0,
997
- height: 0,
998
- top: y,
999
- left: x,
1000
- right: x,
1001
- bottom: y,
1002
- };
1003
- },
1004
- };
1235
+ virtualElementRef.current.getBoundingClientRect = () => ({
1236
+ x,
1237
+ y,
1238
+ width: 0,
1239
+ height: 0,
1240
+ top: y,
1241
+ left: x,
1242
+ right: x,
1243
+ bottom: y,
1244
+ });
1005
1245
  computeTooltipPosition({
1006
1246
  place: (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place) !== null && _a !== void 0 ? _a : place,
1007
1247
  offset,
1008
- elementReference: virtualElement,
1248
+ elementReference: virtualElementRef.current,
1009
1249
  tooltipReference: tooltipRef.current,
1010
1250
  tooltipArrowReference: tooltipArrowRef.current,
1011
1251
  strategy: positionStrategy,
@@ -1088,18 +1328,26 @@
1088
1328
  setActiveAnchor(null);
1089
1329
  clearTimeoutRef(tooltipShowDelayTimerRef);
1090
1330
  clearTimeoutRef(tooltipHideDelayTimerRef);
1331
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1091
1332
  }, [handleShow, setActiveAnchor]);
1092
- const anchorElements = useTooltipAnchors({
1333
+ const shouldTrackAnchors = rendered ||
1334
+ defaultIsOpen ||
1335
+ Boolean(isOpen) ||
1336
+ Boolean(activeAnchor) ||
1337
+ Boolean(imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect);
1338
+ const { anchorElements, selector: anchorSelector } = useTooltipAnchors({
1093
1339
  id,
1094
1340
  anchorSelect,
1095
1341
  imperativeAnchorSelect: imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect,
1096
1342
  activeAnchor,
1097
1343
  disableTooltip,
1098
1344
  onActiveAnchorRemoved: handleActiveAnchorRemoved,
1345
+ trackAnchors: shouldTrackAnchors,
1099
1346
  });
1100
1347
  useTooltipEvents({
1101
1348
  activeAnchor,
1102
1349
  anchorElements,
1350
+ anchorSelector,
1103
1351
  clickable,
1104
1352
  closeEvents,
1105
1353
  delayHide,
@@ -1123,11 +1371,16 @@
1123
1371
  tooltipShowDelayTimerRef,
1124
1372
  updateTooltipPosition,
1125
1373
  });
1374
+ const updateTooltipPositionRef = React.useRef(updateTooltipPosition);
1375
+ updateTooltipPositionRef.current = updateTooltipPosition;
1126
1376
  React.useEffect(() => {
1377
+ if (!rendered) {
1378
+ return;
1379
+ }
1127
1380
  updateTooltipPosition();
1128
- }, [updateTooltipPosition]);
1381
+ }, [rendered, updateTooltipPosition]);
1129
1382
  React.useEffect(() => {
1130
- if (!(contentWrapperRef === null || contentWrapperRef === void 0 ? void 0 : contentWrapperRef.current)) {
1383
+ if (!rendered || !(contentWrapperRef === null || contentWrapperRef === void 0 ? void 0 : contentWrapperRef.current)) {
1131
1384
  return () => null;
1132
1385
  }
1133
1386
  let timeoutId = null;
@@ -1138,7 +1391,7 @@
1138
1391
  }
1139
1392
  timeoutId = setTimeout(() => {
1140
1393
  if (mounted.current) {
1141
- updateTooltipPosition();
1394
+ updateTooltipPositionRef.current();
1142
1395
  }
1143
1396
  timeoutId = null;
1144
1397
  }, 0);
@@ -1150,9 +1403,13 @@
1150
1403
  clearTimeout(timeoutId);
1151
1404
  }
1152
1405
  };
1153
- }, [content, contentWrapperRef, updateTooltipPosition]);
1406
+ }, [content, contentWrapperRef, rendered]);
1154
1407
  React.useEffect(() => {
1155
1408
  var _a;
1409
+ const shouldResolveInitialActiveAnchor = rendered || defaultIsOpen || Boolean(isOpen);
1410
+ if (!shouldResolveInitialActiveAnchor) {
1411
+ return;
1412
+ }
1156
1413
  const activeAnchorMatchesImperativeSelector = (() => {
1157
1414
  if (!activeAnchor || !(imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect)) {
1158
1415
  return false;
@@ -1175,7 +1432,15 @@
1175
1432
  }
1176
1433
  setActiveAnchor((_a = anchorElements[0]) !== null && _a !== void 0 ? _a : null);
1177
1434
  }
1178
- }, [anchorElements, activeAnchor, imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect, setActiveAnchor]);
1435
+ }, [
1436
+ activeAnchor,
1437
+ anchorElements,
1438
+ defaultIsOpen,
1439
+ imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect,
1440
+ isOpen,
1441
+ rendered,
1442
+ setActiveAnchor,
1443
+ ]);
1179
1444
  React.useEffect(() => {
1180
1445
  if (defaultIsOpen) {
1181
1446
  handleShow(true);
@@ -1183,6 +1448,7 @@
1183
1448
  return () => {
1184
1449
  clearTimeoutRef(tooltipShowDelayTimerRef);
1185
1450
  clearTimeoutRef(tooltipHideDelayTimerRef);
1451
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1186
1452
  clearTimeoutRef(missedTransitionTimerRef);
1187
1453
  };
1188
1454
  }, [defaultIsOpen, handleShow]);
@@ -1198,7 +1464,20 @@
1198
1464
  }, [delayShow, handleShowTooltipDelayed]);
1199
1465
  const actualContent = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.content) !== null && _a !== void 0 ? _a : content;
1200
1466
  const hasContent = actualContent !== null && actualContent !== undefined;
1201
- const canShow = show && Object.keys(computedPosition.tooltipStyles).length > 0;
1467
+ const canShow = show && computedPosition.tooltipStyles.left !== undefined;
1468
+ const tooltipStyle = React.useMemo(() => ({
1469
+ ...externalStyles,
1470
+ ...computedPosition.tooltipStyles,
1471
+ opacity: opacity !== undefined && canShow ? opacity : undefined,
1472
+ }), [externalStyles, computedPosition.tooltipStyles, opacity, canShow]);
1473
+ const arrowBackground = React.useMemo(() => arrowColor
1474
+ ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`
1475
+ : undefined, [arrowColor]);
1476
+ const arrowStyle = React.useMemo(() => ({
1477
+ ...computedPosition.tooltipArrowStyles,
1478
+ background: arrowBackground,
1479
+ '--rt-arrow-size': `${arrowSize}px`,
1480
+ }), [computedPosition.tooltipArrowStyles, arrowBackground, arrowSize]);
1202
1481
  React.useImperativeHandle(forwardRef, () => ({
1203
1482
  open: (options) => {
1204
1483
  let imperativeAnchor = null;
@@ -1241,6 +1520,7 @@
1241
1520
  // Final cleanup to ensure no memory leaks
1242
1521
  clearTimeoutRef(tooltipShowDelayTimerRef);
1243
1522
  clearTimeoutRef(tooltipHideDelayTimerRef);
1523
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1244
1524
  clearTimeoutRef(missedTransitionTimerRef);
1245
1525
  };
1246
1526
  }, []);
@@ -1252,19 +1532,9 @@
1252
1532
  setRendered(false);
1253
1533
  setImperativeOptions(null);
1254
1534
  afterHide === null || afterHide === void 0 ? void 0 : afterHide();
1255
- }, style: {
1256
- ...externalStyles,
1257
- ...computedPosition.tooltipStyles,
1258
- opacity: opacity !== undefined && canShow ? opacity : undefined,
1259
- }, ref: tooltipRef },
1535
+ }, style: tooltipStyle, ref: tooltipRef },
1260
1536
  React.createElement(WrapperElement, { className: clsx('react-tooltip-content-wrapper', coreStyles['content'], styles['content']) }, actualContent),
1261
- React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: {
1262
- ...computedPosition.tooltipArrowStyles,
1263
- background: arrowColor
1264
- ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`
1265
- : undefined,
1266
- '--rt-arrow-size': `${arrowSize}px`,
1267
- }, ref: tooltipArrowRef }))) : null;
1537
+ React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: arrowStyle, ref: tooltipArrowRef }))) : null;
1268
1538
  if (!tooltipNode) {
1269
1539
  return null;
1270
1540
  }
@@ -1275,12 +1545,82 @@
1275
1545
  };
1276
1546
  var Tooltip$1 = React.memo(Tooltip);
1277
1547
 
1278
- const TooltipController = React.forwardRef(({ id, anchorSelect, content, render, className, classNameArrow, variant = 'dark', portalRoot, place = 'top', offset = 10, wrapper = 'div', children = null, openOnClick = false, positionStrategy = 'absolute', middlewares, delayShow = 0, delayHide = 0, float = false, hidden = false, noArrow = false, clickable = false, openEvents, closeEvents, globalCloseEvents, imperativeModeOnly = false, style, position, isOpen, defaultIsOpen = false, disableStyleInjection = false, border, opacity, arrowColor, arrowSize, setIsOpen, afterShow, afterHide, disableTooltip, role = 'tooltip', }, ref) => {
1548
+ /**
1549
+ * Shared MutationObserver for data-tooltip-* attribute changes.
1550
+ * Instead of N observers (one per tooltip), a single observer watches
1551
+ * all active anchors and dispatches changes to registered callbacks.
1552
+ */
1553
+ const observedElements = new Map();
1554
+ let sharedObserver = null;
1555
+ const observerConfig = {
1556
+ attributes: true,
1557
+ childList: false,
1558
+ subtree: false,
1559
+ };
1560
+ function getObserver() {
1561
+ if (!sharedObserver) {
1562
+ sharedObserver = new MutationObserver((mutationList) => {
1563
+ var _a;
1564
+ for (const mutation of mutationList) {
1565
+ if (mutation.type !== 'attributes' ||
1566
+ !((_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.startsWith('data-tooltip-'))) {
1567
+ continue;
1568
+ }
1569
+ const target = mutation.target;
1570
+ const callbacks = observedElements.get(target);
1571
+ if (callbacks) {
1572
+ callbacks.forEach((cb) => cb(target));
1573
+ }
1574
+ }
1575
+ });
1576
+ }
1577
+ return sharedObserver;
1578
+ }
1579
+ function observeAnchorAttributes(element, callback) {
1580
+ const observer = getObserver();
1581
+ let callbacks = observedElements.get(element);
1582
+ if (!callbacks) {
1583
+ callbacks = new Set();
1584
+ observedElements.set(element, callbacks);
1585
+ observer.observe(element, observerConfig);
1586
+ }
1587
+ callbacks.add(callback);
1588
+ return () => {
1589
+ const cbs = observedElements.get(element);
1590
+ if (cbs) {
1591
+ cbs.delete(callback);
1592
+ if (cbs.size === 0) {
1593
+ observedElements.delete(element);
1594
+ // MutationObserver doesn't have unobserve — if no elements left, disconnect & reset
1595
+ if (observedElements.size === 0) {
1596
+ observer.disconnect();
1597
+ }
1598
+ else {
1599
+ // Re-observe remaining elements (MutationObserver has no per-target unobserve)
1600
+ observer.disconnect();
1601
+ observedElements.forEach((_cbs, el) => {
1602
+ observer.observe(el, observerConfig);
1603
+ });
1604
+ }
1605
+ }
1606
+ }
1607
+ };
1608
+ }
1609
+
1610
+ const TooltipController = React.forwardRef(({ id, anchorSelect, content, render, className, classNameArrow, variant = 'dark', portalRoot, place = 'top', offset = 10, wrapper = 'div', children = null, openOnClick = false, positionStrategy = 'absolute', middlewares, delayShow = 0, delayHide = 0, autoClose, float = false, hidden = false, noArrow = false, clickable = false, openEvents, closeEvents, globalCloseEvents, imperativeModeOnly = false, style, position, isOpen, defaultIsOpen = false, disableStyleInjection = false, border, opacity, arrowColor, arrowSize, setIsOpen, afterShow, afterHide, disableTooltip, role = 'tooltip', }, ref) => {
1279
1611
  var _a, _b, _c, _d, _e, _f, _g, _h;
1280
1612
  const [activeAnchor, setActiveAnchor] = React.useState(null);
1281
1613
  const [anchorDataAttributes, setAnchorDataAttributes] = React.useState({});
1282
1614
  const previousActiveAnchorRef = React.useRef(null);
1283
1615
  const styleInjectionRef = React.useRef(disableStyleInjection);
1616
+ const handleSetActiveAnchor = React.useCallback((anchor) => {
1617
+ setActiveAnchor((prev) => {
1618
+ if (!(anchor === null || anchor === void 0 ? void 0 : anchor.isSameNode(prev))) {
1619
+ previousActiveAnchorRef.current = prev;
1620
+ }
1621
+ return anchor;
1622
+ });
1623
+ }, []);
1284
1624
  /* c8 ignore start */
1285
1625
  const getDataAttributesFromAnchorElement = (elementReference) => {
1286
1626
  const dataAttributes = elementReference === null || elementReference === void 0 ? void 0 : elementReference.getAttributeNames().reduce((acc, name) => {
@@ -1312,37 +1652,24 @@
1312
1652
  // eslint-disable-next-line react-hooks/exhaustive-deps
1313
1653
  }, []);
1314
1654
  React.useEffect(() => {
1315
- const observerCallback = (mutationList) => {
1316
- mutationList.forEach((mutation) => {
1317
- var _a;
1318
- if (!activeAnchor ||
1319
- mutation.type !== 'attributes' ||
1320
- !((_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.startsWith('data-tooltip-'))) {
1321
- return;
1322
- }
1323
- // make sure to get all set attributes, since all unset attributes are reset
1324
- const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor);
1325
- setAnchorDataAttributes(dataAttributes);
1326
- });
1327
- };
1328
- // Create an observer instance linked to the callback function
1329
- const observer = new MutationObserver(observerCallback);
1330
- // do not check for subtree and childrens, we only want to know attribute changes
1331
- // to stay watching `data-attributes-*` from anchor element
1332
- const observerConfig = { attributes: true, childList: false, subtree: false };
1333
- if (activeAnchor) {
1334
- const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor);
1335
- setAnchorDataAttributes(dataAttributes);
1336
- // Start observing the target node for configured mutations
1337
- observer.observe(activeAnchor, observerConfig);
1338
- }
1339
- else {
1655
+ if (!activeAnchor) {
1340
1656
  setAnchorDataAttributes({});
1657
+ return () => { };
1341
1658
  }
1342
- return () => {
1343
- // Remove the observer when the tooltip is destroyed
1344
- observer.disconnect();
1659
+ const updateAttributes = (element) => {
1660
+ const attrs = getDataAttributesFromAnchorElement(element);
1661
+ setAnchorDataAttributes((prev) => {
1662
+ const keys = Object.keys(attrs);
1663
+ const prevKeys = Object.keys(prev);
1664
+ if (keys.length === prevKeys.length && keys.every((key) => attrs[key] === prev[key])) {
1665
+ return prev;
1666
+ }
1667
+ return attrs;
1668
+ });
1345
1669
  };
1670
+ updateAttributes(activeAnchor);
1671
+ const unsubscribe = observeAnchorAttributes(activeAnchor, updateAttributes);
1672
+ return unsubscribe;
1346
1673
  }, [activeAnchor, anchorSelect]);
1347
1674
  React.useEffect(() => {
1348
1675
  /* c8 ignore start */
@@ -1366,6 +1693,9 @@
1366
1693
  const tooltipDelayHide = anchorDataAttributes['delay-hide'] == null
1367
1694
  ? delayHide
1368
1695
  : Number(anchorDataAttributes['delay-hide']);
1696
+ const tooltipAutoClose = anchorDataAttributes['auto-close'] == null
1697
+ ? autoClose
1698
+ : Number(anchorDataAttributes['auto-close']);
1369
1699
  const tooltipFloat = anchorDataAttributes.float == null ? float : anchorDataAttributes.float === 'true';
1370
1700
  const tooltipHidden = anchorDataAttributes.hidden == null ? hidden : anchorDataAttributes.hidden === 'true';
1371
1701
  const tooltipClassName = (_f = anchorDataAttributes['class-name']) !== null && _f !== void 0 ? _f : null;
@@ -1397,6 +1727,7 @@
1397
1727
  middlewares,
1398
1728
  delayShow: tooltipDelayShow,
1399
1729
  delayHide: tooltipDelayHide,
1730
+ autoClose: tooltipAutoClose,
1400
1731
  float: tooltipFloat,
1401
1732
  hidden: tooltipHidden,
1402
1733
  noArrow,
@@ -1419,19 +1750,12 @@
1419
1750
  disableTooltip,
1420
1751
  activeAnchor,
1421
1752
  previousActiveAnchor: previousActiveAnchorRef.current,
1422
- setActiveAnchor: (anchor) => {
1423
- setActiveAnchor((prev) => {
1424
- if (!(anchor === null || anchor === void 0 ? void 0 : anchor.isSameNode(prev))) {
1425
- previousActiveAnchorRef.current = prev;
1426
- }
1427
- return anchor;
1428
- });
1429
- },
1753
+ setActiveAnchor: handleSetActiveAnchor,
1430
1754
  role,
1431
1755
  };
1432
1756
  return React.createElement(Tooltip$1, { ...props });
1433
1757
  });
1434
- var TooltipController$1 = React.memo(TooltipController);
1758
+ var TooltipController_default = React.memo(TooltipController);
1435
1759
 
1436
1760
  // those content will be replaced in build time with the `react-tooltip.css` builded content
1437
1761
  const TooltipCoreStyles = `:root {
@@ -1453,7 +1777,6 @@
1453
1777
  left: 0;
1454
1778
  pointer-events: none;
1455
1779
  opacity: 0;
1456
- will-change: opacity;
1457
1780
  }
1458
1781
 
1459
1782
  .core-styles-module_fixed__pcSol {
@@ -1484,11 +1807,13 @@
1484
1807
  .core-styles-module_show__Nt9eE {
1485
1808
  opacity: var(--rt-opacity);
1486
1809
  transition: opacity var(--rt-transition-show-delay) ease-out;
1810
+ will-change: opacity;
1487
1811
  }
1488
1812
 
1489
1813
  .core-styles-module_closing__sGnxF {
1490
1814
  opacity: 0;
1491
1815
  transition: opacity var(--rt-transition-closing-delay) ease-in;
1816
+ will-change: opacity;
1492
1817
  }
1493
1818
 
1494
1819
  `;
@@ -1569,7 +1894,7 @@
1569
1894
  }));
1570
1895
  }
1571
1896
 
1572
- exports.Tooltip = TooltipController$1;
1897
+ exports.Tooltip = TooltipController_default;
1573
1898
 
1574
1899
  }));
1575
1900
  //# sourceMappingURL=react-tooltip.umd.js.map