react-tooltip 6.0.0-beta.1179.rc.9 → 6.0.1

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, rendered, 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,113 @@
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
+ // `rendered` needs to be a dependency because `tooltipRef` becomes stale when the
936
+ // tooltip is removed from / added to the DOM.
937
+ // eslint-disable-next-line react-hooks/exhaustive-deps
938
+ }, [actualOpenEvents, actualCloseEvents, float, clickable, rendered]);
939
+ // --- Effect 2: Global close events + auto-update ---
940
+ // Re-runs when the global close config changes, or when the active anchor changes
941
+ // (for scroll parent listeners and floating-ui autoUpdate).
942
+ React.useEffect(() => {
943
+ const handleScrollResize = () => {
944
+ handleShowRef.current(false);
945
+ };
946
+ const tooltipScrollParent = tooltipScrollParentRef.current;
947
+ const anchorScrollParent = anchorScrollParentRef.current;
948
+ if (actualGlobalCloseEvents.scroll) {
949
+ window.addEventListener('scroll', handleScrollResize);
950
+ anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.addEventListener('scroll', handleScrollResize);
951
+ tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.addEventListener('scroll', handleScrollResize);
952
+ }
953
+ let updateTooltipCleanup = null;
954
+ if (actualGlobalCloseEvents.resize) {
955
+ window.addEventListener('resize', handleScrollResize);
956
+ }
957
+ else if (activeAnchor && tooltipRef.current) {
958
+ updateTooltipCleanup = dom.autoUpdate(activeAnchor, tooltipRef.current, () => updateTooltipPositionRef.current(), {
959
+ ancestorResize: true,
960
+ elementResize: true,
961
+ layoutShift: true,
962
+ });
963
+ }
964
+ const handleEsc = (event) => {
965
+ if (event.key !== 'Escape') {
966
+ return;
967
+ }
968
+ handleShowRef.current(false);
969
+ };
970
+ if (actualGlobalCloseEvents.escape) {
971
+ window.addEventListener('keydown', handleEsc);
972
+ }
973
+ const handleClickOutsideAnchors = (event) => {
974
+ var _a, _b;
975
+ if (!showRef.current) {
976
+ return;
977
+ }
978
+ const target = event.target;
979
+ if (!(target === null || target === void 0 ? void 0 : target.isConnected)) {
980
+ return;
981
+ }
982
+ if ((_a = tooltipRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) {
983
+ return;
984
+ }
985
+ if ((_b = activeAnchorRef.current) === null || _b === void 0 ? void 0 : _b.contains(target)) {
986
+ return;
987
+ }
988
+ if (anchorElementsRef.current.some((anchor) => anchor === null || anchor === void 0 ? void 0 : anchor.contains(target))) {
989
+ return;
990
+ }
991
+ handleShowRef.current(false);
765
992
  clearTimeoutRef(tooltipShowDelayTimerRef);
766
- clearTimeoutRef(tooltipHideDelayTimerRef);
993
+ };
994
+ if (actualGlobalCloseEvents.clickOutsideAnchor) {
995
+ window.addEventListener('click', handleClickOutsideAnchors);
996
+ }
997
+ return () => {
767
998
  if (actualGlobalCloseEvents.scroll) {
768
999
  window.removeEventListener('scroll', handleScrollResize);
769
1000
  anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.removeEventListener('scroll', handleScrollResize);
@@ -775,50 +1006,19 @@
775
1006
  if (updateTooltipCleanup) {
776
1007
  updateTooltipCleanup();
777
1008
  }
778
- if (actualGlobalCloseEvents.clickOutsideAnchor) {
779
- window.removeEventListener('click', handleClickOutsideAnchors);
780
- }
781
1009
  if (actualGlobalCloseEvents.escape) {
782
1010
  window.removeEventListener('keydown', handleEsc);
783
1011
  }
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);
1012
+ if (actualGlobalCloseEvents.clickOutsideAnchor) {
1013
+ window.removeEventListener('click', handleClickOutsideAnchors);
787
1014
  }
788
- delegatedEvents.forEach(({ event, listener }) => {
789
- document.removeEventListener(event, listener);
790
- });
791
- internalDebouncedHandleShowTooltip.cancel();
792
- internalDebouncedHandleHideTooltip.cancel();
793
1015
  };
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
- ]);
1016
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1017
+ }, [actualGlobalCloseEvents, activeAnchor]);
820
1018
  };
