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

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