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