react-tooltip 6.0.0-beta.1179.rc.9 → 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,50 +1004,19 @@
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
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,
@@ -843,6 +1041,18 @@
843
1041
  const lastFloatPosition = React.useRef(null);
844
1042
  const hoveringTooltip = React.useRef(false);
845
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
+ });
846
1056
  /**
847
1057
  * useLayoutEffect runs before useEffect,
848
1058
  * but should be used carefully because of caveats
@@ -903,7 +1113,6 @@
903
1113
  else {
904
1114
  removeAriaDescribedBy(activeAnchor);
905
1115
  }
906
- // eslint-disable-next-line consistent-return
907
1116
  return () => {
908
1117
  // cleanup aria-describedby when the tooltip is closed
909
1118
  removeAriaDescribedBy(activeAnchor);
@@ -941,8 +1150,11 @@
941
1150
  /**
942
1151
  * see `onTransitionEnd` on tooltip wrapper
943
1152
  */
944
- const style = getComputedStyle(document.body);
945
- 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;
946
1158
  missedTransitionTimerRef.current = setTimeout(() => {
947
1159
  /**
948
1160
  * if the tooltip switches from `show === true` to `show === false` too fast
@@ -973,9 +1185,26 @@
973
1185
  if (!mounted.current) {
974
1186
  return;
975
1187
  }
976
- setComputedPosition((oldComputedPosition) => deepEqual(oldComputedPosition, newComputedPosition)
977
- ? oldComputedPosition
978
- : 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
+ });
979
1208
  }, []);
980
1209
  const handleShowTooltipDelayed = React.useCallback((delay = delayShow) => {
981
1210
  if (tooltipShowDelayTimerRef.current) {
@@ -1003,24 +1232,20 @@
1003
1232
  }, [delayHide, handleShow]);
1004
1233
  const handleTooltipPosition = React.useCallback(({ x, y }) => {
1005
1234
  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
- };
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
+ });
1020
1245
  computeTooltipPosition({
1021
1246
  place: (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place) !== null && _a !== void 0 ? _a : place,
1022
1247
  offset,
1023
- elementReference: virtualElement,
1248
+ elementReference: virtualElementRef.current,
1024
1249
  tooltipReference: tooltipRef.current,
1025
1250
  tooltipArrowReference: tooltipArrowRef.current,
1026
1251
  strategy: positionStrategy,
@@ -1105,17 +1330,24 @@
1105
1330
  clearTimeoutRef(tooltipHideDelayTimerRef);
1106
1331
  clearTimeoutRef(tooltipAutoCloseTimerRef);
1107
1332
  }, [handleShow, setActiveAnchor]);
1108
- 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({
1109
1339
  id,
1110
1340
  anchorSelect,
1111
1341
  imperativeAnchorSelect: imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect,
1112
1342
  activeAnchor,
1113
1343
  disableTooltip,
1114
1344
  onActiveAnchorRemoved: handleActiveAnchorRemoved,
1345
+ trackAnchors: shouldTrackAnchors,
1115
1346
  });
1116
1347
  useTooltipEvents({
1117
1348
  activeAnchor,
1118
1349
  anchorElements,
1350
+ anchorSelector,
1119
1351
  clickable,
1120
1352
  closeEvents,
1121
1353
  delayHide,
@@ -1139,11 +1371,16 @@
1139
1371
  tooltipShowDelayTimerRef,
1140
1372
  updateTooltipPosition,
1141
1373
  });
1374
+ const updateTooltipPositionRef = React.useRef(updateTooltipPosition);
1375
+ updateTooltipPositionRef.current = updateTooltipPosition;
1142
1376
  React.useEffect(() => {
1377
+ if (!rendered) {
1378
+ return;
1379
+ }
1143
1380
  updateTooltipPosition();
1144
- }, [updateTooltipPosition]);
1381
+ }, [rendered, updateTooltipPosition]);
1145
1382
  React.useEffect(() => {
1146
- if (!(contentWrapperRef === null || contentWrapperRef === void 0 ? void 0 : contentWrapperRef.current)) {
1383
+ if (!rendered || !(contentWrapperRef === null || contentWrapperRef === void 0 ? void 0 : contentWrapperRef.current)) {
1147
1384
  return () => null;
1148
1385
  }
1149
1386
  let timeoutId = null;
@@ -1154,7 +1391,7 @@
1154
1391
  }
1155
1392
  timeoutId = setTimeout(() => {
1156
1393
  if (mounted.current) {
1157
- updateTooltipPosition();
1394
+ updateTooltipPositionRef.current();
1158
1395
  }
1159
1396
  timeoutId = null;
1160
1397
  }, 0);
@@ -1166,9 +1403,13 @@
1166
1403
  clearTimeout(timeoutId);
1167
1404
  }
1168
1405
  };
1169
- }, [content, contentWrapperRef, updateTooltipPosition]);
1406
+ }, [content, contentWrapperRef, rendered]);
1170
1407
  React.useEffect(() => {
1171
1408
  var _a;
1409
+ const shouldResolveInitialActiveAnchor = rendered || defaultIsOpen || Boolean(isOpen);
1410
+ if (!shouldResolveInitialActiveAnchor) {
1411
+ return;
1412
+ }
1172
1413
  const activeAnchorMatchesImperativeSelector = (() => {
1173
1414
  if (!activeAnchor || !(imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect)) {
1174
1415
  return false;
@@ -1191,7 +1432,15 @@
1191
1432
  }
1192
1433
  setActiveAnchor((_a = anchorElements[0]) !== null && _a !== void 0 ? _a : null);
1193
1434
  }
1194
- }, [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
+ ]);
1195
1444
  React.useEffect(() => {
1196
1445
  if (defaultIsOpen) {
1197
1446
  handleShow(true);
@@ -1215,7 +1464,20 @@
1215
1464
  }, [delayShow, handleShowTooltipDelayed]);
1216
1465
  const actualContent = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.content) !== null && _a !== void 0 ? _a : content;
1217
1466
  const hasContent = actualContent !== null && actualContent !== undefined;
1218
- 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]);
1219
1481
  React.useImperativeHandle(forwardRef, () => ({
1220
1482
  open: (options) => {
1221
1483
  let imperativeAnchor = null;
@@ -1270,19 +1532,9 @@
1270
1532
  setRendered(false);
1271
1533
  setImperativeOptions(null);
1272
1534
  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 },
1535
+ }, style: tooltipStyle, ref: tooltipRef },
1278
1536
  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;
1537
+ React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: arrowStyle, ref: tooltipArrowRef }))) : null;
1286
1538
  if (!tooltipNode) {
1287
1539
  return null;
1288
1540
  }
@@ -1293,12 +1545,82 @@
1293
1545
  };
1294
1546
  var Tooltip$1 = React.memo(Tooltip);
1295
1547
 
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
+
1296
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) => {
1297
1611
  var _a, _b, _c, _d, _e, _f, _g, _h;
1298
1612
  const [activeAnchor, setActiveAnchor] = React.useState(null);
1299
1613
  const [anchorDataAttributes, setAnchorDataAttributes] = React.useState({});
1300
1614
  const previousActiveAnchorRef = React.useRef(null);
1301
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
+ }, []);
1302
1624
  /* c8 ignore start */
1303
1625
  const getDataAttributesFromAnchorElement = (elementReference) => {
1304
1626
  const dataAttributes = elementReference === null || elementReference === void 0 ? void 0 : elementReference.getAttributeNames().reduce((acc, name) => {
@@ -1330,37 +1652,24 @@
1330
1652
  // eslint-disable-next-line react-hooks/exhaustive-deps
1331
1653
  }, []);
1332
1654
  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 {
1655
+ if (!activeAnchor) {
1358
1656
  setAnchorDataAttributes({});
1657
+ return () => { };
1359
1658
  }
1360
- return () => {
1361
- // Remove the observer when the tooltip is destroyed
1362
- 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
+ });
1363
1669
  };
1670
+ updateAttributes(activeAnchor);
1671
+ const unsubscribe = observeAnchorAttributes(activeAnchor, updateAttributes);
1672
+ return unsubscribe;
1364
1673
  }, [activeAnchor, anchorSelect]);
