react-tooltip 6.0.0-beta.1179.rc.16 → 6.0.0-beta.1179.rc.18

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.
@@ -84,13 +84,10 @@ function injectStyle({ css, id = REACT_TOOLTIP_BASE_STYLES_ID, type = 'base', re
84
84
  }
85
85
  }
86
86
 
87
- const computeTooltipPosition = async ({ elementReference = null, tooltipReference = null, tooltipArrowReference = null, place = 'top', offset: offsetValue = 10, strategy = 'absolute', middlewares = [
88
- dom.offset(Number(offsetValue)),
89
- dom.flip({
90
- fallbackAxisSideDirection: 'start',
91
- }),
92
- dom.shift({ padding: 5 }),
93
- ], border, arrowSize = 8, }) => {
87
+ // Hoisted constant middlewares these configs never change
88
+ const defaultFlip = dom.flip({ fallbackAxisSideDirection: 'start' });
89
+ const defaultShift = dom.shift({ padding: 5 });
90
+ 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, }) => {
94
91
  if (!elementReference) {
95
92
  // elementReference can be null or undefined and we will not compute the position
96
93
  // console.error('The reference element for tooltip was not defined: ', elementReference)
@@ -177,6 +174,7 @@ const cssTimeToMs = (time) => {
177
174
  */
178
175
  const debounce = (func, wait, immediate) => {
179
176
  let timeout = null;
177
+ let currentFunc = func;
180
178
  const debounced = function debounced(...args) {
181
179
  const later = () => {
182
180
  timeout = null;
@@ -186,7 +184,7 @@ const debounce = (func, wait, immediate) => {
186
184
  * there's no need to clear the timeout
187
185
  * since we expect it to resolve and set `timeout = null`
188
186
  */
189
- func.apply(this, args);
187
+ currentFunc.apply(this, args);
190
188
  timeout = setTimeout(later, wait);
191
189
  }
192
190
  };
@@ -199,36 +197,12 @@ const debounce = (func, wait, immediate) => {
199
197
  clearTimeout(timeout);
200
198
  timeout = null;
201
199
  };
200
+ debounced.setCallback = (newFunc) => {
201
+ currentFunc = newFunc;
202
+ };
202
203
  return debounced;
203
204
  };
204
205
 
205
- const isObject = (object) => {
206
- return object !== null && !Array.isArray(object) && typeof object === 'object';
207
- };
208
- const deepEqual = (object1, object2) => {
209
- if (object1 === object2) {
210
- return true;
211
- }
212
- if (Array.isArray(object1) && Array.isArray(object2)) {
213
- if (object1.length !== object2.length) {
214
- return false;
215
- }
216
- return object1.every((val, index) => deepEqual(val, object2[index]));
217
- }
218
- if (Array.isArray(object1) !== Array.isArray(object2)) {
219
- return false;
220
- }
221
- if (!isObject(object1) || !isObject(object2)) {
222
- return object1 === object2;
223
- }
224
- const keys1 = Object.keys(object1);
225
- const keys2 = Object.keys(object2);
226
- if (keys1.length !== keys2.length) {
227
- return false;
228
- }
229
- return keys1.every((key) => deepEqual(object1[key], object2[key]));
230
- };
231
-
232
206
  const isScrollable = (node) => {
233
207
  if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
234
208
  return false;
@@ -298,6 +272,14 @@ var styles = {"tooltip":"styles-module_tooltip__mnnfp","content":"styles-module_
298
272
 
299
273
  const registry = new Map();
300
274
  let documentObserver = null;
275
+ /**
276
+ * Extract a tooltip ID from a simple `[data-tooltip-id='value']` selector.
277
+ * Returns null for complex or custom selectors.
278
+ */
279
+ function extractTooltipId(selector) {
280
+ const match = selector.match(/^\[data-tooltip-id=(['"])((?:\\.|(?!\1).)*)\1\]$/);
281
+ return match ? match[2].replace(/\\(['"])/g, '$1') : null;
282
+ }
301
283
  function areAnchorListsEqual(left, right) {
302
284
  if (left.length !== right.length) {
303
285
  return false;
@@ -319,8 +301,7 @@ function readAnchorsForSelector(selector) {
319
301
  }
320
302
  }
321
303
  function notifySubscribers(entry) {
322
- const anchors = [...entry.anchors];
323
- entry.subscribers.forEach((subscriber) => subscriber(anchors, entry.error));
304
+ entry.subscribers.forEach((subscriber) => subscriber(entry.anchors, entry.error));
324
305
  }
325
306
  function refreshEntry(selector, entry) {
326
307
  var _a, _b, _c, _d;
@@ -344,12 +325,117 @@ function refreshAllEntries() {
344
325
  refreshEntry(selector, entry);
345
326
  });
346
327
  }
328
+ let refreshScheduled = false;
329
+ let pendingTooltipIds = null;
330
+ let pendingFullRefresh = false;
331
+ function scheduleRefresh(affectedTooltipIds) {
332
+ if (affectedTooltipIds) {
333
+ if (!pendingTooltipIds) {
334
+ pendingTooltipIds = new Set();
335
+ }
336
+ affectedTooltipIds.forEach((id) => pendingTooltipIds.add(id));
337
+ }
338
+ else {
339
+ pendingFullRefresh = true;
340
+ }
341
+ if (refreshScheduled) {
342
+ return;
343
+ }
344
+ refreshScheduled = true;
345
+ const flush = () => {
346
+ refreshScheduled = false;
347
+ const fullRefresh = pendingFullRefresh;
348
+ const ids = pendingTooltipIds;
349
+ pendingFullRefresh = false;
350
+ pendingTooltipIds = null;
351
+ if (fullRefresh) {
352
+ refreshAllEntries();
353
+ }
354
+ else if (ids && ids.size > 0) {
355
+ refreshEntriesForTooltipIds(ids);
356
+ }
357
+ };
358
+ if (typeof requestAnimationFrame === 'function') {
359
+ requestAnimationFrame(flush);
360
+ }
361
+ else {
362
+ Promise.resolve().then(flush);
363
+ }
364
+ }
365
+ /**
366
+ * Only refresh entries whose tooltipId is in the affected set,
367
+ * plus any entries with custom (non-tooltipId) selectors.
368
+ */
369
+ function refreshEntriesForTooltipIds(affectedIds) {
370
+ registry.forEach((entry, selector) => {
371
+ if (entry.tooltipId === null || affectedIds.has(entry.tooltipId)) {
372
+ refreshEntry(selector, entry);
373
+ }
374
+ });
375
+ }
376
+ /**
377
+ * Collect tooltip IDs from mutation records. Returns null when targeted
378
+ * analysis is not worthwhile (few registry entries, or too many nodes to scan).
379
+ */
380
+ function collectAffectedTooltipIds(records) {
381
+ var _a;
382
+ // Targeted refresh only pays off when there are many distinct selectors.
383
+ // With few entries, full refresh is already cheap — skip the analysis overhead.
384
+ if (registry.size <= 4) {
385
+ return null;
386
+ }
387
+ const ids = new Set();
388
+ for (const record of records) {
389
+ if (record.type === 'attributes') {
390
+ const target = record.target;
391
+ const currentId = (_a = target.getAttribute) === null || _a === void 0 ? void 0 : _a.call(target, 'data-tooltip-id');
392
+ if (currentId)
393
+ ids.add(currentId);
394
+ if (record.oldValue)
395
+ ids.add(record.oldValue);
396
+ continue;
397
+ }
398
+ if (record.type === 'childList') {
399
+ const gatherIds = (nodes) => {
400
+ var _a, _b;
401
+ for (let i = 0; i < nodes.length; i++) {
402
+ const node = nodes[i];
403
+ if (node.nodeType !== Node.ELEMENT_NODE)
404
+ continue;
405
+ const el = node;
406
+ const id = (_a = el.getAttribute) === null || _a === void 0 ? void 0 : _a.call(el, 'data-tooltip-id');
407
+ if (id)
408
+ ids.add(id);
409
+ // For large subtrees, bail out to full refresh to avoid double-scanning
410
+ const descendants = (_b = el.querySelectorAll) === null || _b === void 0 ? void 0 : _b.call(el, '[data-tooltip-id]');
411
+ if (descendants) {
412
+ if (descendants.length > 50) {
413
+ return true; // signal bail-out
414
+ }
415
+ for (let j = 0; j < descendants.length; j++) {
416
+ const descId = descendants[j].getAttribute('data-tooltip-id');
417
+ if (descId)
418
+ ids.add(descId);
419
+ }
420
+ }
421
+ }
422
+ return false;
423
+ };
424
+ if (gatherIds(record.addedNodes) || gatherIds(record.removedNodes)) {
425
+ return null; // large mutation — full refresh is cheaper
426
+ }
427
+ continue;
428
+ }
429
+ }
430
+ return ids;
431
+ }
347
432
  function ensureDocumentObserver() {
348
433
  if (documentObserver || typeof MutationObserver === 'undefined') {
349
434
  return;
350
435
  }
351
- documentObserver = new MutationObserver(() => {
352
- refreshAllEntries();
436
+ documentObserver = new MutationObserver((records) => {
437
+ const affectedIds = collectAffectedTooltipIds(records);
438
+ scheduleRefresh(affectedIds);
353
439
  });
354
440
  documentObserver.observe(document.body, {
355
441
  childList: true,
@@ -374,6 +460,7 @@ function subscribeAnchorSelector(selector, subscriber) {
374
460
  anchors: initialState.anchors,
375
461
  error: initialState.error,
376
462
  subscribers: new Set(),
463
+ tooltipId: extractTooltipId(selector),
377
464
  };
378
465
  registry.set(selector, entry);
379
466
  }
@@ -417,7 +504,8 @@ const useTooltipAnchors = ({ id, anchorSelect, imperativeAnchorSelect, activeAnc
417
504
  catch (_a) {
418
505
  return false;
419
506
  }
420
- }, [activeAnchor, selector]);
507
+ // eslint-disable-next-line react-hooks/exhaustive-deps
508
+ }, [activeAnchor, selector, anchorElements]);
421
509
  React.useEffect(() => {
422
510
  if (!selector || !trackAnchors) {
423
511
  setRawAnchorElements([]);
@@ -454,141 +542,75 @@ const useTooltipAnchors = ({ id, anchorSelect, imperativeAnchorSelect, activeAnc
454
542
  };
455
543
  };
456
544
 
545
+ /**
546
+ * Shared document event delegation.
547
+ *
548
+ * Instead of N tooltips each calling document.addEventListener(type, handler),
549
+ * we maintain ONE document listener per event type. When the event fires,
550
+ * we iterate through all registered handlers for that type.
551
+ *
552
+ * This reduces document-level listeners from O(N × eventTypes) to O(eventTypes).
553
+ */
554
+ const handlersByType = new Map();
555
+ function getOrCreateSet(eventType) {
556
+ let set = handlersByType.get(eventType);
557
+ if (!set) {
558
+ set = new Set();
559
+ handlersByType.set(eventType, set);
560
+ document.addEventListener(eventType, dispatch);
561
+ }
562
+ return set;
563
+ }
564
+ function dispatch(event) {
565
+ const handlers = handlersByType.get(event.type);
566
+ if (handlers) {
567
+ // Safe to iterate directly — mutations (add/remove) only happen in
568
+ // setup/cleanup, not during dispatch. Set iteration is stable for
569
+ // entries that existed when iteration began.
570
+ handlers.forEach((handler) => {
571
+ handler(event);
572
+ });
573
+ }
574
+ }
575
+ /**
576
+ * Register a handler for a document-level event type.
577
+ * Returns an unsubscribe function.
578
+ */
579
+ function addDelegatedEventListener(eventType, handler) {
580
+ const set = getOrCreateSet(eventType);
581
+ set.add(handler);
582
+ return () => {
583
+ set.delete(handler);
584
+ if (set.size === 0) {
585
+ handlersByType.delete(eventType);
586
+ document.removeEventListener(eventType, dispatch);
587
+ }
588
+ };
589
+ }
590
+
457
591
  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, }) => {
458
- React.useEffect(() => {
459
- const dataTooltipId = anchorSelector ? parseDataTooltipIdSelector(anchorSelector) : null;
460
- const resolveAnchorElement = (target) => {
461
- var _a, _b;
462
- const targetElement = target;
463
- if (!(targetElement === null || targetElement === void 0 ? void 0 : targetElement.isConnected)) {
464
- return null;
465
- }
466
- if (dataTooltipId) {
467
- const matchedAnchor = resolveDataTooltipAnchor(targetElement, dataTooltipId);
468
- if (matchedAnchor && !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(matchedAnchor))) {
469
- return matchedAnchor;
470
- }
471
- }
472
- else if (anchorSelector) {
473
- try {
474
- const matchedAnchor = (_a = (targetElement.matches(anchorSelector)
475
- ? targetElement
476
- : targetElement.closest(anchorSelector))) !== null && _a !== void 0 ? _a : null;
477
- if (matchedAnchor && !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(matchedAnchor))) {
478
- return matchedAnchor;
479
- }
480
- }
481
- catch (_c) {
482
- return null;
483
- }
484
- }
485
- return ((_b = anchorElements.find((anchor) => anchor === targetElement || anchor.contains(targetElement))) !== null && _b !== void 0 ? _b : null);
486
- };
487
- const handlePointerMove = (event) => {
488
- if (!event) {
489
- return;
490
- }
491
- if (!activeAnchor) {
492
- return;
493
- }
494
- const targetAnchor = resolveAnchorElement(event.target);
495
- if (targetAnchor !== activeAnchor) {
496
- return;
497
- }
498
- const mouseEvent = event;
499
- const mousePosition = {
500
- x: mouseEvent.clientX,
501
- y: mouseEvent.clientY,
502
- };
503
- handleTooltipPosition(mousePosition);
504
- lastFloatPosition.current = mousePosition;
505
- };
506
- const handleClickOutsideAnchors = (event) => {
507
- var _a;
508
- if (!show) {
509
- return;
510
- }
511
- const target = event.target;
512
- if (!(target === null || target === void 0 ? void 0 : target.isConnected)) {
513
- return;
514
- }
515
- if ((_a = tooltipRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) {
516
- return;
517
- }
518
- if (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.contains(target)) {
519
- return;
520
- }
521
- if (anchorElements.some((anchor) => anchor === null || anchor === void 0 ? void 0 : anchor.contains(target))) {
522
- return;
523
- }
524
- handleShow(false);
525
- clearTimeoutRef(tooltipShowDelayTimerRef);
526
- };
527
- const handleShowTooltip = (anchor) => {
528
- if (!anchor) {
529
- return;
530
- }
531
- if (!anchor.isConnected) {
532
- setActiveAnchor(null);
533
- return;
534
- }
535
- if (disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(anchor)) {
536
- return;
537
- }
538
- if (delayShow) {
539
- handleShowTooltipDelayed();
540
- }
541
- else {
542
- handleShow(true);
543
- }
544
- if (delayShow && activeAnchor && anchor !== activeAnchor) {
545
- // Moving to a different anchor while one is already active — defer the anchor
546
- // switch until the show delay fires to prevent content/position from updating
547
- // before visibility transitions complete.
548
- if (tooltipShowDelayTimerRef.current) {
549
- clearTimeout(tooltipShowDelayTimerRef.current);
550
- }
551
- tooltipShowDelayTimerRef.current = setTimeout(() => {
552
- setActiveAnchor(anchor);
553
- handleShow(true);
554
- }, delayShow);
555
- }
556
- else {
557
- setActiveAnchor(anchor);
558
- }
559
- if (tooltipHideDelayTimerRef.current) {
560
- clearTimeout(tooltipHideDelayTimerRef.current);
561
- }
562
- };
563
- const handleHideTooltip = () => {
564
- if (clickable) {
565
- handleHideTooltipDelayed(delayHide || 100);
566
- }
567
- else if (delayHide) {
568
- handleHideTooltipDelayed();
569
- }
570
- else {
571
- handleShow(false);
572
- }
573
- if (tooltipShowDelayTimerRef.current) {
574
- clearTimeout(tooltipShowDelayTimerRef.current);
575
- }
576
- };
577
- const internalDebouncedHandleShowTooltip = debounce(handleShowTooltip, 50);
578
- const internalDebouncedHandleHideTooltip = debounce(handleHideTooltip, 50);
579
- const debouncedHandleShowTooltip = (anchor) => {
580
- internalDebouncedHandleHideTooltip.cancel();
581
- internalDebouncedHandleShowTooltip(anchor);
582
- };
583
- const debouncedHandleHideTooltip = () => {
584
- internalDebouncedHandleShowTooltip.cancel();
585
- internalDebouncedHandleHideTooltip();
586
- };
587
- const handleScrollResize = () => {
588
- handleShow(false);
589
- };
590
- 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);
591
- const actualOpenEvents = openEvents
592
+ // Ref-stable debounced handlers — avoids recreating debounce instances on every effect run
593
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
594
+ const debouncedShowRef = React.useRef(debounce((_anchor) => { }, 50));
595
+ const debouncedHideRef = React.useRef(debounce(() => { }, 50));
596
+ // Cache scroll parents — only recompute when the element actually changes
597
+ const anchorScrollParentRef = React.useRef(null);
598
+ const tooltipScrollParentRef = React.useRef(null);
599
+ const prevAnchorRef = React.useRef(null);
600
+ const prevTooltipRef = React.useRef(null);
601
+ if (activeAnchor !== prevAnchorRef.current) {
602
+ prevAnchorRef.current = activeAnchor;
603
+ anchorScrollParentRef.current = getScrollParent(activeAnchor);
604
+ }
605
+ const currentTooltipEl = tooltipRef.current;
606
+ if (currentTooltipEl !== prevTooltipRef.current) {
607
+ prevTooltipRef.current = currentTooltipEl;
608
+ tooltipScrollParentRef.current = getScrollParent(currentTooltipEl);
609
+ }
610
+ // Memoize event config objects only rebuild when the relevant props change
611
+ 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);
612
+ const actualOpenEvents = React.useMemo(() => {
613
+ const events = openEvents
592
614
  ? { ...openEvents }
593
615
  : {
594
616
  mouseenter: true,
@@ -598,13 +620,25 @@ const useTooltipEvents = ({ activeAnchor, anchorElements, anchorSelector, clicka
598
620
  mousedown: false,
599
621
  };
600
622
  if (!openEvents && openOnClick) {
601
- Object.assign(actualOpenEvents, {
623
+ Object.assign(events, {
602
624
  mouseenter: false,
603
625
  focus: false,
604
626
  click: true,
605
627
  });
606
628
  }
607
- const actualCloseEvents = closeEvents
629
+ if (imperativeModeOnly) {
630
+ Object.assign(events, {
631
+ mouseenter: false,
632
+ focus: false,
633
+ click: false,
634
+ dblclick: false,
635
+ mousedown: false,
636
+ });
637
+ }
638
+ return events;
639
+ }, [openEvents, openOnClick, imperativeModeOnly]);
640
+ const actualCloseEvents = React.useMemo(() => {
641
+ const events = closeEvents
608
642
  ? { ...closeEvents }
609
643
  : {
610
644
  mouseleave: true,
@@ -614,12 +648,24 @@ const useTooltipEvents = ({ activeAnchor, anchorElements, anchorSelector, clicka
614
648
  mouseup: false,
615
649
  };
616
650
  if (!closeEvents && openOnClick) {
617
- Object.assign(actualCloseEvents, {
651
+ Object.assign(events, {
618
652
  mouseleave: false,
619
653
  blur: false,
620
654
  });
621
655
  }
622
- const actualGlobalCloseEvents = globalCloseEvents
656
+ if (imperativeModeOnly) {
657
+ Object.assign(events, {
658
+ mouseleave: false,
659
+ blur: false,
660
+ click: false,
661
+ dblclick: false,
662
+ mouseup: false,
663
+ });
664
+ }
665
+ return events;
666
+ }, [closeEvents, openOnClick, imperativeModeOnly]);
667
+ const actualGlobalCloseEvents = React.useMemo(() => {
668
+ const events = globalCloseEvents
623
669
  ? { ...globalCloseEvents }
624
670
  : {
625
671
  escape: false,
@@ -628,108 +674,157 @@ const useTooltipEvents = ({ activeAnchor, anchorElements, anchorSelector, clicka
628
674
  clickOutsideAnchor: hasClickEvent || false,
629
675
  };
630
676
  if (imperativeModeOnly) {
631
- Object.assign(actualOpenEvents, {
632
- mouseenter: false,
633
- focus: false,
634
- click: false,
635
- dblclick: false,
636
- mousedown: false,
637
- });
638
- Object.assign(actualCloseEvents, {
639
- mouseleave: false,
640
- blur: false,
641
- click: false,
642
- dblclick: false,
643
- mouseup: false,
644
- });
645
- Object.assign(actualGlobalCloseEvents, {
677
+ Object.assign(events, {
646
678
  escape: false,
647
679
  scroll: false,
648
680
  resize: false,
649
681
  clickOutsideAnchor: false,
650
682
  });
651
683
  }
652
- const tooltipElement = tooltipRef.current;
653
- const tooltipScrollParent = getScrollParent(tooltipRef.current);
654
- const anchorScrollParent = getScrollParent(activeAnchor);
655
- if (actualGlobalCloseEvents.scroll) {
656
- window.addEventListener('scroll', handleScrollResize);
657
- anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.addEventListener('scroll', handleScrollResize);
658
- tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.addEventListener('scroll', handleScrollResize);
684
+ return events;
685
+ }, [globalCloseEvents, hasClickEvent, imperativeModeOnly]);
686
+ // --- Refs for values read inside event handlers (avoids effect deps) ---
687
+ const activeAnchorRef = React.useRef(activeAnchor);
688
+ activeAnchorRef.current = activeAnchor;
689
+ const showRef = React.useRef(show);
690
+ showRef.current = show;
691
+ const anchorElementsRef = React.useRef(anchorElements);
692
+ anchorElementsRef.current = anchorElements;
693
+ const handleShowRef = React.useRef(handleShow);
694
+ handleShowRef.current = handleShow;
695
+ const handleTooltipPositionRef = React.useRef(handleTooltipPosition);
696
+ handleTooltipPositionRef.current = handleTooltipPosition;
697
+ const updateTooltipPositionRef = React.useRef(updateTooltipPosition);
698
+ updateTooltipPositionRef.current = updateTooltipPosition;
699
+ // --- Handler refs (updated every render, read via ref indirection in effects) ---
700
+ const resolveAnchorElementRef = React.useRef(() => null);
701
+ const handleShowTooltipRef = React.useRef(() => { });
702
+ const handleHideTooltipRef = React.useRef(() => { });
703
+ const dataTooltipId = anchorSelector ? parseDataTooltipIdSelector(anchorSelector) : null;
704
+ resolveAnchorElementRef.current = (target) => {
705
+ var _a, _b;
706
+ const targetElement = target;
707
+ if (!(targetElement === null || targetElement === void 0 ? void 0 : targetElement.isConnected)) {
708
+ return null;
709
+ }
710
+ if (dataTooltipId) {
711
+ const matchedAnchor = resolveDataTooltipAnchor(targetElement, dataTooltipId);
712
+ if (matchedAnchor && !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(matchedAnchor))) {
713
+ return matchedAnchor;
714
+ }
659
715
  }
660
- let updateTooltipCleanup = null;
661
- if (actualGlobalCloseEvents.resize) {
662
- window.addEventListener('resize', handleScrollResize);
716
+ else if (anchorSelector) {
717
+ try {
718
+ const matchedAnchor = (_a = (targetElement.matches(anchorSelector)
719
+ ? targetElement
720
+ : targetElement.closest(anchorSelector))) !== null && _a !== void 0 ? _a : null;
721
+ if (matchedAnchor && !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(matchedAnchor))) {
722
+ return matchedAnchor;
723
+ }
724
+ }
725
+ catch (_c) {
726
+ return null;
727
+ }
663
728
  }
664
- else if (activeAnchor && tooltipRef.current) {
665
- updateTooltipCleanup = dom.autoUpdate(activeAnchor, tooltipRef.current, updateTooltipPosition, {
666
- ancestorResize: true,
667
- elementResize: true,
668
- layoutShift: true,
669
- });
729
+ return ((_b = anchorElementsRef.current.find((anchor) => anchor === targetElement || anchor.contains(targetElement))) !== null && _b !== void 0 ? _b : null);
730
+ };
731
+ handleShowTooltipRef.current = (anchor) => {
732
+ if (!anchor) {
733
+ return;
670
734
  }
671
- const handleEsc = (event) => {
672
- if (event.key !== 'Escape') {
673
- return;
735
+ if (!anchor.isConnected) {
736
+ setActiveAnchor(null);
737
+ return;
738
+ }
739
+ if (disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(anchor)) {
740
+ return;
741
+ }
742
+ if (delayShow) {
743
+ handleShowTooltipDelayed();
744
+ }
745
+ else {
746
+ handleShow(true);
747
+ }
748
+ if (delayShow && activeAnchorRef.current && anchor !== activeAnchorRef.current) {
749
+ // Moving to a different anchor while one is already active — defer the anchor
750
+ // switch until the show delay fires to prevent content/position from updating
751
+ // before visibility transitions complete.
752
+ if (tooltipShowDelayTimerRef.current) {
753
+ clearTimeout(tooltipShowDelayTimerRef.current);
674
754
  }
755
+ tooltipShowDelayTimerRef.current = setTimeout(() => {
756
+ setActiveAnchor(anchor);
757
+ handleShow(true);
758
+ }, delayShow);
759
+ }
760
+ else {
761
+ setActiveAnchor(anchor);
762
+ }
763
+ if (tooltipHideDelayTimerRef.current) {
764
+ clearTimeout(tooltipHideDelayTimerRef.current);
765
+ }
766
+ };
767
+ handleHideTooltipRef.current = () => {
768
+ if (clickable) {
769
+ handleHideTooltipDelayed(delayHide || 100);
770
+ }
771
+ else if (delayHide) {
772
+ handleHideTooltipDelayed();
773
+ }
774
+ else {
675
775
  handleShow(false);
676
- };
677
- if (actualGlobalCloseEvents.escape) {
678
- window.addEventListener('keydown', handleEsc);
679
776
  }
680
- if (actualGlobalCloseEvents.clickOutsideAnchor) {
681
- window.addEventListener('click', handleClickOutsideAnchors);
777
+ if (tooltipShowDelayTimerRef.current) {
778
+ clearTimeout(tooltipShowDelayTimerRef.current);
682
779
  }
683
- const activeAnchorContainsTarget = (event) => Boolean((event === null || event === void 0 ? void 0 : event.target) && (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.contains(event.target)));
684
- const handleClickOpenTooltipAnchor = (event) => {
685
- var _a;
686
- const anchor = resolveAnchorElement((_a = event === null || event === void 0 ? void 0 : event.target) !== null && _a !== void 0 ? _a : null);
687
- if (!anchor) {
688
- return;
689
- }
690
- if (show && activeAnchor === anchor) {
691
- return;
692
- }
693
- handleShowTooltip(anchor);
780
+ };
781
+ // Update debounced callbacks to always delegate to latest handler refs
782
+ const debouncedShow = debouncedShowRef.current;
783
+ const debouncedHide = debouncedHideRef.current;
784
+ debouncedShow.setCallback((anchor) => handleShowTooltipRef.current(anchor));
785
+ debouncedHide.setCallback(() => handleHideTooltipRef.current());
786
+ // --- Effect 1: Delegated anchor events + tooltip hover ---
787
+ // Only re-runs when the set of active event types or interaction mode changes.
788
+ // Handlers read reactive values (activeAnchor, show, etc.) from refs at invocation
789
+ // time, so this effect is decoupled from show/hide state changes.
790
+ React.useEffect(() => {
791
+ const cleanupFns = [];
792
+ const addDelegatedListener = (eventType, listener) => {
793
+ cleanupFns.push(addDelegatedEventListener(eventType, listener));
694
794
  };
695
- const handleClickCloseTooltipAnchor = (event) => {
696
- if (!show || !activeAnchorContainsTarget(event)) {
697
- return;
698
- }
699
- handleHideTooltip();
795
+ 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))); };
796
+ const debouncedHandleShowTooltip = (anchor) => {
797
+ debouncedHide.cancel();
798
+ debouncedShow(anchor);
799
+ };
800
+ const debouncedHandleHideTooltip = () => {
801
+ debouncedShow.cancel();
802
+ debouncedHide();
700
803
  };
701
- const regularEvents = ['mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'focus', 'blur'];
702
- const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup'];
703
- const delegatedEvents = [];
704
804
  const addDelegatedHoverOpenListener = () => {
705
- delegatedEvents.push({
706
- event: 'mouseover',
707
- listener: (event) => {
708
- const anchor = resolveAnchorElement(event.target);
709
- if (!anchor) {
710
- return;
711
- }
712
- const relatedAnchor = resolveAnchorElement(event.relatedTarget);
713
- if (relatedAnchor === anchor) {
714
- return;
715
- }
716
- debouncedHandleShowTooltip(anchor);
717
- },
805
+ addDelegatedListener('mouseover', (event) => {
806
+ const anchor = resolveAnchorElementRef.current(event.target);
807
+ if (!anchor) {
808
+ return;
809
+ }
810
+ const relatedAnchor = resolveAnchorElementRef.current(event.relatedTarget);
811
+ if (relatedAnchor === anchor) {
812
+ return;
813
+ }
814
+ debouncedHandleShowTooltip(anchor);
718
815
  });
719
816
  };
720
817
  const addDelegatedHoverCloseListener = () => {
721
- delegatedEvents.push({
722
- event: 'mouseout',
723
- listener: (event) => {
724
- if (!activeAnchorContainsTarget(event)) {
725
- return;
726
- }
727
- const relatedTarget = event.relatedTarget;
728
- if (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.contains(relatedTarget)) {
729
- return;
730
- }
731
- debouncedHandleHideTooltip();
732
- },
818
+ addDelegatedListener('mouseout', (event) => {
819
+ var _a;
820
+ if (!activeAnchorContainsTarget(event)) {
821
+ return;
822
+ }
823
+ const relatedTarget = event.relatedTarget;
824
+ if ((_a = activeAnchorRef.current) === null || _a === void 0 ? void 0 : _a.contains(relatedTarget)) {
825
+ return;
826
+ }
827
+ debouncedHandleHideTooltip();
733
828
  });
734
829
  };
735
830
  if (actualOpenEvents.mouseenter) {
@@ -745,37 +840,48 @@ const useTooltipEvents = ({ activeAnchor, anchorElements, anchorSelector, clicka
745
840
  addDelegatedHoverCloseListener();
746
841
  }
747
842
  if (actualOpenEvents.focus) {
748
- delegatedEvents.push({
749
- event: 'focusin',
750
- listener: (event) => {
751
- debouncedHandleShowTooltip(resolveAnchorElement(event.target));
752
- },
843
+ addDelegatedListener('focusin', (event) => {
844
+ debouncedHandleShowTooltip(resolveAnchorElementRef.current(event.target));
753
845
  });
754
846
  }
755
847
  if (actualCloseEvents.blur) {
756
- delegatedEvents.push({
757
- event: 'focusout',
758
- listener: (event) => {
759
- if (!activeAnchorContainsTarget(event)) {
760
- return;
761
- }
762
- const relatedTarget = event.relatedTarget;
763
- if (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.contains(relatedTarget)) {
764
- return;
765
- }
766
- debouncedHandleHideTooltip();
767
- },
848
+ addDelegatedListener('focusout', (event) => {
849
+ var _a;
850
+ if (!activeAnchorContainsTarget(event)) {
851
+ return;
852
+ }
853
+ const relatedTarget = event.relatedTarget;
854
+ if ((_a = activeAnchorRef.current) === null || _a === void 0 ? void 0 : _a.contains(relatedTarget)) {
855
+ return;
856
+ }
857
+ debouncedHandleHideTooltip();
768
858
  });
769
859
  }
860
+ const regularEvents = ['mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'focus', 'blur'];
861
+ const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup'];
862
+ const handleClickOpenTooltipAnchor = (event) => {
863
+ var _a;
864
+ const anchor = resolveAnchorElementRef.current((_a = event === null || event === void 0 ? void 0 : event.target) !== null && _a !== void 0 ? _a : null);
865
+ if (!anchor) {
866
+ return;
867
+ }
868
+ if (showRef.current && activeAnchorRef.current === anchor) {
869
+ return;
870
+ }
871
+ handleShowTooltipRef.current(anchor);
872
+ };
873
+ const handleClickCloseTooltipAnchor = (event) => {
874
+ if (!showRef.current || !activeAnchorContainsTarget(event)) {
875
+ return;
876
+ }
877
+ handleHideTooltipRef.current();
878
+ };
770
879
  Object.entries(actualOpenEvents).forEach(([event, enabled]) => {
771
880
  if (!enabled || regularEvents.includes(event)) {
772
881
  return;
773
882
  }
774
883
  if (clickEvents.includes(event)) {
775
- delegatedEvents.push({
776
- event,
777
- listener: handleClickOpenTooltipAnchor,
778
- });
884
+ addDelegatedListener(event, handleClickOpenTooltipAnchor);
779
885
  }
780
886
  });
781
887
  Object.entries(actualCloseEvents).forEach(([event, enabled]) => {
@@ -783,33 +889,110 @@ const useTooltipEvents = ({ activeAnchor, anchorElements, anchorSelector, clicka
783
889
  return;
784
890
  }
785
891
  if (clickEvents.includes(event)) {
786
- delegatedEvents.push({
787
- event,
788
- listener: handleClickCloseTooltipAnchor,
789
- });
892
+ addDelegatedListener(event, handleClickCloseTooltipAnchor);
790
893
  }
791
894
  });
792
895
  if (float) {
793
- delegatedEvents.push({
794
- event: 'pointermove',
795
- listener: handlePointerMove,
896
+ addDelegatedListener('pointermove', (event) => {
897
+ const currentActiveAnchor = activeAnchorRef.current;
898
+ if (!currentActiveAnchor) {
899
+ return;
900
+ }
901
+ const targetAnchor = resolveAnchorElementRef.current(event.target);
902
+ if (targetAnchor !== currentActiveAnchor) {
903
+ return;
904
+ }
905
+ const mouseEvent = event;
906
+ const mousePosition = {
907
+ x: mouseEvent.clientX,
908
+ y: mouseEvent.clientY,
909
+ };
910
+ handleTooltipPositionRef.current(mousePosition);
911
+ lastFloatPosition.current = mousePosition;
796
912
  });
797
913
  }
914
+ const tooltipElement = tooltipRef.current;
798
915
  const handleMouseOverTooltip = () => {
799
916
  hoveringTooltip.current = true;
800
917
  };
801
918
  const handleMouseOutTooltip = () => {
802
919
  hoveringTooltip.current = false;
803
- handleHideTooltip();
920
+ handleHideTooltipRef.current();
804
921
  };
805
922
  const addHoveringTooltipListeners = clickable && (actualCloseEvents.mouseout || actualCloseEvents.mouseleave);
806
923
  if (addHoveringTooltipListeners) {
807
924
  tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseover', handleMouseOverTooltip);
808
925
  tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseout', handleMouseOutTooltip);
809
926
  }
810
- delegatedEvents.forEach(({ event, listener }) => {
811
- document.addEventListener(event, listener);
812
- });
927
+ return () => {
928
+ cleanupFns.forEach((fn) => fn());
929
+ if (addHoveringTooltipListeners) {
930
+ tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseover', handleMouseOverTooltip);
931
+ tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseout', handleMouseOutTooltip);
932
+ }
933
+ debouncedShow.cancel();
934
+ debouncedHide.cancel();
935
+ };
936
+ // eslint-disable-next-line react-hooks/exhaustive-deps
937
+ }, [actualOpenEvents, actualCloseEvents, float, clickable]);
938
+ // --- Effect 2: Global close events + auto-update ---
939
+ // Re-runs when the global close config changes, or when the active anchor changes
940
+ // (for scroll parent listeners and floating-ui autoUpdate).
941
+ React.useEffect(() => {
942
+ const handleScrollResize = () => {
943
+ handleShowRef.current(false);
944
+ };
945
+ const tooltipScrollParent = tooltipScrollParentRef.current;
946
+ const anchorScrollParent = anchorScrollParentRef.current;
947
+ if (actualGlobalCloseEvents.scroll) {
948
+ window.addEventListener('scroll', handleScrollResize);
949
+ anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.addEventListener('scroll', handleScrollResize);
950
+ tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.addEventListener('scroll', handleScrollResize);
951
+ }
952
+ let updateTooltipCleanup = null;
953
+ if (actualGlobalCloseEvents.resize) {
954
+ window.addEventListener('resize', handleScrollResize);
955
+ }
956
+ else if (activeAnchor && tooltipRef.current) {
957
+ updateTooltipCleanup = dom.autoUpdate(activeAnchor, tooltipRef.current, () => updateTooltipPositionRef.current(), {
958
+ ancestorResize: true,
959
+ elementResize: true,
960
+ layoutShift: true,
961
+ });
962
+ }
963
+ const handleEsc = (event) => {
964
+ if (event.key !== 'Escape') {
965
+ return;
966
+ }
967
+ handleShowRef.current(false);
968
+ };
969
+ if (actualGlobalCloseEvents.escape) {
970
+ window.addEventListener('keydown', handleEsc);
971
+ }
972
+ const handleClickOutsideAnchors = (event) => {
973
+ var _a, _b;
974
+ if (!showRef.current) {
975
+ return;
976
+ }
977
+ const target = event.target;
978
+ if (!(target === null || target === void 0 ? void 0 : target.isConnected)) {
979
+ return;
980
+ }
981
+ if ((_a = tooltipRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) {
982
+ return;
983
+ }
984
+ if ((_b = activeAnchorRef.current) === null || _b === void 0 ? void 0 : _b.contains(target)) {
985
+ return;
986
+ }
987
+ if (anchorElementsRef.current.some((anchor) => anchor === null || anchor === void 0 ? void 0 : anchor.contains(target))) {
988
+ return;
989
+ }
990
+ handleShowRef.current(false);
991
+ clearTimeoutRef(tooltipShowDelayTimerRef);
992
+ };
993
+ if (actualGlobalCloseEvents.clickOutsideAnchor) {
994
+ window.addEventListener('click', handleClickOutsideAnchors);
995
+ }
813
996
  return () => {
814
997
  if (actualGlobalCloseEvents.scroll) {
815
998
  window.removeEventListener('scroll', handleScrollResize);
@@ -822,51 +1005,19 @@ const useTooltipEvents = ({ activeAnchor, anchorElements, anchorSelector, clicka
822
1005
  if (updateTooltipCleanup) {
823
1006
  updateTooltipCleanup();
824
1007
  }
825
- if (actualGlobalCloseEvents.clickOutsideAnchor) {
826
- window.removeEventListener('click', handleClickOutsideAnchors);
827
- }
828
1008
  if (actualGlobalCloseEvents.escape) {
829
1009
  window.removeEventListener('keydown', handleEsc);
830
1010
  }
831
- if (addHoveringTooltipListeners) {
832
- tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseover', handleMouseOverTooltip);
833
- tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseout', handleMouseOutTooltip);
1011
+ if (actualGlobalCloseEvents.clickOutsideAnchor) {
1012
+ window.removeEventListener('click', handleClickOutsideAnchors);
834
1013
  }
835
- delegatedEvents.forEach(({ event, listener }) => {
836
- document.removeEventListener(event, listener);
837
- });
838
- internalDebouncedHandleShowTooltip.cancel();
839
- internalDebouncedHandleHideTooltip.cancel();
840
1014
  };
841
- }, [
842
- activeAnchor,
843
- anchorElements,
844
- anchorSelector,
845
- clickable,
846
- closeEvents,
847
- delayHide,
848
- delayShow,
849
- disableTooltip,
850
- float,
851
- globalCloseEvents,
852
- handleHideTooltipDelayed,
853
- handleShow,
854
- handleShowTooltipDelayed,
855
- handleTooltipPosition,
856
- imperativeModeOnly,
857
- lastFloatPosition,
858
- openEvents,
859
- openOnClick,
860
- setActiveAnchor,
861
- show,
862
- tooltipHideDelayTimerRef,
863
- tooltipRef,
864
- tooltipShowDelayTimerRef,
865
- updateTooltipPosition,
866
- hoveringTooltip,
867
- ]);
1015
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1016
+ }, [actualGlobalCloseEvents, activeAnchor]);
868
1017
  };
869
1018
 
1019
+ // Shared across all tooltip instances — the CSS variable is on :root and never changes per-instance
1020
+ let globalTransitionShowDelay = null;
870
1021
  const Tooltip = ({
871
1022
  // props
872
1023
  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,
@@ -891,6 +1042,18 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
891
1042
  const lastFloatPosition = React.useRef(null);
892
1043
  const hoveringTooltip = React.useRef(false);
893
1044
  const mounted = React.useRef(false);
1045
+ const virtualElementRef = React.useRef({
1046
+ getBoundingClientRect: () => ({
1047
+ x: 0,
1048
+ y: 0,
1049
+ width: 0,
1050
+ height: 0,
1051
+ top: 0,
1052
+ left: 0,
1053
+ right: 0,
1054
+ bottom: 0,
1055
+ }),
1056
+ });
894
1057
  /**
895
1058
  * useLayoutEffect runs before useEffect,
896
1059
  * but should be used carefully because of caveats
@@ -988,8 +1151,11 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
988
1151
  /**
989
1152
  * see `onTransitionEnd` on tooltip wrapper
990
1153
  */
991
- const style = getComputedStyle(document.body);
992
- const transitionShowDelay = cssTimeToMs(style.getPropertyValue('--rt-transition-show-delay'));
1154
+ if (globalTransitionShowDelay === null) {
1155
+ const style = getComputedStyle(document.body);
1156
+ globalTransitionShowDelay = cssTimeToMs(style.getPropertyValue('--rt-transition-show-delay'));
1157
+ }
1158
+ const transitionShowDelay = globalTransitionShowDelay;
993
1159
  missedTransitionTimerRef.current = setTimeout(() => {
994
1160
  /**
995
1161
  * if the tooltip switches from `show === true` to `show === false` too fast
@@ -1020,9 +1186,26 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
1020
1186
  if (!mounted.current) {
1021
1187
  return;
1022
1188
  }
1023
- setComputedPosition((oldComputedPosition) => deepEqual(oldComputedPosition, newComputedPosition)
1024
- ? oldComputedPosition
1025
- : newComputedPosition);
1189
+ setComputedPosition((oldComputedPosition) => {
1190
+ if (oldComputedPosition.place === newComputedPosition.place &&
1191
+ oldComputedPosition.tooltipStyles.left === newComputedPosition.tooltipStyles.left &&
1192
+ oldComputedPosition.tooltipStyles.top === newComputedPosition.tooltipStyles.top &&
1193
+ oldComputedPosition.tooltipStyles.border === newComputedPosition.tooltipStyles.border &&
1194
+ oldComputedPosition.tooltipArrowStyles.left ===
1195
+ newComputedPosition.tooltipArrowStyles.left &&
1196
+ oldComputedPosition.tooltipArrowStyles.top === newComputedPosition.tooltipArrowStyles.top &&
1197
+ oldComputedPosition.tooltipArrowStyles.right ===
1198
+ newComputedPosition.tooltipArrowStyles.right &&
1199
+ oldComputedPosition.tooltipArrowStyles.bottom ===
1200
+ newComputedPosition.tooltipArrowStyles.bottom &&
1201
+ oldComputedPosition.tooltipArrowStyles.borderBottom ===
1202
+ newComputedPosition.tooltipArrowStyles.borderBottom &&
1203
+ oldComputedPosition.tooltipArrowStyles.borderRight ===
1204
+ newComputedPosition.tooltipArrowStyles.borderRight) {
1205
+ return oldComputedPosition;
1206
+ }
1207
+ return newComputedPosition;
1208
+ });
1026
1209
  }, []);
1027
1210
  const handleShowTooltipDelayed = React.useCallback((delay = delayShow) => {
1028
1211
  if (tooltipShowDelayTimerRef.current) {
@@ -1050,24 +1233,20 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
1050
1233
  }, [delayHide, handleShow]);
1051
1234
  const handleTooltipPosition = React.useCallback(({ x, y }) => {
1052
1235
  var _a;
1053
- const virtualElement = {
1054
- getBoundingClientRect() {
1055
- return {
1056
- x,
1057
- y,
1058
- width: 0,
1059
- height: 0,
1060
- top: y,
1061
- left: x,
1062
- right: x,
1063
- bottom: y,
1064
- };
1065
- },
1066
- };
1236
+ virtualElementRef.current.getBoundingClientRect = () => ({
1237
+ x,
1238
+ y,
1239
+ width: 0,
1240
+ height: 0,
1241
+ top: y,
1242
+ left: x,
1243
+ right: x,
1244
+ bottom: y,
1245
+ });
1067
1246
  computeTooltipPosition({
1068
1247
  place: (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place) !== null && _a !== void 0 ? _a : place,
1069
1248
  offset,
1070
- elementReference: virtualElement,
1249
+ elementReference: virtualElementRef.current,
1071
1250
  tooltipReference: tooltipRef.current,
1072
1251
  tooltipArrowReference: tooltipArrowRef.current,
1073
1252
  strategy: positionStrategy,
@@ -1193,6 +1372,8 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
1193
1372
  tooltipShowDelayTimerRef,
1194
1373
  updateTooltipPosition,
1195
1374
  });
1375
+ const updateTooltipPositionRef = React.useRef(updateTooltipPosition);
1376
+ updateTooltipPositionRef.current = updateTooltipPosition;
1196
1377
  React.useEffect(() => {
1197
1378
  if (!rendered) {
1198
1379
  return;
@@ -1211,7 +1392,7 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
1211
1392
  }
1212
1393
  timeoutId = setTimeout(() => {
1213
1394
  if (mounted.current) {
1214
- updateTooltipPosition();
1395
+ updateTooltipPositionRef.current();
1215
1396
  }
1216
1397
  timeoutId = null;
1217
1398
  }, 0);
@@ -1223,7 +1404,7 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
1223
1404
  clearTimeout(timeoutId);
1224
1405
  }
1225
1406
  };
1226
- }, [content, contentWrapperRef, rendered, updateTooltipPosition]);
1407
+ }, [content, contentWrapperRef, rendered]);
1227
1408
  React.useEffect(() => {
1228
1409
  var _a;
1229
1410
  const shouldResolveInitialActiveAnchor = rendered || defaultIsOpen || Boolean(isOpen);
@@ -1284,7 +1465,20 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
1284
1465
  }, [delayShow, handleShowTooltipDelayed]);
1285
1466
  const actualContent = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.content) !== null && _a !== void 0 ? _a : content;
1286
1467
  const hasContent = actualContent !== null && actualContent !== undefined;
1287
- const canShow = show && Object.keys(computedPosition.tooltipStyles).length > 0;
1468
+ const canShow = show && computedPosition.tooltipStyles.left !== undefined;
1469
+ const tooltipStyle = React.useMemo(() => ({
1470
+ ...externalStyles,
1471
+ ...computedPosition.tooltipStyles,
1472
+ opacity: opacity !== undefined && canShow ? opacity : undefined,
1473
+ }), [externalStyles, computedPosition.tooltipStyles, opacity, canShow]);
1474
+ const arrowBackground = React.useMemo(() => arrowColor
1475
+ ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`
1476
+ : undefined, [arrowColor]);
1477
+ const arrowStyle = React.useMemo(() => ({
1478
+ ...computedPosition.tooltipArrowStyles,
1479
+ background: arrowBackground,
1480
+ '--rt-arrow-size': `${arrowSize}px`,
1481
+ }), [computedPosition.tooltipArrowStyles, arrowBackground, arrowSize]);
1288
1482
  React.useImperativeHandle(forwardRef, () => ({
1289
1483
  open: (options) => {
1290
1484
  let imperativeAnchor = null;
@@ -1339,19 +1533,9 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
1339
1533
  setRendered(false);
1340
1534
  setImperativeOptions(null);
1341
1535
  afterHide === null || afterHide === void 0 ? void 0 : afterHide();
1342
- }, style: {
1343
- ...externalStyles,
1344
- ...computedPosition.tooltipStyles,
1345
- opacity: opacity !== undefined && canShow ? opacity : undefined,
1346
- }, ref: tooltipRef },
1536
+ }, style: tooltipStyle, ref: tooltipRef },
1347
1537
  React.createElement(WrapperElement, { className: clsx('react-tooltip-content-wrapper', coreStyles['content'], styles['content']) }, actualContent),
1348
- React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: {
1349
- ...computedPosition.tooltipArrowStyles,
1350
- background: arrowColor
1351
- ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`
1352
- : undefined,
1353
- '--rt-arrow-size': `${arrowSize}px`,
1354
- }, ref: tooltipArrowRef }))) : null;
1538
+ React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: arrowStyle, ref: tooltipArrowRef }))) : null;
1355
1539
  if (!tooltipNode) {
1356
1540
  return null;
1357
1541
  }
@@ -1362,12 +1546,82 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousAc
1362
1546
  };
1363
1547
  var Tooltip$1 = React.memo(Tooltip);
1364
1548
 
1549
+ /**
1550
+ * Shared MutationObserver for data-tooltip-* attribute changes.
1551
+ * Instead of N observers (one per tooltip), a single observer watches
1552
+ * all active anchors and dispatches changes to registered callbacks.
1553
+ */
1554
+ const observedElements = new Map();
1555
+ let sharedObserver = null;
1556
+ const observerConfig = {
1557
+ attributes: true,
1558
+ childList: false,
1559
+ subtree: false,
1560
+ };
1561
+ function getObserver() {
1562
+ if (!sharedObserver) {
1563
+ sharedObserver = new MutationObserver((mutationList) => {
1564
+ var _a;
1565
+ for (const mutation of mutationList) {
1566
+ if (mutation.type !== 'attributes' ||
1567
+ !((_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.startsWith('data-tooltip-'))) {
1568
+ continue;
1569
+ }
1570
+ const target = mutation.target;
1571
+ const callbacks = observedElements.get(target);
1572
+ if (callbacks) {
1573
+ callbacks.forEach((cb) => cb(target));
1574
+ }
1575
+ }
1576
+ });
1577
+ }
1578
+ return sharedObserver;
1579
+ }
1580
+ function observeAnchorAttributes(element, callback) {
1581
+ const observer = getObserver();
1582
+ let callbacks = observedElements.get(element);
1583
+ if (!callbacks) {
1584
+ callbacks = new Set();
1585
+ observedElements.set(element, callbacks);
1586
+ observer.observe(element, observerConfig);
1587
+ }
1588
+ callbacks.add(callback);
1589
+ return () => {
1590
+ const cbs = observedElements.get(element);
1591
+ if (cbs) {
1592
+ cbs.delete(callback);
1593
+ if (cbs.size === 0) {
1594
+ observedElements.delete(element);
1595
+ // MutationObserver doesn't have unobserve — if no elements left, disconnect & reset
1596
+ if (observedElements.size === 0) {
1597
+ observer.disconnect();
1598
+ }
1599
+ else {
1600
+ // Re-observe remaining elements (MutationObserver has no per-target unobserve)
1601
+ observer.disconnect();
1602
+ observedElements.forEach((_cbs, el) => {
1603
+ observer.observe(el, observerConfig);
1604
+ });
1605
+ }
1606
+ }
1607
+ }
1608
+ };
1609
+ }
1610
+
1365
1611
  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) => {
1366
1612
  var _a, _b, _c, _d, _e, _f, _g, _h;
1367
1613
  const [activeAnchor, setActiveAnchor] = React.useState(null);
1368
1614
  const [anchorDataAttributes, setAnchorDataAttributes] = React.useState({});
1369
1615
  const previousActiveAnchorRef = React.useRef(null);
1370
1616
  const styleInjectionRef = React.useRef(disableStyleInjection);
1617
+ const handleSetActiveAnchor = React.useCallback((anchor) => {
1618
+ setActiveAnchor((prev) => {
1619
+ if (!(anchor === null || anchor === void 0 ? void 0 : anchor.isSameNode(prev))) {
1620
+ previousActiveAnchorRef.current = prev;
1621
+ }
1622
+ return anchor;
1623
+ });
1624
+ }, []);
1371
1625
  /* c8 ignore start */
1372
1626
  const getDataAttributesFromAnchorElement = (elementReference) => {
1373
1627
  const dataAttributes = elementReference === null || elementReference === void 0 ? void 0 : elementReference.getAttributeNames().reduce((acc, name) => {
@@ -1399,37 +1653,24 @@ const TooltipController = React.forwardRef(({ id, anchorSelect, content, render,
1399
1653
  // eslint-disable-next-line react-hooks/exhaustive-deps
1400
1654
  }, []);
1401
1655
  React.useEffect(() => {
1402
- const observerCallback = (mutationList) => {
1403
- mutationList.forEach((mutation) => {
1404
- var _a;
1405
- if (!activeAnchor ||
1406
- mutation.type !== 'attributes' ||
1407
- !((_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.startsWith('data-tooltip-'))) {
1408
- return;
1656
+ if (!activeAnchor) {
1657
+ setAnchorDataAttributes({});
1658
+ return () => { };
1659
+ }
1660
+ const updateAttributes = (element) => {
1661
+ const attrs = getDataAttributesFromAnchorElement(element);
1662
+ setAnchorDataAttributes((prev) => {
1663
+ const keys = Object.keys(attrs);
1664
+ const prevKeys = Object.keys(prev);
1665
+ if (keys.length === prevKeys.length && keys.every((key) => attrs[key] === prev[key])) {
1666
+ return prev;
1409
1667
  }
1410
- // make sure to get all set attributes, since all unset attributes are reset
1411
- const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor);
1412
- setAnchorDataAttributes(dataAttributes);
1668
+ return attrs;
1413
1669
  });
1414
1670
  };
1415
- // Create an observer instance linked to the callback function
1416
- const observer = new MutationObserver(observerCallback);
1417
- // do not check for subtree and childrens, we only want to know attribute changes
1418
- // to stay watching `data-attributes-*` from anchor element
1419
- const observerConfig = { attributes: true, childList: false, subtree: false };
1420
- if (activeAnchor) {
1421
- const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor);
1422
- setAnchorDataAttributes(dataAttributes);
1423
- // Start observing the target node for configured mutations
1424
- observer.observe(activeAnchor, observerConfig);
1425
- }
1426
- else {
1427
- setAnchorDataAttributes({});
1428
- }
1429
- return () => {
1430
- // Remove the observer when the tooltip is destroyed
1431
- observer.disconnect();
1432
- };
1671
+ updateAttributes(activeAnchor);
1672
+ const unsubscribe = observeAnchorAttributes(activeAnchor, updateAttributes);
1673
+ return unsubscribe;
1433
1674
  }, [activeAnchor, anchorSelect]);
1434
1675
  React.useEffect(() => {
1435
1676
  /* c8 ignore start */
@@ -1510,14 +1751,7 @@ const TooltipController = React.forwardRef(({ id, anchorSelect, content, render,
1510
1751
  disableTooltip,
1511
1752
  activeAnchor,
1512
1753
  previousActiveAnchor: previousActiveAnchorRef.current,
1513
- setActiveAnchor: (anchor) => {
1514
- setActiveAnchor((prev) => {
1515
- if (!(anchor === null || anchor === void 0 ? void 0 : anchor.isSameNode(prev))) {
1516
- previousActiveAnchorRef.current = prev;
1517
- }
1518
- return anchor;
1519
- });
1520
- },
1754
+ setActiveAnchor: handleSetActiveAnchor,
1521
1755
  role,
1522
1756
  };
1523
1757
  return React.createElement(Tooltip$1, { ...props });
@@ -1544,7 +1778,6 @@ const TooltipCoreStyles = `:root {
1544
1778
  left: 0;
1545
1779
  pointer-events: none;
1546
1780
  opacity: 0;
1547
- will-change: opacity;
1548
1781
  }
1549
1782
 
1550
1783
  .core-styles-module_fixed__pcSol {
@@ -1575,11 +1808,13 @@ const TooltipCoreStyles = `:root {
1575
1808
  .core-styles-module_show__Nt9eE {
1576
1809
  opacity: var(--rt-opacity);
1577
1810
  transition: opacity var(--rt-transition-show-delay) ease-out;
1811
+ will-change: opacity;
1578
1812
  }
1579
1813
 
1580
1814
  .core-styles-module_closing__sGnxF {
1581
1815
  opacity: 0;
1582
1816
  transition: opacity var(--rt-transition-closing-delay) ease-in;
1817
+ will-change: opacity;
1583
1818
  }
1584
1819
 
1585
1820
  `;