react-tooltip 6.0.0-beta.1179.rc.2 → 6.0.0-beta.1179.rc.21

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.
@@ -5,9 +5,10 @@
5
5
  * @copyright ReactTooltip Team
6
6
  * @license MIT
7
7
  */
8
- import React, { useLayoutEffect, useEffect, useRef, useState, useCallback, useImperativeHandle } from 'react';
9
- import { arrow, computePosition, offset, flip, shift, autoUpdate } from '@floating-ui/dom';
8
+ import React, { useLayoutEffect, useEffect, useState, useRef, useMemo, memo, useCallback, useImperativeHandle } from 'react';
10
9
  import clsx from 'clsx';
10
+ import { createPortal } from 'react-dom';
11
+ import { flip, shift, arrow, computePosition, offset, autoUpdate } from '@floating-ui/dom';
11
12
 
12
13
  // This is the ID for the core styles of ReactTooltip
13
14
  const REACT_TOOLTIP_CORE_STYLES_ID = 'react-tooltip-core-styles';
@@ -17,27 +18,33 @@ const injected = {
17
18
  core: false,
18
19
  base: false,
19
20
  };
20
- function injectStyle({ css, id = REACT_TOOLTIP_BASE_STYLES_ID, type = 'base', ref, }) {
21
- var _a, _b;
22
- if (!css || typeof document === 'undefined' || injected[type]) {
21
+ /**
22
+ * Note about `state` parameter:
23
+ * This parameter is used to keep track of the state of the styles
24
+ * into the tests since the const `injected` is not acessible or resettable in the tests
25
+ */
26
+ function injectStyle({ css, id = REACT_TOOLTIP_BASE_STYLES_ID, type = 'base', ref, state = {}, }) {
27
+ if (!css ||
28
+ typeof document === 'undefined' ||
29
+ (typeof state[type] !== 'undefined' ? state[type] : injected[type])) {
23
30
  return;
24
31
  }
25
32
  if (type === 'core' &&
26
33
  typeof process !== 'undefined' && // this validation prevents docs from breaking even with `process?`
27
- ((_a = process === null || process === void 0 ? void 0 : process.env) === null || _a === void 0 ? void 0 : _a.REACT_TOOLTIP_DISABLE_CORE_STYLES)) {
34
+ process.env &&
35
+ process.env.REACT_TOOLTIP_DISABLE_CORE_STYLES) {
28
36
  return;
29
37
  }
30
- if (type !== 'base' &&
38
+ if (type === 'base' &&
31
39
  typeof process !== 'undefined' && // this validation prevents docs from breaking even with `process?`
32
- ((_b = process === null || process === void 0 ? void 0 : process.env) === null || _b === void 0 ? void 0 : _b.REACT_TOOLTIP_DISABLE_BASE_STYLES)) {
40
+ process.env &&
41
+ process.env.REACT_TOOLTIP_DISABLE_BASE_STYLES) {
33
42
  return;
34
43
  }
35
44
  if (type === 'core') {
36
- // eslint-disable-next-line no-param-reassign
37
45
  id = REACT_TOOLTIP_CORE_STYLES_ID;
38
46
  }
39
47
  if (!ref) {
40
- // eslint-disable-next-line no-param-reassign
41
48
  ref = {};
42
49
  }
43
50
  const { insertAt } = ref;
@@ -67,26 +74,27 @@ function injectStyle({ css, id = REACT_TOOLTIP_BASE_STYLES_ID, type = 'base', re
67
74
  else {
68
75
  style.appendChild(document.createTextNode(css));
69
76
  }
70
- injected[type] = true;
77
+ if (typeof state[type] !== 'undefined') {
78
+ state[type] = true;
79
+ }
80
+ else {
81
+ injected[type] = true; // internal global state that jest doesn't have access
82
+ }
71
83
  }
72
84
 
73
- const computeTooltipPosition = async ({ elementReference = null, tooltipReference = null, tooltipArrowReference = null, place = 'top', offset: offsetValue = 10, strategy = 'absolute', middlewares = [
74
- offset(Number(offsetValue)),
75
- flip({
76
- fallbackAxisSideDirection: 'start',
77
- }),
78
- shift({ padding: 5 }),
79
- ], border, }) => {
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, }) => {
80
89
  if (!elementReference) {
81
90
  // elementReference can be null or undefined and we will not compute the position
82
- // eslint-disable-next-line no-console
83
91
  // console.error('The reference element for tooltip was not defined: ', elementReference)
84
92
  return { tooltipStyles: {}, tooltipArrowStyles: {}, place };
85
93
  }
86
94
  if (tooltipReference === null) {
87
95
  return { tooltipStyles: {}, tooltipArrowStyles: {}, place };
88
96
  }
89
- const middleware = middlewares;
97
+ const middleware = [...middlewares];
90
98
  if (tooltipArrowReference) {
91
99
  middleware.push(arrow({ element: tooltipArrowReference, padding: 5 }));
92
100
  return computePosition(elementReference, tooltipReference, {
@@ -130,7 +138,7 @@ const computeTooltipPosition = async ({ elementReference = null, tooltipReferenc
130
138
  right: '',
131
139
  bottom: '',
132
140
  ...borderSide,
133
- [staticSide]: `-${4 + borderWidth}px`,
141
+ [staticSide]: `-${arrowSize / 2 + borderWidth - 1}px`,
134
142
  };
135
143
  /* c8 ignore end */
136
144
  return { tooltipStyles: styles, tooltipArrowStyles: arrowStyle, place: placement };
@@ -164,6 +172,7 @@ const cssTimeToMs = (time) => {
164
172
  */
165
173
  const debounce = (func, wait, immediate) => {
166
174
  let timeout = null;
175
+ let currentFunc = func;
167
176
  const debounced = function debounced(...args) {
168
177
  const later = () => {
169
178
  timeout = null;
@@ -173,7 +182,7 @@ const debounce = (func, wait, immediate) => {
173
182
  * there's no need to clear the timeout
174
183
  * since we expect it to resolve and set `timeout = null`
175
184
  */
176
- func.apply(this, args);
185
+ currentFunc.apply(this, args);
177
186
  timeout = setTimeout(later, wait);
178
187
  }
179
188
  };
@@ -186,36 +195,12 @@ const debounce = (func, wait, immediate) => {
186
195
  clearTimeout(timeout);
187
196
  timeout = null;
188
197
  };
198
+ debounced.setCallback = (newFunc) => {
199
+ currentFunc = newFunc;
200
+ };
189
201
  return debounced;
190
202
  };
191
203
 
192
- const isObject = (object) => {
193
- return object !== null && !Array.isArray(object) && typeof object === 'object';
194
- };
195
- const deepEqual = (object1, object2) => {
196
- if (object1 === object2) {
197
- return true;
198
- }
199
- if (Array.isArray(object1) && Array.isArray(object2)) {
200
- if (object1.length !== object2.length) {
201
- return false;
202
- }
203
- return object1.every((val, index) => deepEqual(val, object2[index]));
204
- }
205
- if (Array.isArray(object1) !== Array.isArray(object2)) {
206
- return false;
207
- }
208
- if (!isObject(object1) || !isObject(object2)) {
209
- return object1 === object2;
210
- }
211
- const keys1 = Object.keys(object1);
212
- const keys2 = Object.keys(object2);
213
- if (keys1.length !== keys2.length) {
214
- return false;
215
- }
216
- return keys1.every((key) => deepEqual(object1[key], object2[key]));
217
- };
218
-
219
204
  const isScrollable = (node) => {
220
205
  if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
221
206
  return false;
@@ -240,336 +225,390 @@ const getScrollParent = (node) => {
240
225
  return document.scrollingElement || document.documentElement;
241
226
  };
242
227
 
243
- const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
228
+ // React currently throws a warning when using useLayoutEffect on the server.
229
+ // To get around it, we can conditionally useEffect on the server (no-op) and
230
+ // useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
231
+ // subscription callback always has the selector from the latest render commit
232
+ // available, otherwise a store update may happen between render and the effect,
233
+ // which may cause missed updates; we also must ensure the store subscription
234
+ // is created synchronously, otherwise a store update may occur before the
235
+ // subscription is created and an inconsistent state may be observed
236
+ const isHopefullyDomEnvironment = typeof window !== 'undefined' &&
237
+ typeof window.document !== 'undefined' &&
238
+ typeof window.document.createElement !== 'undefined';
239
+ const useIsomorphicLayoutEffect = isHopefullyDomEnvironment ? useLayoutEffect : useEffect;
244
240
 
245
241
  const clearTimeoutRef = (ref) => {
246
242
  if (ref.current) {
247
243
  clearTimeout(ref.current);
248
- // eslint-disable-next-line no-param-reassign
249
244
  ref.current = null;
250
245
  }
251
246
  };
252
247
 
253
- var coreStyles = {"tooltip":"core-styles-module_tooltip__3vRRp","fixed":"core-styles-module_fixed__pcSol","arrow":"core-styles-module_arrow__cvMwQ","noArrow":"core-styles-module_noArrow__xock6","clickable":"core-styles-module_clickable__ZuTTB","show":"core-styles-module_show__Nt9eE","closing":"core-styles-module_closing__sGnxF"};
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
+ }
254
255
 
255
- var styles = {"tooltip":"styles-module_tooltip__mnnfp","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"};
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
+ }
256
266
 
257
- const Tooltip = ({
258
- // props
259
- forwardRef, id, className, classNameArrow, variant = 'dark', anchorSelect, place = 'top', offset = 10, openOnClick = false, positionStrategy = 'absolute', middlewares, wrapper: WrapperElement, delayShow = 0, delayHide = 0, float = false, hidden = false, noArrow = false, clickable = false, openEvents, closeEvents, globalCloseEvents, imperativeModeOnly, style: externalStyles, position, afterShow, afterHide,
260
- // props handled by controller
261
- content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnchor, setActiveAnchor, border, opacity, arrowColor, role = 'tooltip', }) => {
262
- var _a;
263
- const tooltipRef = useRef(null);
264
- const tooltipArrowRef = useRef(null);
265
- const tooltipShowDelayTimerRef = useRef(null);
266
- const tooltipHideDelayTimerRef = useRef(null);
267
- const missedTransitionTimerRef = useRef(null);
268
- const [computedPosition, setComputedPosition] = useState({
269
- tooltipStyles: {},
270
- tooltipArrowStyles: {},
271
- place,
272
- });
273
- const [show, setShow] = useState(false);
274
- const [rendered, setRendered] = useState(false);
275
- const [imperativeOptions, setImperativeOptions] = useState(null);
276
- const wasShowing = useRef(false);
277
- const lastFloatPosition = useRef(null);
278
- const hoveringTooltip = useRef(false);
279
- const [anchorElements, setAnchorElements] = useState([]);
280
- const mounted = useRef(false);
281
- /**
282
- * useLayoutEffect runs before useEffect,
283
- * but should be used carefully because of caveats
284
- * https://beta.reactjs.org/reference/react/useLayoutEffect#caveats
285
- */
286
- useIsomorphicLayoutEffect(() => {
287
- mounted.current = true;
288
- return () => {
289
- mounted.current = false;
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"};
268
+
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"};
270
+
271
+ const registry = new Map();
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
+ }
281
+ function areAnchorListsEqual(left, right) {
282
+ if (left.length !== right.length) {
283
+ return false;
284
+ }
285
+ return left.every((anchor, index) => anchor === right[index]);
286
+ }
287
+ function readAnchorsForSelector(selector) {
288
+ try {
289
+ return {
290
+ anchors: Array.from(document.querySelectorAll(selector)),
291
+ error: null,
290
292
  };
291
- }, []);
292
- const handleShow = useCallback((value) => {
293
- if (!mounted.current) {
294
- return;
293
+ }
294
+ catch (error) {
295
+ return {
296
+ anchors: [],
297
+ error: error instanceof Error ? error : new Error(String(error)),
298
+ };
299
+ }
300
+ }
301
+ function notifySubscribers(entry) {
302
+ entry.subscribers.forEach((subscriber) => subscriber(entry.anchors, entry.error));
303
+ }
304
+ function refreshEntry(selector, entry) {
305
+ var _a, _b, _c, _d;
306
+ const nextState = readAnchorsForSelector(selector);
307
+ const nextErrorMessage = (_b = (_a = nextState.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : null;
308
+ const previousErrorMessage = (_d = (_c = entry.error) === null || _c === void 0 ? void 0 : _c.message) !== null && _d !== void 0 ? _d : null;
309
+ if (areAnchorListsEqual(entry.anchors, nextState.anchors) &&
310
+ nextErrorMessage === previousErrorMessage) {
311
+ return;
312
+ }
313
+ const nextEntry = {
314
+ ...entry,
315
+ anchors: nextState.anchors,
316
+ error: nextState.error,
317
+ };
318
+ registry.set(selector, nextEntry);
319
+ notifySubscribers(nextEntry);
320
+ }
321
+ function refreshAllEntries() {
322
+ registry.forEach((entry, selector) => {
323
+ refreshEntry(selector, entry);
324
+ });
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();
295
333
  }
296
- if (value) {
297
- setRendered(true);
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();
298
351
  }
299
- /**
300
- * wait for the component to render and calculate position
301
- * before actually showing
302
- */
303
- setTimeout(() => {
304
- if (!mounted.current) {
305
- return;
306
- }
307
- setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(value);
308
- if (isOpen === undefined) {
309
- setShow(value);
310
- }
311
- }, 10);
312
- }, [isOpen, setIsOpen]);
313
- /**
314
- * this replicates the effect from `handleShow()`
315
- * when `isOpen` is changed from outside
316
- */
317
- useEffect(() => {
318
- if (isOpen === undefined) {
319
- return () => null;
352
+ else if (ids && ids.size > 0) {
353
+ refreshEntriesForTooltipIds(ids);
320
354
  }
321
- if (isOpen) {
322
- setRendered(true);
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);
323
371
  }
324
- const timeout = setTimeout(() => {
325
- setShow(isOpen);
326
- }, 10);
327
- return () => {
328
- clearTimeout(timeout);
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
+ }
430
+ function ensureDocumentObserver() {
431
+ if (documentObserver || typeof MutationObserver === 'undefined') {
432
+ return;
433
+ }
434
+ documentObserver = new MutationObserver((records) => {
435
+ const affectedIds = collectAffectedTooltipIds(records);
436
+ scheduleRefresh(affectedIds);
437
+ });
438
+ documentObserver.observe(document.body, {
439
+ childList: true,
440
+ subtree: true,
441
+ attributes: true,
442
+ attributeFilter: ['data-tooltip-id'],
443
+ attributeOldValue: true,
444
+ });
445
+ }
446
+ function cleanupDocumentObserverIfUnused() {
447
+ if (registry.size !== 0 || !documentObserver) {
448
+ return;
449
+ }
450
+ documentObserver.disconnect();
451
+ documentObserver = null;
452
+ }
453
+ function subscribeAnchorSelector(selector, subscriber) {
454
+ let entry = registry.get(selector);
455
+ if (!entry) {
456
+ const initialState = readAnchorsForSelector(selector);
457
+ entry = {
458
+ anchors: initialState.anchors,
459
+ error: initialState.error,
460
+ subscribers: new Set(),
461
+ tooltipId: extractTooltipId(selector),
329
462
  };
330
- }, [isOpen]);
331
- useEffect(() => {
332
- if (show === wasShowing.current) {
463
+ registry.set(selector, entry);
464
+ }
465
+ entry.subscribers.add(subscriber);
466
+ ensureDocumentObserver();
467
+ subscriber([...entry.anchors], entry.error);
468
+ return () => {
469
+ const currentEntry = registry.get(selector);
470
+ if (!currentEntry) {
333
471
  return;
334
472
  }
335
- clearTimeoutRef(missedTransitionTimerRef);
336
- wasShowing.current = show;
337
- if (show) {
338
- afterShow === null || afterShow === void 0 ? void 0 : afterShow();
339
- }
340
- else {
341
- /**
342
- * see `onTransitionEnd` on tooltip wrapper
343
- */
344
- const style = getComputedStyle(document.body);
345
- const transitionShowDelay = cssTimeToMs(style.getPropertyValue('--rt-transition-show-delay'));
346
- missedTransitionTimerRef.current = setTimeout(() => {
347
- /**
348
- * if the tooltip switches from `show === true` to `show === false` too fast
349
- * the transition never runs, so `onTransitionEnd` callback never gets fired
350
- */
351
- setRendered(false);
352
- setImperativeOptions(null);
353
- afterHide === null || afterHide === void 0 ? void 0 : afterHide();
354
- // +25ms just to make sure `onTransitionEnd` (if it gets fired) has time to run
355
- }, transitionShowDelay + 25);
473
+ currentEntry.subscribers.delete(subscriber);
474
+ if (currentEntry.subscribers.size === 0) {
475
+ registry.delete(selector);
356
476
  }
357
- }, [afterHide, afterShow, show]);
358
- const handleComputedPosition = (newComputedPosition) => {
359
- setComputedPosition((oldComputedPosition) => deepEqual(oldComputedPosition, newComputedPosition)
360
- ? oldComputedPosition
361
- : newComputedPosition);
477
+ cleanupDocumentObserverIfUnused();
362
478
  };
363
- const handleShowTooltipDelayed = useCallback((delay = delayShow) => {
364
- if (tooltipShowDelayTimerRef.current) {
365
- clearTimeout(tooltipShowDelayTimerRef.current);
479
+ }
480
+
481
+ const getAnchorSelector = ({ id, anchorSelect, imperativeAnchorSelect, }) => {
482
+ var _a;
483
+ let selector = (_a = imperativeAnchorSelect !== null && imperativeAnchorSelect !== void 0 ? imperativeAnchorSelect : anchorSelect) !== null && _a !== void 0 ? _a : '';
484
+ if (!selector && id) {
485
+ selector = `[data-tooltip-id='${id.replace(/'/g, "\\'")}']`;
486
+ }
487
+ return selector;
488
+ };
489
+ const useTooltipAnchors = ({ id, anchorSelect, imperativeAnchorSelect, activeAnchor, disableTooltip, onActiveAnchorRemoved, trackAnchors, }) => {
490
+ const [rawAnchorElements, setRawAnchorElements] = useState([]);
491
+ const [selectorError, setSelectorError] = useState(null);
492
+ const warnedSelectorRef = useRef(null);
493
+ const selector = useMemo(() => getAnchorSelector({ id, anchorSelect, imperativeAnchorSelect }), [id, anchorSelect, imperativeAnchorSelect]);
494
+ const anchorElements = useMemo(() => rawAnchorElements.filter((anchor) => !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(anchor))), [rawAnchorElements, disableTooltip]);
495
+ const activeAnchorMatchesSelector = useMemo(() => {
496
+ if (!activeAnchor || !selector) {
497
+ return false;
366
498
  }
367
- if (rendered) {
368
- // if the tooltip is already rendered, ignore delay
369
- handleShow(true);
370
- return;
499
+ try {
500
+ return activeAnchor.matches(selector);
371
501
  }
372
- tooltipShowDelayTimerRef.current = setTimeout(() => {
373
- handleShow(true);
374
- }, delay);
375
- }, [delayShow, handleShow, rendered]);
376
- const handleHideTooltipDelayed = useCallback((delay = delayHide) => {
377
- if (tooltipHideDelayTimerRef.current) {
378
- clearTimeout(tooltipHideDelayTimerRef.current);
502
+ catch (_a) {
503
+ return false;
379
504
  }
380
- tooltipHideDelayTimerRef.current = setTimeout(() => {
381
- if (hoveringTooltip.current) {
382
- return;
383
- }
384
- handleShow(false);
385
- }, delay);
386
- }, [delayHide, handleShow]);
387
- const handleTooltipPosition = useCallback(({ x, y }) => {
388
- var _a;
389
- const virtualElement = {
390
- getBoundingClientRect() {
391
- return {
392
- x,
393
- y,
394
- width: 0,
395
- height: 0,
396
- top: y,
397
- left: x,
398
- right: x,
399
- bottom: y,
400
- };
401
- },
402
- };
403
- computeTooltipPosition({
404
- place: (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place) !== null && _a !== void 0 ? _a : place,
405
- offset,
406
- elementReference: virtualElement,
407
- tooltipReference: tooltipRef.current,
408
- tooltipArrowReference: tooltipArrowRef.current,
409
- strategy: positionStrategy,
410
- middlewares,
411
- border,
412
- }).then((computedStylesData) => {
413
- handleComputedPosition(computedStylesData);
505
+ // eslint-disable-next-line react-hooks/exhaustive-deps
506
+ }, [activeAnchor, selector, anchorElements]);
507
+ useEffect(() => {
508
+ if (!selector || !trackAnchors) {
509
+ setRawAnchorElements([]);
510
+ setSelectorError(null);
511
+ return undefined;
512
+ }
513
+ return subscribeAnchorSelector(selector, (anchors, error) => {
514
+ setRawAnchorElements(anchors);
515
+ setSelectorError(error);
414
516
  });
415
- }, [imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place, place, offset, positionStrategy, middlewares, border]);
416
- const updateTooltipPosition = useCallback(() => {
417
- var _a, _b;
418
- const actualPosition = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.position) !== null && _a !== void 0 ? _a : position;
419
- if (actualPosition) {
420
- // if `position` is set, override regular and `float` positioning
421
- handleTooltipPosition(actualPosition);
517
+ }, [selector, trackAnchors]);
518
+ useEffect(() => {
519
+ if (!selectorError || warnedSelectorRef.current === selector) {
422
520
  return;
423
521
  }
424
- if (float) {
425
- if (lastFloatPosition.current) {
426
- /*
427
- Without this, changes to `content`, `place`, `offset`, ..., will only
428
- trigger a position calculation after a `mousemove` event.
429
-
430
- To see why this matters, comment this line, run `yarn dev` and click the
431
- "Hover me!" anchor.
432
- */
433
- handleTooltipPosition(lastFloatPosition.current);
434
- }
435
- // if `float` is set, override regular positioning
522
+ warnedSelectorRef.current = selector;
523
+ /* c8 ignore end */
524
+ }, [selector, selectorError]);
525
+ useEffect(() => {
526
+ if (!activeAnchor) {
436
527
  return;
437
528
  }
438
- if (!(activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.isConnected)) {
529
+ if (!activeAnchor.isConnected) {
530
+ onActiveAnchorRemoved();
439
531
  return;
440
532
  }
441
- computeTooltipPosition({
442
- place: (_b = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place) !== null && _b !== void 0 ? _b : place,
443
- offset,
444
- elementReference: activeAnchor,
445
- tooltipReference: tooltipRef.current,
446
- tooltipArrowReference: tooltipArrowRef.current,
447
- strategy: positionStrategy,
448
- middlewares,
449
- border,
450
- }).then((computedStylesData) => {
451
- if (!mounted.current) {
452
- // invalidate computed positions after remount
453
- return;
454
- }
455
- handleComputedPosition(computedStylesData);
456
- });
457
- }, [
458
- imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.position,
459
- imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place,
460
- position,
461
- float,
462
- activeAnchor,
463
- place,
464
- offset,
465
- positionStrategy,
466
- middlewares,
467
- border,
468
- handleTooltipPosition,
469
- ]);
470
- useEffect(() => {
471
- /**
472
- * TODO(V6): break this effect down into callbacks for clarity
473
- * - `handleKeyboardEvents()`
474
- * - `handleMouseEvents()`
475
- * - `handleGlobalCloseEvents()`
476
- * - `handleAnchorEvents()`
477
- * - ...
478
- */
479
- const handlePointerMove = (event) => {
480
- if (!event) {
481
- return;
482
- }
483
- const mouseEvent = event;
484
- const mousePosition = {
485
- x: mouseEvent.clientX,
486
- y: mouseEvent.clientY,
487
- };
488
- handleTooltipPosition(mousePosition);
489
- lastFloatPosition.current = mousePosition;
490
- };
491
- const handleClickOutsideAnchors = (event) => {
492
- var _a;
493
- if (!show) {
494
- return;
495
- }
496
- const target = event.target;
497
- if (!target.isConnected) {
498
- return;
499
- }
500
- if ((_a = tooltipRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) {
501
- return;
502
- }
503
- if (anchorElements.some((anchor) => anchor === null || anchor === void 0 ? void 0 : anchor.contains(target))) {
504
- return;
505
- }
506
- handleShow(false);
507
- if (tooltipShowDelayTimerRef.current) {
508
- clearTimeout(tooltipShowDelayTimerRef.current);
509
- }
510
- };
511
- const handleShowTooltip = (event) => {
512
- var _a;
513
- if (!event) {
514
- return;
515
- }
516
- const target = ((_a = event.currentTarget) !== null && _a !== void 0 ? _a : event.target);
517
- if (!(target === null || target === void 0 ? void 0 : target.isConnected)) {
518
- /**
519
- * this happens when the target is removed from the DOM
520
- * at the same time the tooltip gets triggered
521
- */
522
- setActiveAnchor(null);
523
- return;
524
- }
525
- if (delayShow) {
526
- handleShowTooltipDelayed();
527
- }
528
- else {
529
- handleShow(true);
530
- }
531
- setActiveAnchor(target);
532
- if (tooltipHideDelayTimerRef.current) {
533
- clearTimeout(tooltipHideDelayTimerRef.current);
534
- }
535
- };
536
- const handleHideTooltip = () => {
537
- if (clickable) {
538
- // allow time for the mouse to reach the tooltip, in case there's a gap
539
- handleHideTooltipDelayed(delayHide || 100);
540
- }
541
- else if (delayHide) {
542
- handleHideTooltipDelayed();
543
- }
544
- else {
545
- handleShow(false);
546
- }
547
- if (tooltipShowDelayTimerRef.current) {
548
- clearTimeout(tooltipShowDelayTimerRef.current);
549
- }
550
- };
551
- // debounce handler to prevent call twice when
552
- // mouse enter and focus events being triggered toggether
553
- const internalDebouncedHandleShowTooltip = debounce(handleShowTooltip, 50);
554
- const internalDebouncedHandleHideTooltip = debounce(handleHideTooltip, 50);
555
- // If either of the functions is called while the other is still debounced,
556
- // reset the timeout. Otherwise if there is a sub-50ms (leave A, enter B, leave B)
557
- // sequence of events, the tooltip will stay open because the hide debounce
558
- // from leave A prevented the leave B event from calling it, leaving the
559
- // tooltip visible.
560
- const debouncedHandleShowTooltip = (e) => {
561
- internalDebouncedHandleHideTooltip.cancel();
562
- internalDebouncedHandleShowTooltip(e);
563
- };
564
- const debouncedHandleHideTooltip = () => {
565
- internalDebouncedHandleShowTooltip.cancel();
566
- internalDebouncedHandleHideTooltip();
567
- };
568
- const handleScrollResize = () => {
569
- handleShow(false);
570
- };
571
- 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);
572
- const actualOpenEvents = openEvents
533
+ if (!anchorElements.includes(activeAnchor) && !activeAnchorMatchesSelector) {
534
+ onActiveAnchorRemoved();
535
+ }
536
+ }, [activeAnchor, anchorElements, activeAnchorMatchesSelector, onActiveAnchorRemoved]);
537
+ return {
538
+ anchorElements,
539
+ selector,
540
+ };
541
+ };
542
+
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, 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
573
612
  ? { ...openEvents }
574
613
  : {
575
614
  mouseenter: true,
@@ -579,13 +618,25 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
579
618
  mousedown: false,
580
619
  };
581
620
  if (!openEvents && openOnClick) {
582
- Object.assign(actualOpenEvents, {
621
+ Object.assign(events, {
583
622
  mouseenter: false,
584
623
  focus: false,
585
624
  click: true,
586
625
  });
587
626
  }
588
- 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
589
640
  ? { ...closeEvents }
590
641
  : {
591
642
  mouseleave: true,
@@ -595,12 +646,24 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
595
646
  mouseup: false,
596
647
  };
597
648
  if (!closeEvents && openOnClick) {
598
- Object.assign(actualCloseEvents, {
649
+ Object.assign(events, {
650
+ mouseleave: false,
651
+ blur: false,
652
+ });
653
+ }
654
+ if (imperativeModeOnly) {
655
+ Object.assign(events, {
599
656
  mouseleave: false,
600
657
  blur: false,
658
+ click: false,
659
+ dblclick: false,
660
+ mouseup: false,
601
661
  });
602
662
  }
603
- const actualGlobalCloseEvents = globalCloseEvents
663
+ return events;
664
+ }, [closeEvents, openOnClick, imperativeModeOnly]);
665
+ const actualGlobalCloseEvents = useMemo(() => {
666
+ const events = globalCloseEvents
604
667
  ? { ...globalCloseEvents }
605
668
  : {
606
669
  escape: false,
@@ -609,30 +672,276 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
609
672
  clickOutsideAnchor: hasClickEvent || false,
610
673
  };
611
674
  if (imperativeModeOnly) {
612
- Object.assign(actualOpenEvents, {
613
- mouseenter: false,
614
- focus: false,
615
- click: false,
616
- dblclick: false,
617
- mousedown: false,
618
- });
619
- Object.assign(actualCloseEvents, {
620
- mouseleave: false,
621
- blur: false,
622
- click: false,
623
- dblclick: false,
624
- mouseup: false,
625
- });
626
- Object.assign(actualGlobalCloseEvents, {
675
+ Object.assign(events, {
627
676
  escape: false,
628
677
  scroll: false,
629
678
  resize: false,
630
679
  clickOutsideAnchor: false,
631
680
  });
632
681
  }
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;
707
+ }
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
+ }
713
+ }
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
+ }
726
+ }
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);
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 {
773
+ handleShow(false);
774
+ }
775
+ if (tooltipShowDelayTimerRef.current) {
776
+ clearTimeout(tooltipShowDelayTimerRef.current);
777
+ }
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));
792
+ };
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();
801
+ };
802
+ const addDelegatedHoverOpenListener = () => {
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);
813
+ });
814
+ };
815
+ const addDelegatedHoverCloseListener = () => {
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();
826
+ });
827
+ };
828
+ if (actualOpenEvents.mouseenter) {
829
+ addDelegatedHoverOpenListener();
830
+ }
831
+ if (actualCloseEvents.mouseleave) {
832
+ addDelegatedHoverCloseListener();
833
+ }
834
+ if (actualOpenEvents.mouseover) {
835
+ addDelegatedHoverOpenListener();
836
+ }
837
+ if (actualCloseEvents.mouseout) {
838
+ addDelegatedHoverCloseListener();
839
+ }
840
+ if (actualOpenEvents.focus) {
841
+ addDelegatedListener('focusin', (event) => {
842
+ debouncedHandleShowTooltip(resolveAnchorElementRef.current(event.target));
843
+ });
844
+ }
845
+ if (actualCloseEvents.blur) {
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();
856
+ });
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
+ };
877
+ Object.entries(actualOpenEvents).forEach(([event, enabled]) => {
878
+ if (!enabled || regularEvents.includes(event)) {
879
+ return;
880
+ }
881
+ if (clickEvents.includes(event)) {
882
+ addDelegatedListener(event, handleClickOpenTooltipAnchor);
883
+ }
884
+ });
885
+ Object.entries(actualCloseEvents).forEach(([event, enabled]) => {
886
+ if (!enabled || regularEvents.includes(event)) {
887
+ return;
888
+ }
889
+ if (clickEvents.includes(event)) {
890
+ addDelegatedListener(event, handleClickCloseTooltipAnchor);
891
+ }
892
+ });
893
+ if (float) {
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;
910
+ });
911
+ }
633
912
  const tooltipElement = tooltipRef.current;
634
- const tooltipScrollParent = getScrollParent(tooltipRef.current);
635
- const anchorScrollParent = getScrollParent(activeAnchor);
913
+ const handleMouseOverTooltip = () => {
914
+ hoveringTooltip.current = true;
915
+ };
916
+ const handleMouseOutTooltip = () => {
917
+ hoveringTooltip.current = false;
918
+ handleHideTooltipRef.current();
919
+ };
920
+ const addHoveringTooltipListeners = clickable && (actualCloseEvents.mouseout || actualCloseEvents.mouseleave);
921
+ if (addHoveringTooltipListeners) {
922
+ tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseover', handleMouseOverTooltip);
923
+ tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseout', handleMouseOutTooltip);
924
+ }
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
+ // eslint-disable-next-line react-hooks/exhaustive-deps
935
+ }, [actualOpenEvents, actualCloseEvents, float, clickable]);
936
+ // --- Effect 2: Global close events + auto-update ---
937
+ // Re-runs when the global close config changes, or when the active anchor changes
938
+ // (for scroll parent listeners and floating-ui autoUpdate).
939
+ useEffect(() => {
940
+ const handleScrollResize = () => {
941
+ handleShowRef.current(false);
942
+ };
943
+ const tooltipScrollParent = tooltipScrollParentRef.current;
944
+ const anchorScrollParent = anchorScrollParentRef.current;
636
945
  if (actualGlobalCloseEvents.scroll) {
637
946
  window.addEventListener('scroll', handleScrollResize);
638
947
  anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.addEventListener('scroll', handleScrollResize);
@@ -643,7 +952,7 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
643
952
  window.addEventListener('resize', handleScrollResize);
644
953
  }
645
954
  else if (activeAnchor && tooltipRef.current) {
646
- updateTooltipCleanup = autoUpdate(activeAnchor, tooltipRef.current, updateTooltipPosition, {
955
+ updateTooltipCleanup = autoUpdate(activeAnchor, tooltipRef.current, () => updateTooltipPositionRef.current(), {
647
956
  ancestorResize: true,
648
957
  elementResize: true,
649
958
  layoutShift: true,
@@ -653,295 +962,484 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
653
962
  if (event.key !== 'Escape') {
654
963
  return;
655
964
  }
656
- handleShow(false);
657
- };
658
- if (actualGlobalCloseEvents.escape) {
659
- window.addEventListener('keydown', handleEsc);
965
+ handleShowRef.current(false);
966
+ };
967
+ if (actualGlobalCloseEvents.escape) {
968
+ window.addEventListener('keydown', handleEsc);
969
+ }
970
+ const handleClickOutsideAnchors = (event) => {
971
+ var _a, _b;
972
+ if (!showRef.current) {
973
+ return;
974
+ }
975
+ const target = event.target;
976
+ if (!(target === null || target === void 0 ? void 0 : target.isConnected)) {
977
+ return;
978
+ }
979
+ if ((_a = tooltipRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) {
980
+ return;
981
+ }
982
+ if ((_b = activeAnchorRef.current) === null || _b === void 0 ? void 0 : _b.contains(target)) {
983
+ return;
984
+ }
985
+ if (anchorElementsRef.current.some((anchor) => anchor === null || anchor === void 0 ? void 0 : anchor.contains(target))) {
986
+ return;
987
+ }
988
+ handleShowRef.current(false);
989
+ clearTimeoutRef(tooltipShowDelayTimerRef);
990
+ };
991
+ if (actualGlobalCloseEvents.clickOutsideAnchor) {
992
+ window.addEventListener('click', handleClickOutsideAnchors);
993
+ }
994
+ return () => {
995
+ if (actualGlobalCloseEvents.scroll) {
996
+ window.removeEventListener('scroll', handleScrollResize);
997
+ anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.removeEventListener('scroll', handleScrollResize);
998
+ tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.removeEventListener('scroll', handleScrollResize);
999
+ }
1000
+ if (actualGlobalCloseEvents.resize) {
1001
+ window.removeEventListener('resize', handleScrollResize);
1002
+ }
1003
+ if (updateTooltipCleanup) {
1004
+ updateTooltipCleanup();
1005
+ }
1006
+ if (actualGlobalCloseEvents.escape) {
1007
+ window.removeEventListener('keydown', handleEsc);
1008
+ }
1009
+ if (actualGlobalCloseEvents.clickOutsideAnchor) {
1010
+ window.removeEventListener('click', handleClickOutsideAnchors);
1011
+ }
1012
+ };
1013
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1014
+ }, [actualGlobalCloseEvents, activeAnchor]);
1015
+ };
1016
+
1017
+ // Shared across all tooltip instances — the CSS variable is on :root and never changes per-instance
1018
+ let globalTransitionShowDelay = null;
1019
+ const Tooltip = ({
1020
+ // props
1021
+ 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,
1022
+ // props handled by controller
1023
+ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousActiveAnchor, activeAnchor, setActiveAnchor, border, opacity, arrowColor, arrowSize = 8, role = 'tooltip', }) => {
1024
+ var _a;
1025
+ const tooltipRef = useRef(null);
1026
+ const tooltipArrowRef = useRef(null);
1027
+ const tooltipShowDelayTimerRef = useRef(null);
1028
+ const tooltipHideDelayTimerRef = useRef(null);
1029
+ const tooltipAutoCloseTimerRef = useRef(null);
1030
+ const missedTransitionTimerRef = useRef(null);
1031
+ const [computedPosition, setComputedPosition] = useState({
1032
+ tooltipStyles: {},
1033
+ tooltipArrowStyles: {},
1034
+ place,
1035
+ });
1036
+ const [show, setShow] = useState(false);
1037
+ const [rendered, setRendered] = useState(false);
1038
+ const [imperativeOptions, setImperativeOptions] = useState(null);
1039
+ const wasShowing = useRef(false);
1040
+ const lastFloatPosition = useRef(null);
1041
+ const hoveringTooltip = useRef(false);
1042
+ const mounted = useRef(false);
1043
+ const virtualElementRef = useRef({
1044
+ getBoundingClientRect: () => ({
1045
+ x: 0,
1046
+ y: 0,
1047
+ width: 0,
1048
+ height: 0,
1049
+ top: 0,
1050
+ left: 0,
1051
+ right: 0,
1052
+ bottom: 0,
1053
+ }),
1054
+ });
1055
+ /**
1056
+ * useLayoutEffect runs before useEffect,
1057
+ * but should be used carefully because of caveats
1058
+ * https://beta.reactjs.org/reference/react/useLayoutEffect#caveats
1059
+ */
1060
+ useIsomorphicLayoutEffect(() => {
1061
+ mounted.current = true;
1062
+ return () => {
1063
+ mounted.current = false;
1064
+ };
1065
+ }, []);
1066
+ const handleShow = useCallback((value) => {
1067
+ if (!mounted.current) {
1068
+ return;
1069
+ }
1070
+ if (value) {
1071
+ setRendered(true);
1072
+ }
1073
+ /**
1074
+ * wait for the component to render and calculate position
1075
+ * before actually showing
1076
+ */
1077
+ setTimeout(() => {
1078
+ if (!mounted.current) {
1079
+ return;
1080
+ }
1081
+ setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(value);
1082
+ if (isOpen === undefined) {
1083
+ setShow(value);
1084
+ }
1085
+ }, 10);
1086
+ }, [isOpen, setIsOpen]);
1087
+ /**
1088
+ * Add aria-describedby to activeAnchor when tooltip is active
1089
+ */
1090
+ useEffect(() => {
1091
+ if (!id)
1092
+ return;
1093
+ function getAriaDescribedBy(element) {
1094
+ var _a;
1095
+ return ((_a = element === null || element === void 0 ? void 0 : element.getAttribute('aria-describedby')) === null || _a === void 0 ? void 0 : _a.split(' ')) || [];
1096
+ }
1097
+ function removeAriaDescribedBy(element) {
1098
+ const newDescribedBy = getAriaDescribedBy(element).filter((s) => s !== id);
1099
+ if (newDescribedBy.length) {
1100
+ element === null || element === void 0 ? void 0 : element.setAttribute('aria-describedby', newDescribedBy.join(' '));
1101
+ }
1102
+ else {
1103
+ element === null || element === void 0 ? void 0 : element.removeAttribute('aria-describedby');
1104
+ }
1105
+ }
1106
+ if (show) {
1107
+ removeAriaDescribedBy(previousActiveAnchor);
1108
+ const currentDescribedBy = getAriaDescribedBy(activeAnchor);
1109
+ const describedBy = [...new Set([...currentDescribedBy, id])].filter(Boolean).join(' ');
1110
+ activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.setAttribute('aria-describedby', describedBy);
1111
+ }
1112
+ else {
1113
+ removeAriaDescribedBy(activeAnchor);
1114
+ }
1115
+ return () => {
1116
+ // cleanup aria-describedby when the tooltip is closed
1117
+ removeAriaDescribedBy(activeAnchor);
1118
+ removeAriaDescribedBy(previousActiveAnchor);
1119
+ };
1120
+ }, [activeAnchor, show, id, previousActiveAnchor]);
1121
+ /**
1122
+ * this replicates the effect from `handleShow()`
1123
+ * when `isOpen` is changed from outside
1124
+ */
1125
+ useEffect(() => {
1126
+ if (isOpen === undefined) {
1127
+ return () => null;
1128
+ }
1129
+ if (isOpen) {
1130
+ setRendered(true);
1131
+ }
1132
+ const timeout = setTimeout(() => {
1133
+ setShow(isOpen);
1134
+ }, 10);
1135
+ return () => {
1136
+ clearTimeout(timeout);
1137
+ };
1138
+ }, [isOpen]);
1139
+ useEffect(() => {
1140
+ if (show === wasShowing.current) {
1141
+ return;
1142
+ }
1143
+ clearTimeoutRef(missedTransitionTimerRef);
1144
+ wasShowing.current = show;
1145
+ if (show) {
1146
+ afterShow === null || afterShow === void 0 ? void 0 : afterShow();
1147
+ }
1148
+ else {
1149
+ /**
1150
+ * see `onTransitionEnd` on tooltip wrapper
1151
+ */
1152
+ if (globalTransitionShowDelay === null) {
1153
+ const style = getComputedStyle(document.body);
1154
+ globalTransitionShowDelay = cssTimeToMs(style.getPropertyValue('--rt-transition-show-delay'));
1155
+ }
1156
+ const transitionShowDelay = globalTransitionShowDelay;
1157
+ missedTransitionTimerRef.current = setTimeout(() => {
1158
+ /**
1159
+ * if the tooltip switches from `show === true` to `show === false` too fast
1160
+ * the transition never runs, so `onTransitionEnd` callback never gets fired
1161
+ */
1162
+ setRendered(false);
1163
+ setImperativeOptions(null);
1164
+ afterHide === null || afterHide === void 0 ? void 0 : afterHide();
1165
+ // +25ms just to make sure `onTransitionEnd` (if it gets fired) has time to run
1166
+ }, transitionShowDelay + 25);
1167
+ }
1168
+ }, [afterHide, afterShow, show]);
1169
+ useEffect(() => {
1170
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1171
+ if (!show || !autoClose || autoClose <= 0) {
1172
+ return () => {
1173
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1174
+ };
1175
+ }
1176
+ tooltipAutoCloseTimerRef.current = setTimeout(() => {
1177
+ handleShow(false);
1178
+ }, autoClose);
1179
+ return () => {
1180
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1181
+ };
1182
+ }, [activeAnchor, autoClose, handleShow, show]);
1183
+ const handleComputedPosition = useCallback((newComputedPosition) => {
1184
+ if (!mounted.current) {
1185
+ return;
1186
+ }
1187
+ setComputedPosition((oldComputedPosition) => {
1188
+ if (oldComputedPosition.place === newComputedPosition.place &&
1189
+ oldComputedPosition.tooltipStyles.left === newComputedPosition.tooltipStyles.left &&
1190
+ oldComputedPosition.tooltipStyles.top === newComputedPosition.tooltipStyles.top &&
1191
+ oldComputedPosition.tooltipStyles.border === newComputedPosition.tooltipStyles.border &&
1192
+ oldComputedPosition.tooltipArrowStyles.left ===
1193
+ newComputedPosition.tooltipArrowStyles.left &&
1194
+ oldComputedPosition.tooltipArrowStyles.top === newComputedPosition.tooltipArrowStyles.top &&
1195
+ oldComputedPosition.tooltipArrowStyles.right ===
1196
+ newComputedPosition.tooltipArrowStyles.right &&
1197
+ oldComputedPosition.tooltipArrowStyles.bottom ===
1198
+ newComputedPosition.tooltipArrowStyles.bottom &&
1199
+ oldComputedPosition.tooltipArrowStyles.borderBottom ===
1200
+ newComputedPosition.tooltipArrowStyles.borderBottom &&
1201
+ oldComputedPosition.tooltipArrowStyles.borderRight ===
1202
+ newComputedPosition.tooltipArrowStyles.borderRight) {
1203
+ return oldComputedPosition;
1204
+ }
1205
+ return newComputedPosition;
1206
+ });
1207
+ }, []);
1208
+ const handleShowTooltipDelayed = useCallback((delay = delayShow) => {
1209
+ if (tooltipShowDelayTimerRef.current) {
1210
+ clearTimeout(tooltipShowDelayTimerRef.current);
1211
+ }
1212
+ if (rendered) {
1213
+ // if the tooltip is already rendered, ignore delay
1214
+ handleShow(true);
1215
+ return;
1216
+ }
1217
+ tooltipShowDelayTimerRef.current = setTimeout(() => {
1218
+ handleShow(true);
1219
+ }, delay);
1220
+ }, [delayShow, handleShow, rendered]);
1221
+ const handleHideTooltipDelayed = useCallback((delay = delayHide) => {
1222
+ if (tooltipHideDelayTimerRef.current) {
1223
+ clearTimeout(tooltipHideDelayTimerRef.current);
1224
+ }
1225
+ tooltipHideDelayTimerRef.current = setTimeout(() => {
1226
+ if (hoveringTooltip.current) {
1227
+ return;
1228
+ }
1229
+ handleShow(false);
1230
+ }, delay);
1231
+ }, [delayHide, handleShow]);
1232
+ const handleTooltipPosition = useCallback(({ x, y }) => {
1233
+ var _a;
1234
+ virtualElementRef.current.getBoundingClientRect = () => ({
1235
+ x,
1236
+ y,
1237
+ width: 0,
1238
+ height: 0,
1239
+ top: y,
1240
+ left: x,
1241
+ right: x,
1242
+ bottom: y,
1243
+ });
1244
+ computeTooltipPosition({
1245
+ place: (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place) !== null && _a !== void 0 ? _a : place,
1246
+ offset,
1247
+ elementReference: virtualElementRef.current,
1248
+ tooltipReference: tooltipRef.current,
1249
+ tooltipArrowReference: tooltipArrowRef.current,
1250
+ strategy: positionStrategy,
1251
+ middlewares,
1252
+ border,
1253
+ arrowSize,
1254
+ }).then((computedStylesData) => {
1255
+ handleComputedPosition(computedStylesData);
1256
+ });
1257
+ }, [
1258
+ imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place,
1259
+ place,
1260
+ offset,
1261
+ positionStrategy,
1262
+ middlewares,
1263
+ border,
1264
+ arrowSize,
1265
+ handleComputedPosition,
1266
+ ]);
1267
+ const updateTooltipPosition = useCallback(() => {
1268
+ var _a, _b;
1269
+ const actualPosition = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.position) !== null && _a !== void 0 ? _a : position;
1270
+ if (actualPosition) {
1271
+ // if `position` is set, override regular and `float` positioning
1272
+ handleTooltipPosition(actualPosition);
1273
+ return;
1274
+ }
1275
+ if (float) {
1276
+ if (lastFloatPosition.current) {
1277
+ /*
1278
+ Without this, changes to `content`, `place`, `offset`, ..., will only
1279
+ trigger a position calculation after a `mousemove` event.
1280
+
1281
+ To see why this matters, comment this line, run `yarn dev` and click the
1282
+ "Hover me!" anchor.
1283
+ */
1284
+ handleTooltipPosition(lastFloatPosition.current);
1285
+ }
1286
+ // if `float` is set, override regular positioning
1287
+ return;
660
1288
  }
661
- if (actualGlobalCloseEvents.clickOutsideAnchor) {
662
- window.addEventListener('click', handleClickOutsideAnchors);
1289
+ if (!(activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.isConnected)) {
1290
+ return;
663
1291
  }
664
- const enabledEvents = [];
665
- const handleClickOpenTooltipAnchor = (event) => {
666
- if (show && (event === null || event === void 0 ? void 0 : event.target) === activeAnchor) {
667
- /**
668
- * ignore clicking the anchor that was used to open the tooltip.
669
- * this avoids conflict with the click close event.
670
- */
671
- return;
672
- }
673
- handleShowTooltip(event);
674
- };
675
- const handleClickCloseTooltipAnchor = (event) => {
676
- if (!show || (event === null || event === void 0 ? void 0 : event.target) !== activeAnchor) {
677
- /**
678
- * ignore clicking the anchor that was NOT used to open the tooltip.
679
- * this avoids closing the tooltip when clicking on a
680
- * new anchor with the tooltip already open.
681
- */
682
- return;
683
- }
684
- handleHideTooltip();
685
- };
686
- const regularEvents = ['mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'focus', 'blur'];
687
- const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup'];
688
- Object.entries(actualOpenEvents).forEach(([event, enabled]) => {
689
- if (!enabled) {
690
- return;
691
- }
692
- if (regularEvents.includes(event)) {
693
- enabledEvents.push({ event, listener: debouncedHandleShowTooltip });
694
- }
695
- else if (clickEvents.includes(event)) {
696
- enabledEvents.push({ event, listener: handleClickOpenTooltipAnchor });
697
- }
698
- else ;
699
- });
700
- Object.entries(actualCloseEvents).forEach(([event, enabled]) => {
701
- if (!enabled) {
1292
+ computeTooltipPosition({
1293
+ place: (_b = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place) !== null && _b !== void 0 ? _b : place,
1294
+ offset,
1295
+ elementReference: activeAnchor,
1296
+ tooltipReference: tooltipRef.current,
1297
+ tooltipArrowReference: tooltipArrowRef.current,
1298
+ strategy: positionStrategy,
1299
+ middlewares,
1300
+ border,
1301
+ arrowSize,
1302
+ }).then((computedStylesData) => {
1303
+ if (!mounted.current) {
1304
+ // invalidate computed positions after remount
702
1305
  return;
703
1306
  }
704
- if (regularEvents.includes(event)) {
705
- enabledEvents.push({ event, listener: debouncedHandleHideTooltip });
706
- }
707
- else if (clickEvents.includes(event)) {
708
- enabledEvents.push({ event, listener: handleClickCloseTooltipAnchor });
709
- }
710
- else ;
711
- });
712
- if (float) {
713
- enabledEvents.push({
714
- event: 'pointermove',
715
- listener: handlePointerMove,
716
- });
717
- }
718
- const handleMouseEnterTooltip = () => {
719
- hoveringTooltip.current = true;
720
- };
721
- const handleMouseLeaveTooltip = () => {
722
- hoveringTooltip.current = false;
723
- handleHideTooltip();
724
- };
725
- if (clickable && !hasClickEvent) {
726
- // used to keep the tooltip open when hovering content.
727
- // not needed if using click events.
728
- tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseenter', handleMouseEnterTooltip);
729
- tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseleave', handleMouseLeaveTooltip);
730
- }
731
- enabledEvents.forEach(({ event, listener }) => {
732
- anchorElements.forEach((anchor) => {
733
- anchor.addEventListener(event, listener);
734
- });
1307
+ handleComputedPosition(computedStylesData);
735
1308
  });
736
- return () => {
737
- if (actualGlobalCloseEvents.scroll) {
738
- window.removeEventListener('scroll', handleScrollResize);
739
- anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.removeEventListener('scroll', handleScrollResize);
740
- tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.removeEventListener('scroll', handleScrollResize);
741
- }
742
- if (actualGlobalCloseEvents.resize) {
743
- window.removeEventListener('resize', handleScrollResize);
744
- }
745
- else {
746
- updateTooltipCleanup === null || updateTooltipCleanup === void 0 ? void 0 : updateTooltipCleanup();
747
- }
748
- if (actualGlobalCloseEvents.clickOutsideAnchor) {
749
- window.removeEventListener('click', handleClickOutsideAnchors);
750
- }
751
- if (actualGlobalCloseEvents.escape) {
752
- window.removeEventListener('keydown', handleEsc);
753
- }
754
- if (clickable && !hasClickEvent) {
755
- tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseenter', handleMouseEnterTooltip);
756
- tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseleave', handleMouseLeaveTooltip);
757
- }
758
- enabledEvents.forEach(({ event, listener }) => {
759
- anchorElements.forEach((anchor) => {
760
- anchor.removeEventListener(event, listener);
761
- });
762
- });
763
- };
764
- /**
765
- * rendered is also a dependency to ensure anchor observers are re-registered
766
- * since `tooltipRef` becomes stale after removing/adding the tooltip to the DOM
767
- */
768
1309
  }, [
1310
+ imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.position,
1311
+ imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.place,
1312
+ position,
1313
+ float,
1314
+ activeAnchor,
1315
+ place,
1316
+ offset,
1317
+ positionStrategy,
1318
+ middlewares,
1319
+ border,
1320
+ handleTooltipPosition,
1321
+ handleComputedPosition,
1322
+ arrowSize,
1323
+ ]);
1324
+ const handleActiveAnchorRemoved = useCallback(() => {
1325
+ setRendered(false);
1326
+ handleShow(false);
1327
+ setActiveAnchor(null);
1328
+ clearTimeoutRef(tooltipShowDelayTimerRef);
1329
+ clearTimeoutRef(tooltipHideDelayTimerRef);
1330
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1331
+ }, [handleShow, setActiveAnchor]);
1332
+ const shouldTrackAnchors = rendered ||
1333
+ defaultIsOpen ||
1334
+ Boolean(isOpen) ||
1335
+ Boolean(activeAnchor) ||
1336
+ Boolean(imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect);
1337
+ const { anchorElements, selector: anchorSelector } = useTooltipAnchors({
1338
+ id,
1339
+ anchorSelect,
1340
+ imperativeAnchorSelect: imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect,
1341
+ activeAnchor,
1342
+ disableTooltip,
1343
+ onActiveAnchorRemoved: handleActiveAnchorRemoved,
1344
+ trackAnchors: shouldTrackAnchors,
1345
+ });
1346
+ useTooltipEvents({
769
1347
  activeAnchor,
770
1348
  anchorElements,
1349
+ anchorSelector,
771
1350
  clickable,
772
1351
  closeEvents,
773
1352
  delayHide,
774
1353
  delayShow,
1354
+ disableTooltip,
775
1355
  float,
776
1356
  globalCloseEvents,
777
1357
  handleHideTooltipDelayed,
778
1358
  handleShow,
779
1359
  handleShowTooltipDelayed,
780
1360
  handleTooltipPosition,
1361
+ hoveringTooltip,
781
1362
  imperativeModeOnly,
1363
+ lastFloatPosition,
782
1364
  openEvents,
783
1365
  openOnClick,
784
1366
  setActiveAnchor,
785
1367
  show,
1368
+ tooltipHideDelayTimerRef,
1369
+ tooltipRef,
1370
+ tooltipShowDelayTimerRef,
786
1371
  updateTooltipPosition,
787
- ]);
788
- useEffect(() => {
789
- var _a, _b;
790
- /**
791
- * TODO(V6): break down observer callback for clarity
792
- * - `handleAddedAnchors()`
793
- * - `handleRemovedAnchors()`
794
- */
795
- let selector = (_b = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect) !== null && _a !== void 0 ? _a : anchorSelect) !== null && _b !== void 0 ? _b : '';
796
- if (!selector && id) {
797
- selector = `[data-tooltip-id='${id.replace(/'/g, "\\'")}']`;
798
- }
799
- const documentObserverCallback = (mutationList) => {
800
- const addedAnchors = new Set();
801
- const removedAnchors = new Set();
802
- mutationList.forEach((mutation) => {
803
- if (mutation.type === 'attributes' && mutation.attributeName === 'data-tooltip-id') {
804
- const target = mutation.target;
805
- const newId = target.getAttribute('data-tooltip-id');
806
- if (newId === id) {
807
- addedAnchors.add(target);
808
- }
809
- else if (mutation.oldValue === id) {
810
- // data-tooltip-id has now been changed, so we need to remove this anchor
811
- removedAnchors.add(target);
812
- }
813
- }
814
- if (mutation.type !== 'childList') {
815
- return;
816
- }
817
- const removedNodes = [...mutation.removedNodes].filter((node) => node.nodeType === 1);
818
- if (activeAnchor) {
819
- removedNodes.some((node) => {
820
- var _a;
821
- /**
822
- * TODO(V6)
823
- * - isn't `!activeAnchor.isConnected` better?
824
- * - maybe move to `handleDisconnectedAnchor()`
825
- */
826
- if ((_a = node === null || node === void 0 ? void 0 : node.contains) === null || _a === void 0 ? void 0 : _a.call(node, activeAnchor)) {
827
- setRendered(false);
828
- handleShow(false);
829
- setActiveAnchor(null);
830
- clearTimeoutRef(tooltipShowDelayTimerRef);
831
- clearTimeoutRef(tooltipHideDelayTimerRef);
832
- return true;
833
- }
834
- return false;
835
- });
836
- }
837
- if (!selector) {
838
- return;
839
- }
840
- try {
841
- removedNodes.forEach((node) => {
842
- const element = node;
843
- if (element.matches(selector)) {
844
- // the element itself is an anchor
845
- removedAnchors.add(element);
846
- }
847
- else {
848
- /**
849
- * TODO(V6): do we care if an element which is an anchor,
850
- * has children which are also anchors?
851
- * (i.e. should we remove `else` and always do this)
852
- */
853
- // the element has children which are anchors
854
- element
855
- .querySelectorAll(selector)
856
- .forEach((innerNode) => removedAnchors.add(innerNode));
857
- }
858
- });
859
- }
860
- catch (_a) {
861
- /* c8 ignore start */
862
- {
863
- // eslint-disable-next-line no-console
864
- console.warn(`[react-tooltip] "${selector}" is not a valid CSS selector`);
865
- }
866
- /* c8 ignore end */
867
- }
868
- try {
869
- const addedNodes = [...mutation.addedNodes].filter((node) => node.nodeType === 1);
870
- addedNodes.forEach((node) => {
871
- const element = node;
872
- if (element.matches(selector)) {
873
- // the element itself is an anchor
874
- addedAnchors.add(element);
875
- }
876
- else {
877
- /**
878
- * TODO(V6): do we care if an element which is an anchor,
879
- * has children which are also anchors?
880
- * (i.e. should we remove `else` and always do this)
881
- */
882
- // the element has children which are anchors
883
- element
884
- .querySelectorAll(selector)
885
- .forEach((innerNode) => addedAnchors.add(innerNode));
886
- }
887
- });
888
- }
889
- catch (_b) {
890
- /* c8 ignore start */
891
- {
892
- // eslint-disable-next-line no-console
893
- console.warn(`[react-tooltip] "${selector}" is not a valid CSS selector`);
894
- }
895
- /* c8 ignore end */
896
- }
897
- });
898
- if (addedAnchors.size || removedAnchors.size) {
899
- setAnchorElements((anchors) => [
900
- ...anchors.filter((anchor) => !removedAnchors.has(anchor)),
901
- ...addedAnchors,
902
- ]);
903
- }
904
- };
905
- const documentObserver = new MutationObserver(documentObserverCallback);
906
- // watch for anchor being removed from the DOM
907
- documentObserver.observe(document.body, {
908
- childList: true,
909
- subtree: true,
910
- attributes: true,
911
- attributeFilter: ['data-tooltip-id'],
912
- // to track the prev value if we need to remove anchor when data-tooltip-id gets changed
913
- attributeOldValue: true,
914
- });
915
- return () => {
916
- documentObserver.disconnect();
917
- };
918
- }, [id, anchorSelect, imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect, activeAnchor, handleShow, setActiveAnchor]);
1372
+ });
1373
+ const updateTooltipPositionRef = useRef(updateTooltipPosition);
1374
+ updateTooltipPositionRef.current = updateTooltipPosition;
919
1375
  useEffect(() => {
1376
+ if (!rendered) {
1377
+ return;
1378
+ }
920
1379
  updateTooltipPosition();
921
- }, [updateTooltipPosition]);
1380
+ }, [rendered, updateTooltipPosition]);
922
1381
  useEffect(() => {
923
- if (!(contentWrapperRef === null || contentWrapperRef === void 0 ? void 0 : contentWrapperRef.current)) {
1382
+ if (!rendered || !(contentWrapperRef === null || contentWrapperRef === void 0 ? void 0 : contentWrapperRef.current)) {
924
1383
  return () => null;
925
1384
  }
1385
+ let timeoutId = null;
926
1386
  const contentObserver = new ResizeObserver(() => {
927
- setTimeout(() => updateTooltipPosition());
1387
+ // Clear any existing timeout to prevent memory leaks
1388
+ if (timeoutId) {
1389
+ clearTimeout(timeoutId);
1390
+ }
1391
+ timeoutId = setTimeout(() => {
1392
+ if (mounted.current) {
1393
+ updateTooltipPositionRef.current();
1394
+ }
1395
+ timeoutId = null;
1396
+ }, 0);
928
1397
  });
929
1398
  contentObserver.observe(contentWrapperRef.current);
930
1399
  return () => {
931
1400
  contentObserver.disconnect();
1401
+ if (timeoutId) {
1402
+ clearTimeout(timeoutId);
1403
+ }
932
1404
  };
933
- }, [content, contentWrapperRef, updateTooltipPosition]);
1405
+ }, [content, contentWrapperRef, rendered]);
934
1406
  useEffect(() => {
935
1407
  var _a;
1408
+ const shouldResolveInitialActiveAnchor = rendered || defaultIsOpen || Boolean(isOpen);
1409
+ if (!shouldResolveInitialActiveAnchor) {
1410
+ return;
1411
+ }
1412
+ const activeAnchorMatchesImperativeSelector = (() => {
1413
+ if (!activeAnchor || !(imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect)) {
1414
+ return false;
1415
+ }
1416
+ try {
1417
+ return activeAnchor.matches(imperativeOptions.anchorSelect);
1418
+ }
1419
+ catch (_a) {
1420
+ return false;
1421
+ }
1422
+ })();
936
1423
  if (!activeAnchor || !anchorElements.includes(activeAnchor)) {
937
1424
  /**
938
1425
  * if there is no active anchor,
939
1426
  * or if the current active anchor is not amongst the allowed ones,
940
1427
  * reset it
941
1428
  */
1429
+ if (activeAnchorMatchesImperativeSelector) {
1430
+ return;
1431
+ }
942
1432
  setActiveAnchor((_a = anchorElements[0]) !== null && _a !== void 0 ? _a : null);
943
1433
  }
944
- }, [anchorElements, activeAnchor, setActiveAnchor]);
1434
+ }, [
1435
+ activeAnchor,
1436
+ anchorElements,
1437
+ defaultIsOpen,
1438
+ imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect,
1439
+ isOpen,
1440
+ rendered,
1441
+ setActiveAnchor,
1442
+ ]);
945
1443
  useEffect(() => {
946
1444
  if (defaultIsOpen) {
947
1445
  handleShow(true);
@@ -949,26 +1447,10 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
949
1447
  return () => {
950
1448
  clearTimeoutRef(tooltipShowDelayTimerRef);
951
1449
  clearTimeoutRef(tooltipHideDelayTimerRef);
1450
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1451
+ clearTimeoutRef(missedTransitionTimerRef);
952
1452
  };
953
1453
  }, [defaultIsOpen, handleShow]);
954
- useEffect(() => {
955
- var _a;
956
- let selector = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect) !== null && _a !== void 0 ? _a : anchorSelect;
957
- if (!selector && id) {
958
- selector = `[data-tooltip-id='${id.replace(/'/g, "\\'")}']`;
959
- }
960
- if (!selector) {
961
- return;
962
- }
963
- try {
964
- const anchors = Array.from(document.querySelectorAll(selector));
965
- setAnchorElements(anchors);
966
- }
967
- catch (_b) {
968
- // warning was already issued in the controller
969
- setAnchorElements([]);
970
- }
971
- }, [id, anchorSelect, imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.anchorSelect]);
972
1454
  useEffect(() => {
973
1455
  if (tooltipShowDelayTimerRef.current) {
974
1456
  /**
@@ -980,20 +1462,37 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
980
1462
  }
981
1463
  }, [delayShow, handleShowTooltipDelayed]);
982
1464
  const actualContent = (_a = imperativeOptions === null || imperativeOptions === void 0 ? void 0 : imperativeOptions.content) !== null && _a !== void 0 ? _a : content;
983
- const canShow = show && Object.keys(computedPosition.tooltipStyles).length > 0;
1465
+ const hasContent = actualContent !== null && actualContent !== undefined;
1466
+ const canShow = show && computedPosition.tooltipStyles.left !== undefined;
1467
+ const tooltipStyle = useMemo(() => ({
1468
+ ...externalStyles,
1469
+ ...computedPosition.tooltipStyles,
1470
+ opacity: opacity !== undefined && canShow ? opacity : undefined,
1471
+ }), [externalStyles, computedPosition.tooltipStyles, opacity, canShow]);
1472
+ const arrowBackground = useMemo(() => arrowColor
1473
+ ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`
1474
+ : undefined, [arrowColor]);
1475
+ const arrowStyle = useMemo(() => ({
1476
+ ...computedPosition.tooltipArrowStyles,
1477
+ background: arrowBackground,
1478
+ '--rt-arrow-size': `${arrowSize}px`,
1479
+ }), [computedPosition.tooltipArrowStyles, arrowBackground, arrowSize]);
984
1480
  useImperativeHandle(forwardRef, () => ({
985
1481
  open: (options) => {
1482
+ let imperativeAnchor = null;
986
1483
  if (options === null || options === void 0 ? void 0 : options.anchorSelect) {
987
1484
  try {
988
- document.querySelector(options.anchorSelect);
1485
+ imperativeAnchor = document.querySelector(options.anchorSelect);
989
1486
  }
990
1487
  catch (_a) {
991
- {
992
- // eslint-disable-next-line no-console
993
- console.warn(`[react-tooltip] "${options.anchorSelect}" is not a valid CSS selector`);
994
- }
995
1488
  return;
996
1489
  }
1490
+ if (!imperativeAnchor) {
1491
+ return;
1492
+ }
1493
+ }
1494
+ if (imperativeAnchor) {
1495
+ setActiveAnchor(imperativeAnchor);
997
1496
  }
998
1497
  setImperativeOptions(options !== null && options !== void 0 ? options : null);
999
1498
  if (options === null || options === void 0 ? void 0 : options.delay) {
@@ -1013,9 +1512,18 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
1013
1512
  },
1014
1513
  activeAnchor,
1015
1514
  place: computedPosition.place,
1016
- isOpen: Boolean(rendered && !hidden && actualContent && canShow),
1515
+ isOpen: Boolean(rendered && !hidden && hasContent && canShow),
1017
1516
  }));
1018
- return rendered && !hidden && actualContent ? (React.createElement(WrapperElement, { id: id, role: role, className: clsx('react-tooltip', coreStyles['tooltip'], styles['tooltip'], styles[variant], className, `react-tooltip__place-${computedPosition.place}`, coreStyles[canShow ? 'show' : 'closing'], canShow ? 'react-tooltip__show' : 'react-tooltip__closing', positionStrategy === 'fixed' && coreStyles['fixed'], clickable && coreStyles['clickable']), onTransitionEnd: (event) => {
1517
+ useEffect(() => {
1518
+ return () => {
1519
+ // Final cleanup to ensure no memory leaks
1520
+ clearTimeoutRef(tooltipShowDelayTimerRef);
1521
+ clearTimeoutRef(tooltipHideDelayTimerRef);
1522
+ clearTimeoutRef(tooltipAutoCloseTimerRef);
1523
+ clearTimeoutRef(missedTransitionTimerRef);
1524
+ };
1525
+ }, []);
1526
+ const tooltipNode = rendered && !hidden && hasContent ? (React.createElement(WrapperElement, { id: id, role: role, className: clsx('react-tooltip', coreStyles['tooltip'], styles['tooltip'], styles[variant], className, `react-tooltip__place-${computedPosition.place}`, coreStyles[canShow ? 'show' : 'closing'], canShow ? 'react-tooltip__show' : 'react-tooltip__closing', positionStrategy === 'fixed' && coreStyles['fixed'], clickable && coreStyles['clickable']), onTransitionEnd: (event) => {
1019
1527
  clearTimeoutRef(missedTransitionTimerRef);
1020
1528
  if (show || event.propertyName !== 'opacity') {
1021
1529
  return;
@@ -1023,34 +1531,96 @@ content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, activeAnch
1023
1531
  setRendered(false);
1024
1532
  setImperativeOptions(null);
1025
1533
  afterHide === null || afterHide === void 0 ? void 0 : afterHide();
1026
- }, style: {
1027
- ...externalStyles,
1028
- ...computedPosition.tooltipStyles,
1029
- opacity: opacity !== undefined && canShow ? opacity : undefined,
1030
- }, ref: tooltipRef },
1031
- actualContent,
1032
- React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: {
1033
- ...computedPosition.tooltipArrowStyles,
1034
- background: arrowColor
1035
- ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`
1036
- : undefined,
1037
- }, ref: tooltipArrowRef }))) : null;
1534
+ }, style: tooltipStyle, ref: tooltipRef },
1535
+ React.createElement(WrapperElement, { className: clsx('react-tooltip-content-wrapper', coreStyles['content'], styles['content']) }, actualContent),
1536
+ React.createElement(WrapperElement, { className: clsx('react-tooltip-arrow', coreStyles['arrow'], styles['arrow'], classNameArrow, noArrow && coreStyles['noArrow']), style: arrowStyle, ref: tooltipArrowRef }))) : null;
1537
+ if (!tooltipNode) {
1538
+ return null;
1539
+ }
1540
+ if (portalRoot) {
1541
+ return createPortal(tooltipNode, portalRoot);
1542
+ }
1543
+ return tooltipNode;
1544
+ };
1545
+ var Tooltip$1 = memo(Tooltip);
1546
+
1547
+ /**
1548
+ * Shared MutationObserver for data-tooltip-* attribute changes.
1549
+ * Instead of N observers (one per tooltip), a single observer watches
1550
+ * all active anchors and dispatches changes to registered callbacks.
1551
+ */
1552
+ const observedElements = new Map();
1553
+ let sharedObserver = null;
1554
+ const observerConfig = {
1555
+ attributes: true,
1556
+ childList: false,
1557
+ subtree: false,
1038
1558
  };
1559
+ function getObserver() {
1560
+ if (!sharedObserver) {
1561
+ sharedObserver = new MutationObserver((mutationList) => {
1562
+ var _a;
1563
+ for (const mutation of mutationList) {
1564
+ if (mutation.type !== 'attributes' ||
1565
+ !((_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.startsWith('data-tooltip-'))) {
1566
+ continue;
1567
+ }
1568
+ const target = mutation.target;
1569
+ const callbacks = observedElements.get(target);
1570
+ if (callbacks) {
1571
+ callbacks.forEach((cb) => cb(target));
1572
+ }
1573
+ }
1574
+ });
1575
+ }
1576
+ return sharedObserver;
1577
+ }
1578
+ function observeAnchorAttributes(element, callback) {
1579
+ const observer = getObserver();
1580
+ let callbacks = observedElements.get(element);
1581
+ if (!callbacks) {
1582
+ callbacks = new Set();
1583
+ observedElements.set(element, callbacks);
1584
+ observer.observe(element, observerConfig);
1585
+ }
1586
+ callbacks.add(callback);
1587
+ return () => {
1588
+ const cbs = observedElements.get(element);
1589
+ if (cbs) {
1590
+ cbs.delete(callback);
1591
+ if (cbs.size === 0) {
1592
+ observedElements.delete(element);
1593
+ // MutationObserver doesn't have unobserve — if no elements left, disconnect & reset
1594
+ if (observedElements.size === 0) {
1595
+ observer.disconnect();
1596
+ }
1597
+ else {
1598
+ // Re-observe remaining elements (MutationObserver has no per-target unobserve)
1599
+ observer.disconnect();
1600
+ observedElements.forEach((_cbs, el) => {
1601
+ observer.observe(el, observerConfig);
1602
+ });
1603
+ }
1604
+ }
1605
+ }
1606
+ };
1607
+ }
1039
1608
 
1040
- const TooltipController = React.forwardRef(({ id, anchorSelect, content, render, className, classNameArrow, variant = 'dark', place = 'top', offset = 10, wrapper = 'div', children = null, openOnClick = false, positionStrategy = 'absolute', middlewares, delayShow = 0, delayHide = 0, float = false, hidden = false, noArrow = false, clickable = false, openEvents, closeEvents, globalCloseEvents, imperativeModeOnly = false, style, position, isOpen, defaultIsOpen = false, disableStyleInjection = false, border, opacity, arrowColor, setIsOpen, afterShow, afterHide, role = 'tooltip', }, ref) => {
1041
- const [tooltipContent, setTooltipContent] = useState(content);
1042
- const [tooltipPlace, setTooltipPlace] = useState(place);
1043
- const [tooltipVariant, setTooltipVariant] = useState(variant);
1044
- const [tooltipOffset, setTooltipOffset] = useState(offset);
1045
- const [tooltipDelayShow, setTooltipDelayShow] = useState(delayShow);
1046
- const [tooltipDelayHide, setTooltipDelayHide] = useState(delayHide);
1047
- const [tooltipFloat, setTooltipFloat] = useState(float);
1048
- const [tooltipHidden, setTooltipHidden] = useState(hidden);
1049
- const [tooltipWrapper, setTooltipWrapper] = useState(wrapper);
1050
- const [tooltipPositionStrategy, setTooltipPositionStrategy] = useState(positionStrategy);
1051
- const [tooltipClassName, setTooltipClassName] = useState(null);
1609
+ 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) => {
1610
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1052
1611
  const [activeAnchor, setActiveAnchor] = useState(null);
1612
+ const [anchorDataAttributes, setAnchorDataAttributes] = useState({});
1613
+ const previousActiveAnchorRef = useRef(null);
1053
1614
  const styleInjectionRef = useRef(disableStyleInjection);
1615
+ const handleSetActiveAnchor = useCallback((anchor) => {
1616
+ setActiveAnchor((prev) => {
1617
+ if (!(anchor === null || anchor === void 0 ? void 0 : anchor.isSameNode(prev))) {
1618
+ previousActiveAnchorRef.current = prev;
1619
+ }
1620
+ return anchor;
1621
+ });
1622
+ }, []);
1623
+ /* c8 ignore start */
1054
1624
  const getDataAttributesFromAnchorElement = (elementReference) => {
1055
1625
  const dataAttributes = elementReference === null || elementReference === void 0 ? void 0 : elementReference.getAttributeNames().reduce((acc, name) => {
1056
1626
  var _a;
@@ -1062,101 +1632,11 @@ const TooltipController = React.forwardRef(({ id, anchorSelect, content, render,
1062
1632
  }, {});
1063
1633
  return dataAttributes;
1064
1634
  };
1065
- const applyAllDataAttributesFromAnchorElement = useCallback((dataAttributes) => {
1066
- const handleDataAttributes = {
1067
- place: (value) => {
1068
- var _a;
1069
- setTooltipPlace((_a = value) !== null && _a !== void 0 ? _a : place);
1070
- },
1071
- content: (value) => {
1072
- setTooltipContent(value !== null && value !== void 0 ? value : content);
1073
- },
1074
- variant: (value) => {
1075
- var _a;
1076
- setTooltipVariant((_a = value) !== null && _a !== void 0 ? _a : variant);
1077
- },
1078
- offset: (value) => {
1079
- setTooltipOffset(value === null ? offset : Number(value));
1080
- },
1081
- wrapper: (value) => {
1082
- var _a;
1083
- setTooltipWrapper((_a = value) !== null && _a !== void 0 ? _a : wrapper);
1084
- },
1085
- 'position-strategy': (value) => {
1086
- var _a;
1087
- setTooltipPositionStrategy((_a = value) !== null && _a !== void 0 ? _a : positionStrategy);
1088
- },
1089
- 'delay-show': (value) => {
1090
- setTooltipDelayShow(value === null ? delayShow : Number(value));
1091
- },
1092
- 'delay-hide': (value) => {
1093
- setTooltipDelayHide(value === null ? delayHide : Number(value));
1094
- },
1095
- float: (value) => {
1096
- setTooltipFloat(value === null ? float : value === 'true');
1097
- },
1098
- hidden: (value) => {
1099
- setTooltipHidden(value === null ? hidden : value === 'true');
1100
- },
1101
- 'class-name': (value) => {
1102
- setTooltipClassName(value);
1103
- },
1104
- };
1105
- // reset unset data attributes to default values
1106
- // without this, data attributes from the last active anchor will still be used
1107
- Object.values(handleDataAttributes).forEach((handler) => handler(null));
1108
- Object.entries(dataAttributes).forEach(([key, value]) => {
1109
- var _a;
1110
- (_a = handleDataAttributes[key]) === null || _a === void 0 ? void 0 : _a.call(handleDataAttributes, value);
1111
- });
1112
- }, [
1113
- content,
1114
- delayHide,
1115
- delayShow,
1116
- float,
1117
- hidden,
1118
- offset,
1119
- place,
1120
- positionStrategy,
1121
- variant,
1122
- wrapper,
1123
- ]);
1124
- useEffect(() => {
1125
- setTooltipContent(content);
1126
- }, [content]);
1127
- useEffect(() => {
1128
- setTooltipPlace(place);
1129
- }, [place]);
1130
- useEffect(() => {
1131
- setTooltipVariant(variant);
1132
- }, [variant]);
1133
- useEffect(() => {
1134
- setTooltipOffset(offset);
1135
- }, [offset]);
1136
- useEffect(() => {
1137
- setTooltipDelayShow(delayShow);
1138
- }, [delayShow]);
1139
- useEffect(() => {
1140
- setTooltipDelayHide(delayHide);
1141
- }, [delayHide]);
1142
- useEffect(() => {
1143
- setTooltipFloat(float);
1144
- }, [float]);
1145
- useEffect(() => {
1146
- setTooltipHidden(hidden);
1147
- }, [hidden]);
1148
- useEffect(() => {
1149
- setTooltipPositionStrategy(positionStrategy);
1150
- }, [positionStrategy]);
1635
+ /* c8 ignore end */
1151
1636
  useEffect(() => {
1152
1637
  if (styleInjectionRef.current === disableStyleInjection) {
1153
1638
  return;
1154
1639
  }
1155
- /* c8 ignore start */
1156
- {
1157
- // eslint-disable-next-line no-console
1158
- console.warn('[react-tooltip] Do not change `disableStyleInjection` dynamically.');
1159
- }
1160
1640
  /* c8 ignore end */
1161
1641
  }, [disableStyleInjection]);
1162
1642
  useEffect(() => {
@@ -1171,58 +1651,61 @@ const TooltipController = React.forwardRef(({ id, anchorSelect, content, render,
1171
1651
  // eslint-disable-next-line react-hooks/exhaustive-deps
1172
1652
  }, []);
1173
1653
  useEffect(() => {
1174
- const observerCallback = (mutationList) => {
1175
- mutationList.forEach((mutation) => {
1176
- var _a;
1177
- if (!activeAnchor ||
1178
- mutation.type !== 'attributes' ||
1179
- !((_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.startsWith('data-tooltip-'))) {
1180
- return;
1654
+ if (!activeAnchor) {
1655
+ setAnchorDataAttributes({});
1656
+ return () => { };
1657
+ }
1658
+ const updateAttributes = (element) => {
1659
+ const attrs = getDataAttributesFromAnchorElement(element);
1660
+ setAnchorDataAttributes((prev) => {
1661
+ const keys = Object.keys(attrs);
1662
+ const prevKeys = Object.keys(prev);
1663
+ if (keys.length === prevKeys.length && keys.every((key) => attrs[key] === prev[key])) {
1664
+ return prev;
1181
1665
  }
1182
- // make sure to get all set attributes, since all unset attributes are reset
1183
- const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor);
1184
- applyAllDataAttributesFromAnchorElement(dataAttributes);
1666
+ return attrs;
1185
1667
  });
1186
1668
  };
1187
- // Create an observer instance linked to the callback function
1188
- const observer = new MutationObserver(observerCallback);
1189
- // do not check for subtree and childrens, we only want to know attribute changes
1190
- // to stay watching `data-attributes-*` from anchor element
1191
- const observerConfig = { attributes: true, childList: false, subtree: false };
1192
- if (activeAnchor) {
1193
- const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor);
1194
- applyAllDataAttributesFromAnchorElement(dataAttributes);
1195
- // Start observing the target node for configured mutations
1196
- observer.observe(activeAnchor, observerConfig);
1197
- }
1198
- return () => {
1199
- // Remove the observer when the tooltip is destroyed
1200
- observer.disconnect();
1201
- };
1202
- }, [activeAnchor, anchorSelect, applyAllDataAttributesFromAnchorElement]);
1669
+ updateAttributes(activeAnchor);
1670
+ const unsubscribe = observeAnchorAttributes(activeAnchor, updateAttributes);
1671
+ return unsubscribe;
1672
+ }, [activeAnchor, anchorSelect]);
1203
1673
  useEffect(() => {
1204
- /* c8 ignore end */
1205
- if (style === null || style === void 0 ? void 0 : style.border) {
1206
- // eslint-disable-next-line no-console
1207
- console.warn('[react-tooltip] Do not set `style.border`. Use `border` prop instead.');
1208
- }
1209
- if (style === null || style === void 0 ? void 0 : style.opacity) {
1210
- // eslint-disable-next-line no-console
1211
- console.warn('[react-tooltip] Do not set `style.opacity`. Use `opacity` prop instead.');
1674
+ /* c8 ignore start */
1675
+ {
1676
+ return;
1212
1677
  }
1213
1678
  }, [border, opacity, style === null || style === void 0 ? void 0 : style.border, style === null || style === void 0 ? void 0 : style.opacity]);
1214
1679
  /**
1215
1680
  * content priority: children < render or content < html
1216
1681
  * children should be lower priority so that it can be used as the "default" content
1217
1682
  */
1683
+ const tooltipContent = (_a = anchorDataAttributes.content) !== null && _a !== void 0 ? _a : content;
1684
+ const tooltipPlace = (_b = anchorDataAttributes.place) !== null && _b !== void 0 ? _b : place;
1685
+ const tooltipVariant = (_c = anchorDataAttributes.variant) !== null && _c !== void 0 ? _c : variant;
1686
+ const tooltipOffset = anchorDataAttributes.offset == null ? offset : Number(anchorDataAttributes.offset);
1687
+ const tooltipWrapper = (_d = anchorDataAttributes.wrapper) !== null && _d !== void 0 ? _d : wrapper;
1688
+ const tooltipPositionStrategy = (_e = anchorDataAttributes['position-strategy']) !== null && _e !== void 0 ? _e : positionStrategy;
1689
+ const tooltipDelayShow = anchorDataAttributes['delay-show'] == null
1690
+ ? delayShow
1691
+ : Number(anchorDataAttributes['delay-show']);
1692
+ const tooltipDelayHide = anchorDataAttributes['delay-hide'] == null
1693
+ ? delayHide
1694
+ : Number(anchorDataAttributes['delay-hide']);
1695
+ const tooltipAutoClose = anchorDataAttributes['auto-close'] == null
1696
+ ? autoClose
1697
+ : Number(anchorDataAttributes['auto-close']);
1698
+ const tooltipFloat = anchorDataAttributes.float == null ? float : anchorDataAttributes.float === 'true';
1699
+ const tooltipHidden = anchorDataAttributes.hidden == null ? hidden : anchorDataAttributes.hidden === 'true';
1700
+ const tooltipClassName = (_f = anchorDataAttributes['class-name']) !== null && _f !== void 0 ? _f : null;
1218
1701
  let renderedContent = children;
1219
1702
  const contentWrapperRef = useRef(null);
1220
1703
  if (render) {
1221
- const actualContent = (activeAnchor === null || activeAnchor === void 0 ? void 0 : activeAnchor.getAttribute('data-tooltip-content')) || tooltipContent || null;
1704
+ const actualContent = (_h = (_g = anchorDataAttributes.content) !== null && _g !== void 0 ? _g : tooltipContent) !== null && _h !== void 0 ? _h : null;
1222
1705
  const rendered = render({ content: actualContent, activeAnchor });
1223
1706
  renderedContent = rendered ? (React.createElement("div", { ref: contentWrapperRef, className: "react-tooltip-content-wrapper" }, rendered)) : null;
1224
1707
  }
1225
- else if (tooltipContent) {
1708
+ else if (tooltipContent !== null && tooltipContent !== undefined) {
1226
1709
  renderedContent = tooltipContent;
1227
1710
  }
1228
1711
  const props = {
@@ -1233,6 +1716,7 @@ const TooltipController = React.forwardRef(({ id, anchorSelect, content, render,
1233
1716
  classNameArrow,
1234
1717
  content: renderedContent,
1235
1718
  contentWrapperRef,
1719
+ portalRoot,
1236
1720
  place: tooltipPlace,
1237
1721
  variant: tooltipVariant,
1238
1722
  offset: tooltipOffset,
@@ -1242,6 +1726,7 @@ const TooltipController = React.forwardRef(({ id, anchorSelect, content, render,
1242
1726
  middlewares,
1243
1727
  delayShow: tooltipDelayShow,
1244
1728
  delayHide: tooltipDelayHide,
1729
+ autoClose: tooltipAutoClose,
1245
1730
  float: tooltipFloat,
1246
1731
  hidden: tooltipHidden,
1247
1732
  noArrow,
@@ -1257,15 +1742,19 @@ const TooltipController = React.forwardRef(({ id, anchorSelect, content, render,
1257
1742
  border,
1258
1743
  opacity,
1259
1744
  arrowColor,
1745
+ arrowSize,
1260
1746
  setIsOpen,
1261
1747
  afterShow,
1262
1748
  afterHide,
1749
+ disableTooltip,
1263
1750
  activeAnchor,
1264
- setActiveAnchor,
1751
+ previousActiveAnchor: previousActiveAnchorRef.current,
1752
+ setActiveAnchor: handleSetActiveAnchor,
1265
1753
  role,
1266
1754
  };
1267
- return React.createElement(Tooltip, { ...props });
1755
+ return React.createElement(Tooltip$1, { ...props });
1268
1756
  });
1757
+ var TooltipController_default = memo(TooltipController);
1269
1758
 
1270
1759
  // those content will be replaced in build time with the `react-tooltip.css` builded content
1271
1760
  const TooltipCoreStyles = `:root {
@@ -1278,6 +1767,7 @@ const TooltipCoreStyles = `:root {
1278
1767
  --rt-opacity: 0.9;
1279
1768
  --rt-transition-show-delay: 0.15s;
1280
1769
  --rt-transition-closing-delay: 0.15s;
1770
+ --rt-arrow-size: 8px;
1281
1771
  }
1282
1772
 
1283
1773
  .core-styles-module_tooltip__3vRRp {
@@ -1286,7 +1776,6 @@ const TooltipCoreStyles = `:root {
1286
1776
  left: 0;
1287
1777
  pointer-events: none;
1288
1778
  opacity: 0;
1289
- will-change: opacity;
1290
1779
  }
1291
1780
 
1292
1781
  .core-styles-module_fixed__pcSol {
@@ -1296,6 +1785,14 @@ const TooltipCoreStyles = `:root {
1296
1785
  .core-styles-module_arrow__cvMwQ {
1297
1786
  position: absolute;
1298
1787
  background: inherit;
1788
+ z-index: -1;
1789
+ -webkit-backface-visibility: hidden;
1790
+ backface-visibility: hidden;
1791
+ }
1792
+
1793
+ .core-styles-module_content__BRKdB {
1794
+ position: relative;
1795
+ z-index: 1;
1299
1796
  }
1300
1797
 
1301
1798
  .core-styles-module_noArrow__xock6 {
@@ -1309,26 +1806,33 @@ const TooltipCoreStyles = `:root {
1309
1806
  .core-styles-module_show__Nt9eE {
1310
1807
  opacity: var(--rt-opacity);
1311
1808
  transition: opacity var(--rt-transition-show-delay) ease-out;
1809
+ will-change: opacity;
1312
1810
  }
1313
1811
 
1314
1812
  .core-styles-module_closing__sGnxF {
1315
1813
  opacity: 0;
1316
1814
  transition: opacity var(--rt-transition-closing-delay) ease-in;
1815
+ will-change: opacity;
1317
1816
  }
1318
1817
 
1319
1818
  `;
1320
1819
  const TooltipStyles = `
1321
1820
 
1322
1821
  .styles-module_tooltip__mnnfp {
1323
- padding: 8px 16px;
1324
1822
  border-radius: 3px;
1325
1823
  font-size: 90%;
1326
1824
  width: max-content;
1327
1825
  }
1328
1826
 
1827
+ .styles-module_content__ydYdI {
1828
+ background: inherit;
1829
+ border-radius: inherit;
1830
+ padding: 8px 16px;
1831
+ }
1832
+
1329
1833
  .styles-module_arrow__K0L3T {
1330
- width: 8px;
1331
- height: 8px;
1834
+ width: var(--rt-arrow-size);
1835
+ height: var(--rt-arrow-size);
1332
1836
  }
1333
1837
 
1334
1838
  [class*='react-tooltip__place-top'] > .styles-module_arrow__K0L3T {
@@ -1389,5 +1893,5 @@ if (typeof window !== 'undefined') {
1389
1893
  }));
1390
1894
  }
1391
1895
 
1392
- export { TooltipController as Tooltip };
1896
+ export { TooltipController_default as Tooltip };
1393
1897
  //# sourceMappingURL=react-tooltip.mjs.map