1365
1674
  React.useEffect(() => {
1366
1675
  /* c8 ignore start */
@@ -1441,19 +1750,12 @@
1441
1750
  disableTooltip,
1442
1751
  activeAnchor,
1443
1752
  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
- },
1753
+ setActiveAnchor: handleSetActiveAnchor,
1452
1754
  role,
1453
1755
  };
1454
1756
  return React.createElement(Tooltip$1, { ...props });
1455
1757
  });
1456
- var TooltipController$1 = React.memo(TooltipController);
1758
+ var TooltipController_default = React.memo(TooltipController);
1457
1759
 
1458
1760
  // those content will be replaced in build time with the `react-tooltip.css` builded content
1459
1761
  const TooltipCoreStyles = `:root {
@@ -1475,7 +1777,6 @@
1475
1777
  left: 0;
1476
1778
  pointer-events: none;
1477
1779
  opacity: 0;
1478
- will-change: opacity;
1479
1780
  }
1480
1781
 
1481
1782
  .core-styles-module_fixed__pcSol {
@@ -1506,11 +1807,13 @@
1506
1807
  .core-styles-module_show__Nt9eE {
1507
1808
  opacity: var(--rt-opacity);
1508
1809
  transition: opacity var(--rt-transition-show-delay) ease-out;
1810
+ will-change: opacity;
1509
1811
  }
1510
1812
 
1511
1813
  .core-styles-module_closing__sGnxF {
1512
1814
  opacity: 0;
1513
1815
  transition: opacity var(--rt-transition-closing-delay) ease-in;
1816
+ will-change: opacity;
1514
1817
  }
1515
1818
 
1516
1819
  `;
@@ -1591,7 +1894,7 @@
1591
1894
  }));
1592
1895
  }
1593
1896
 
1594
- exports.Tooltip = TooltipController$1;
1897
+ exports.Tooltip = TooltipController_default;
1595
1898
 
1596
1899
  }));
1597
1900
  //# sourceMappingURL=react-tooltip.umd.js.map