821
1019
 
1020
+ // Shared across all tooltip instances — the CSS variable is on :root and never changes per-instance
1021
+ let globalTransitionShowDelay = null;
822
1022
  const Tooltip = ({
823
1023
  // props
824
1024
  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,
@@ -843,6 +1043,18 @@
843
1043
  const lastFloatPosition = React.useRef(null);
844
1044
  const hoveringTooltip = React.useRef(false);
845
1045
  const mounted = React.useRef(false);
1046
+ const virtualElementRef = React.useRef({
1047
+ getBoundingClientRect: () => ({
1048
+ x: 0,
1049
+ y: 0,
1050
+ width: 0,
1051
+ height: 0,
1052
+ top: 0,
1053
+ left: 0,
1054
+ right: 0,
1055
+ bottom: 0,
1056
+ }),
1057
+ });
846
1058
  /**
847
1059
  * useLayoutEffect runs before useEffect,
848
1060
  * but should be used carefully because of caveats
@@ -903,7 +1115,6 @@
903
1115
  else {
904
1116
  removeAriaDescribedBy(activeAnchor);
905
1117
  }
906
- // eslint-disable-next-line consistent-return
907
1118
  return () => {
908
1119
  // cleanup aria-describedby when the tooltip is closed
909
1120
  removeAriaDescribedBy(activeAnchor);
@@ -941,8 +1152,11 @@
941
1152
  /**
942
1153
  * see `onTransitionEnd` on tooltip wrapper
943
1154
  */
944
- const style = getComputedStyle(document.body);
945
- const transitionShowDelay = cssTimeToMs(style.getPropertyValue('--rt-transition-show-delay'));
1155
+ if (globalTransitionShowDelay === null) {
1156
+ const style = getComputedStyle(document.body);
1157
+ globalTransitionShowDelay = cssTimeToMs(style.getPropertyValue('--rt-transition-show-delay'));
1158
+ }
1159
+ const transitionShowDelay = globalTransitionShowDelay;
946
1160
  missedTransitionTimerRef.current = setTimeout(() => {
947
1161
  /**
948
1162
  * if the tooltip switches from `show === true` to `show === false` too fast
@@ -973,9 +1187,26 @@
973
1187
  if (!mounted.current) {
974
1188
  return;
975
1189
  }
976
- setComputedPosition((oldComputedPosition) => deepEqual(oldComputedPosition, newComputedPosition)
977
- ? oldComputedPosition
978
- : newComputedPosition);
1190
+ setComputedPosition((oldComputedPosition) => {
1191
+ if (oldComputedPosition.place === newComputedPosition.place &&
1192
+ oldComputedPosition.tooltipStyles.left === newComputedPosition.tooltipStyles.left &&
1193
+ oldComputedPosition.tooltipStyles.top === newComputedPosition.tooltipStyles.top &&
1194
+ oldComputedPosition.tooltipStyles.border === newComputedPosition.tooltipStyles.border &&
1195
+ oldComputedPosition.tooltipArrowStyles.left ===
1196
+ newComputedPosition.tooltipArrowStyles.left &&
1197
+ oldComputedPosition.tooltipArrowStyles.top === newComputedPosition.tooltipArrowStyles.top &&
1198
+ oldComputedPosition.tooltipArrowStyles.right ===
1199
+ newComputedPosition.tooltipArrowStyles.right &&
1200
+ oldComputedPosition.tooltipArrowStyles.bottom ===
1201
+ newComputedPosition.tooltipArrowStyles.bottom &&
1202
+ oldComputedPosition.tooltipArrowStyles.borderBottom ===
1203
+ newComputedPosition.tooltipArrowStyles.borderBottom &&
1204
+ oldComputedPosition.tooltipArrowStyles.borderRight ===
1205
+ newComputedPosition.tooltipArrowStyles.borderRight) {
1206
+ return oldComputedPosition;
1207
+ }
1208
+ return newComputedPosition;
1209
+ });
979
1210
  }, []);
980
1211
  const handleShowTooltipDelayed = React.useCallback((delay = delayShow) => {
981
1212
  if (tooltipShowDelayTimerRef.current) {
@@ -1003,24 +1234,20 @@
1003
1234
  }, [delayHide, handleShow]);
1004
1235
  const handleTooltipPosition = React.useCallback(({ x, y }) => {
1005
1236
  var _a;
1006
- const virtualElement = {
1007
- getBoundingClientRect() {
1008
- return {
1009
- x,
1010
- y,
1011
- width: 0,
1012
- height: 0,
1013
- top: y,
1014
- left: x,
1015
- right: x,
1016
- bottom: y,
1017
- };
1018
- },
1019
- };
1237
+ virtualElementRef.current.getBoundingClientRect = () => ({
1238
+ x,
1239
+ y,
1240
+ width: 0,
1241
+ height: 0,
1242
+ top: y,
1243
+ left: x,
1244
+ right: x,
1245
+ bottom: y,
1246
+ });
1020
1247
  computeTooltipPosition({
1021
1248
  place: (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place) !== null && _a !== void 0 ? _a : place,
1022
1249
  offset,
1023
- elementReference: virtualElement,
1250
+ elementReference: virtualElementRef.current,
1024
1251
  tooltipReference: tooltipRef.current,
1025
1252
  tooltipArrowReference: tooltipArrowRef.current,
1026
1253
  strategy: positionStrategy,
@@ -1105,17 +1332,24 @@
1105
1332
  clearTimeoutRef(tooltipHideDelayTimerRef);
1106
1333
  clearTimeoutRef(tooltipAutoCloseTimerRef);
1107
1334
  }, [handleShow, setActiveAnchor]);
1108
- const anchorElements = useTooltipAnchors({
1335
+ const shouldTrackAnchors = rendered ||
1336
+ defaultIsOpen ||
1337
+ Boolean(isOpen) ||
1338
+ Boolean(activeAnchor) ||
1339
+ Boolean(imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect);
1340
+ const { anchorElements, selector: anchorSelector } = useTooltipAnchors({
1109
1341
  id,
1110
1342
  anchorSelect,
1111
1343
  imperativeAnchorSelect: imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect,
1112
1344
  activeAnchor,
1113
1345
  disableTooltip,
1114
1346
  onActiveAnchorRemoved: handleActiveAnchorRemoved,
1347
+ trackAnchors: shouldTrackAnchors,
1115
1348
  });
1116
1349
  useTooltipEvents({
1117
1350
  activeAnchor,
1118
1351
  anchorElements,
1352
+ anchorSelector,
1119
1353
  clickable,
1120
1354
  closeEvents,
1121
1355
  delayHide,
@@ -1132,6 +1366,7 @@
1132
1366
  lastFloatPosition,
1133
1367
  openEvents,
1134
1368
  openOnClick,
1369
+ rendered,
1135
1370
  setActiveAnchor,
1136
1371
  show,
1137
1372
  tooltipHideDelayTimerRef,
@@ -1139,11 +1374,16 @@
1139
1374
  tooltipShowDelayTimerRef,
1140
1375
  updateTooltipPosition,
1141
1376
  });
1377
+ const updateTooltipPositionRef = React.useRef(updateTooltipPosition);
1378
+ updateTooltipPositionRef.current = updateTooltipPosition;
1142
1379
  React.useEffect(() => {
1380
+ if (!rendered) {
1381
+ return;
1382
+ }
1143
1383
  updateTooltipPosition();
1144
- }, [updateTooltipPosition]);
1384
+ }, [rendered, updateTooltipPosition]);
1145
1385
  React.useEffect(() => {
1146
- if (!(contentWrapperRef === null || contentWrapperRef === void 0 ? void 0 : contentWrapperRef.current)) {
1386
+ if (!rendered || !(contentWrapperRef === null || contentWrapperRef === void 0 ? void 0 : contentWrapperRef.current)) {
1147
1387
  return () => null;
1148
1388
  }
1149
1389
  let timeoutId = null;
@@ -1154,7 +1394,7 @@
1154
1394
  }
1155
1395
  timeoutId = setTimeout(() => {
1156
1396
  if (mounted.current) {
1157
- updateTooltipPosition();
1397
+ updateTooltipPositionRef.current();
1158
1398
  }
1159
1399
  timeoutId = null;
1160
1400
  }, 0);
@@ -1166,9 +1406,13 @@
1166
1406
  clearTimeout(timeoutId);
1167
1407
  }
1168
1408
  };
1169
- }, [content, contentWrapperRef, updateTooltipPosition]);
1409
+ }, [content, contentWrapperRef, rendered]);
1170
1410
  React.useEffect(() => {
1171
1411
  var _a;
1412
+ const shouldResolveInitialActiveAnchor = rendered || defaultIsOpen || Boolean(isOpen);
1413
+ if (!shouldResolveInitialActiveAnchor) {
1414
+ return;
1415
+ }
1172
1416
  const activeAnchorMatchesImperativeSelector = (() => {
1173
1417
  if (!activeAnchor || !(imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect)) {
1174
1418
  return false;
@@ -1191,7 +1435,15 @@
1191
1435
  }
1192
1436
  setActiveAnchor((_a = anchorElements[0]) !== null && _a !== void 0 ? _a : null);
1193
1437
  }
1194
- }, [anchorElements, activeAnchor, imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect, setActiveAnchor]);
1438
+ }, [
1439
+ activeAnchor,
1440
+ anchorElements,
1441
+ defaultIsOpen,
1442
+ imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect,
1443
+ isOpen,
1444
+ rendered,
1445
+ setActiveAnchor,
1446
+ ]);
1195
1447
  React.useEffect(() => {
1196
1448
  if (defaultIsOpen) {
1197
1449
  handleShow(true);
@@ -1215,7 +1467,20 @@
1215
1467
  }, [delayShow, handleShowTooltipDelayed]);
1216
1468
  const actualContent = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.content) !== null && _a !== void 0 ? _a : content;
1217
1469
  const hasContent = actualContent !== null && actualContent !== undefined;
1218
- const canShow = show && Object.keys(computedPosition.tooltipStyles).length > 0;
1470
+ const canShow = show && computedPosition.tooltipStyles.left !== undefined;
1471
+ const tooltipStyle = React.useMemo(() => ({
1472
+ ...externalStyles,
1473
+ ...computedPosition.tooltipStyles,
1474
+ opacity: opacity !== undefined && canShow ? opacity : undefined,
1475
+ }), [externalStyles, computedPosition.tooltipStyles, opacity, canShow]);
1476
+ const arrowBackground = React.useMemo(() => arrowColor
1477
+ ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`
1478
+ : undefined, [arrowColor]);
1479
+ const arrowStyle = React.useMemo(() => ({
1480
+ ...computedPosition.tooltipArrowStyles,
1481
+ background: arrowBackground,
1482
+ '--rt-arrow-size': `${arrowSize}px`,
1483
+ }), [computedPosition.tooltipArrowStyles, arrowBackground, arrowSize]);
1219
1484
  React.useImperativeHandle(forwardRef, () => ({
1220
1485
  open: (options) => {
1221
1486
  let imperativeAnchor = null;
@@ -1270,19 +1535,9 @@
1270
1535
  setRendered(false);
1271
1536
  setImperativeOptions(null);
1272
1537
  afterHide === null || afterHide === void 0 ? void 0 : afterHide();
1273
- }, style: {
1274
- ...externalStyles,
1275
- ...computedPosition.tooltipStyles,
1276
- opacity: opacity !== undefined && canShow ? opacity : undefined,
1277
- }, ref: tooltipRef },
1538
+ }, style: tooltipStyle, ref: tooltipRef },
1278
1539
  React.createElement(WrapperElement, { className: clsx('react-tooltip-content-wrapper', coreStyles['content'], styles['content']) }, actualContent),
1279
- React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: {
1280
- ...computedPosition.tooltipArrowStyles,
1281
- background: arrowColor
1282
- ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`
1283
- : undefined,
1284
- '--rt-arrow-size': `${arrowSize}px`,
1285
- }, ref: tooltipArrowRef }))) : null;
1540
+ React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: arrowStyle, ref: tooltipArrowRef }))) : null;
1286
1541
  if (!tooltipNode) {
1287
1542
  return null;
1288
1543
  }
@@ -1293,12 +1548,82 @@
1293
1548
  };
1294
1549
  var Tooltip$1 = React.memo(Tooltip);
1295
1550
 
1551
+ /**
1552
+ * Shared MutationObserver for data-tooltip-* attribute changes.
1553
+ * Instead of N observers (one per tooltip), a single observer watches
1554
+ * all active anchors and dispatches changes to registered callbacks.
1555
+ */
1556
+ const observedElements = new Map();
1557
+ let sharedObserver = null;
1558
+ const observerConfig = {
1559
+ attributes: true,
1560
+ childList: false,
1561
+ subtree: false,
1562
+ };
1563
+ function getObserver() {
1564
+ if (!sharedObserver) {
1565
+ sharedObserver = new MutationObserver((mutationList) => {
1566
+ var _a;
1567
+ for (const mutation of mutationList) {
1568
+ if (mutation.type !== 'attributes' ||
1569
+ !((_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.startsWith('data-tooltip-'))) {
1570
+ continue;
1571
+ }
1572
+ const target = mutation.target;
1573
+ const callbacks = observedElements.get(target);
1574
+ if (callbacks) {
1575
+ callbacks.forEach((cb) => cb(target));
1576
+ }
1577
+ }
1578
+ });
1579
+ }
1580
+ return sharedObserver;
1581
+ }
1582
+ function observeAnchorAttributes(element, callback) {
1583
+ const observer = getObserver();
1584
+ let callbacks = observedElements.get(element);
1585
+ if (!callbacks) {
1586
+ callbacks = new Set();
1587
+ observedElements.set(element, callbacks);
1588
+ observer.observe(element, observerConfig);
1589
+ }
1590
+ callbacks.add(callback);
1591
+ return () => {
1592
+ const cbs = observedElements.get(element);
1593
+ if (cbs) {
1594
+ cbs.delete(callback);
1595
+ if (cbs.size === 0) {
1596
+ observedElements.delete(element);
1597
+ // MutationObserver doesn't have unobserve — if no elements left, disconnect & reset
1598
+ if (observedElements.size === 0) {
1599
+ observer.disconnect();
1600
+ }
1601
+ else {
1602
+ // Re-observe remaining elements (MutationObserver has no per-target unobserve)
1603
+ observer.disconnect();
1604
+ observedElements.forEach((_cbs, el) => {
1605
+ observer.observe(el, observerConfig);
1606
+ });
1607
+ }
1608
+ }
1609
+ }
1610
+ };
1611
+ }
1612
+
1296
1613
  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) => {
1297
1614
  var _a, _b, _c, _d, _e, _f, _g, _h;
1298
1615
  const [activeAnchor, setActiveAnchor] = React.useState(null);
1299
1616
  const [anchorDataAttributes, setAnchorDataAttributes] = React.useState({});
1300
1617
  const previousActiveAnchorRef = React.useRef(null);
1301
1618
  const styleInjectionRef = React.useRef(disableStyleInjection);
1619
+ const handleSetActiveAnchor = React.useCallback((anchor) => {
1620
+ setActiveAnchor((prev) => {
1621
+ if (!(anchor === null || anchor === void 0 ? void 0 : anchor.isSameNode(prev))) {
1622
+ previousActiveAnchorRef.current = prev;
1623
+ }
1624
+ return anchor;
1625
+ });
1626
+ }, []);
1302
1627
  /* c8 ignore start */
1303
1628
  const getDataAttributesFromAnchorElement = (elementReference) => {
1304
1629
  const dataAttributes = elementReference === null || elementReference === void 0 ? void 0 : elementReference.getAttributeNames().reduce((acc, name) => {
@@ -1330,37 +1655,24 @@
1330
1655
  // eslint-disable-next-line react-hooks/exhaustive-deps
1331
1656
  }, []);
1332
1657
  React.useEffect(() => {
1333
- const observerCallback = (mutationList) => {
1334
- mutationList.forEach((mutation) => {
1335
- var _a;
1336
- if (!activeAnchor ||
1337
- mutation.type !== 'attributes' ||
1338
- !((_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.startsWith('data-tooltip-'))) {
1339
- return;
1340
- }
1341
- // make sure to get all set attributes, since all unset attributes are reset
1342
- const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor);
1343
- setAnchorDataAttributes(dataAttributes);
1344
- });
1345
- };
1346
- // Create an observer instance linked to the callback function
1347
- const observer = new MutationObserver(observerCallback);
1348
- // do not check for subtree and childrens, we only want to know attribute changes
1349
- // to stay watching `data-attributes-*` from anchor element
1350
- const observerConfig = { attributes: true, childList: false, subtree: false };
1351
- if (activeAnchor) {
1352
- const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor);
1353
- setAnchorDataAttributes(dataAttributes);
1354
- // Start observing the target node for configured mutations
1355
- observer.observe(activeAnchor, observerConfig);
1356
- }
1357
- else {
1658
+ if (!activeAnchor) {
1358
1659
  setAnchorDataAttributes({});
1660
+ return () => { };
1359
1661
  }
1360
- return () => {
1361
- // Remove the observer when the tooltip is destroyed
1362
- observer.disconnect();
1662
+ const updateAttributes = (element) => {
1663
+ const attrs = getDataAttributesFromAnchorElement(element);
1664
+ setAnchorDataAttributes((prev) => {
1665
+ const keys = Object.keys(attrs);
1666
+ const prevKeys = Object.keys(prev);
1667
+ if (keys.length === prevKeys.length && keys.every((key) => attrs[key] === prev[key])) {
1668
+ return prev;
1669
+ }
1670
+ return attrs;
1671
+ });
1363
1672
  };
1673
+ updateAttributes(activeAnchor);
1674
+ const unsubscribe = observeAnchorAttributes(activeAnchor, updateAttributes);
1675
+ return unsubscribe;
1364
1676
  }, [activeAnchor, anchorSelect]);
1365
1677
  React.useEffect(() => {
1366
1678
  /* c8 ignore start */
@@ -1441,19 +1753,12 @@
1441
1753
  disableTooltip,
1442
1754
  activeAnchor,
1443
1755
  previousActiveAnchor: previousActiveAnchorRef.current,
1444
- setActiveAnchor: (anchor) => {
1445
- setActiveAnchor((prev) => {
1446
- if (!(anchor === null || anchor === void 0 ? void 0 : anchor.isSameNode(prev))) {
1447
- previousActiveAnchorRef.current = prev;
1448
- }
1449
- return anchor;
1450
- });
1451
- },
1756
+ setActiveAnchor: handleSetActiveAnchor,
1452
1757
  role,
1453
1758
  };
1454
1759
  return React.createElement(Tooltip$1, { ...props });
1455
1760
  });
1456
- var TooltipController$1 = React.memo(TooltipController);
1761
+ var TooltipController_default = React.memo(TooltipController);
1457
1762
 
1458
1763
  // those content will be replaced in build time with the `react-tooltip.css` builded content
1459
1764
  const TooltipCoreStyles = `:root {
@@ -1475,7 +1780,6 @@
1475
1780
  left: 0;
1476
1781
  pointer-events: none;
1477
1782
  opacity: 0;
1478
- will-change: opacity;
1479
1783
  }
1480
1784
 
1481
1785
  .core-styles-module_fixed__pcSol {
@@ -1506,11 +1810,13 @@
1506
1810
  .core-styles-module_show__Nt9eE {
1507
1811
  opacity: var(--rt-opacity);
1508
1812
  transition: opacity var(--rt-transition-show-delay) ease-out;
1813
+ will-change: opacity;
1509
1814
  }
1510
1815
 
1511
1816
  .core-styles-module_closing__sGnxF {
1512
1817
  opacity: 0;
1513
1818
  transition: opacity var(--rt-transition-closing-delay) ease-in;
1819
+ will-change: opacity;
1514
1820
  }
1515
1821
 
1516
1822
  `;
@@ -1591,7 +1897,7 @@
1591
1897
  }));
1592
1898
  }
1593
1899
 
1594
- exports.Tooltip = TooltipController$1;
1900
+ exports.Tooltip = TooltipController_default;
1595
1901
 
1596
1902
  }));
1597
1903
  //# sourceMappingURL=react-tooltip.umd.js.map