UNPKG

react-tooltip

Version:
1,109 lines (1,092 loc) 84.5 kB
/* * React Tooltip * {@link https://github.com/ReactTooltip/react-tooltip} * @copyright ReactTooltip Team * @license MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('clsx'), require('react-dom'), require('@floating-ui/dom')) : typeof define === 'function' && define.amd ? define(['exports', 'react', 'clsx', 'react-dom', '@floating-ui/dom'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactTooltip = {}, global.React, global.clsx, global.ReactDOM, global.FloatingUIDOM)); })(this, (function (exports, React, clsx, reactDom, dom) { 'use strict'; // This is the ID for the core styles of ReactTooltip const REACT_TOOLTIP_CORE_STYLES_ID = 'react-tooltip-core-styles'; // This is the ID for the visual styles of ReactTooltip const REACT_TOOLTIP_BASE_STYLES_ID = 'react-tooltip-base-styles'; const injected = { core: false, base: false, }; /** * Note about `state` parameter: * This parameter is used to keep track of the state of the styles * into the tests since the const `injected` is not acessible or resettable in the tests */ function injectStyle({ css, id = REACT_TOOLTIP_BASE_STYLES_ID, type = 'base', ref, state = {}, }) { if (!css || typeof document === 'undefined' || (typeof state[type] !== 'undefined' ? state[type] : injected[type])) { return; } if (type === 'core' && typeof process !== 'undefined' && // this validation prevents docs from breaking even with `process?` process.env && process.env.REACT_TOOLTIP_DISABLE_CORE_STYLES) { return; } if (type === 'base' && typeof process !== 'undefined' && // this validation prevents docs from breaking even with `process?` process.env && process.env.REACT_TOOLTIP_DISABLE_BASE_STYLES) { return; } if (type === 'core') { id = REACT_TOOLTIP_CORE_STYLES_ID; } if (!ref) { ref = {}; } const { insertAt } = ref; if (document.getElementById(id)) { // this could happen in cases the tooltip is imported by multiple js modules return; } const head = document.head || document.getElementsByTagName('head')[0]; // eslint-disable-next-line @typescript-eslint/no-explicit-any const style = document.createElement('style'); style.id = id; style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } if (typeof state[type] !== 'undefined') { state[type] = true; } else { injected[type] = true; // internal global state that jest doesn't have access } } // Hoisted constant middlewares — these configs never change const defaultFlip = dom.flip({ fallbackAxisSideDirection: 'start' }); const defaultShift = dom.shift({ padding: 5 }); 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, }) => { if (!elementReference) { // elementReference can be null or undefined and we will not compute the position // console.error('The reference element for tooltip was not defined: ', elementReference) return { tooltipStyles: {}, tooltipArrowStyles: {}, place }; } if (tooltipReference === null) { return { tooltipStyles: {}, tooltipArrowStyles: {}, place }; } const middleware = [...middlewares]; if (tooltipArrowReference) { middleware.push(dom.arrow({ element: tooltipArrowReference, padding: 5 })); return dom.computePosition(elementReference, tooltipReference, { placement: place, strategy, middleware, }).then(({ x, y, placement, middlewareData }) => { var _a, _b; const styles = { left: `${x}px`, top: `${y}px`, border }; /* c8 ignore start */ const { x: arrowX, y: arrowY } = (_a = middlewareData.arrow) !== null && _a !== void 0 ? _a : { x: 0, y: 0 }; const staticSide = (_b = { top: 'bottom', right: 'left', bottom: 'top', left: 'right', }[placement.split('-')[0]]) !== null && _b !== void 0 ? _b : 'bottom'; /* c8 ignore end */ const borderSide = border && { borderBottom: border, borderRight: border, }; let borderWidth = 0; if (border) { const match = `${border}`.match(/(\d+)px/); if (match === null || match === void 0 ? void 0 : match[1]) { borderWidth = Number(match[1]); } else { /** * this means `border` was set without `width`, * or non-px value (such as `medium`, `thick`, ...) */ borderWidth = 1; } } /* c8 ignore start */ const arrowStyle = { left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', right: '', bottom: '', ...borderSide, [staticSide]: `-${arrowSize / 2 + borderWidth - 1}px`, }; /* c8 ignore end */ return { tooltipStyles: styles, tooltipArrowStyles: arrowStyle, place: placement }; }); } return dom.computePosition(elementReference, tooltipReference, { placement: 'bottom', strategy, middleware, }).then(({ x, y, placement }) => { const styles = { left: `${x}px`, top: `${y}px` }; return { tooltipStyles: styles, tooltipArrowStyles: {}, place: placement }; }); }; const cssTimeToMs = (time) => { const match = time.match(/^([\d.]+)(m?s)$/); if (!match) { return 0; } const [, amount, unit] = match; return Number(amount) * (unit === 'ms' ? 1 : 1000); }; /* eslint-disable @typescript-eslint/no-explicit-any */ /** * This function debounce the received function * @param { function } func Function to be debounced * @param { number } wait Time to wait before execut the function * @param { boolean } immediate Param to define if the function will be executed immediately */ const debounce = (func, wait, immediate) => { let timeout = null; let currentFunc = func; const debounced = function debounced(...args) { const later = () => { timeout = null; }; if (!timeout) { /** * there's no need to clear the timeout * since we expect it to resolve and set `timeout = null` */ currentFunc.apply(this, args); timeout = setTimeout(later, wait); } }; debounced.cancel = () => { /* c8 ignore start */ if (!timeout) { return; } /* c8 ignore end */ clearTimeout(timeout); timeout = null; }; debounced.setCallback = (newFunc) => { currentFunc = newFunc; }; return debounced; }; const isScrollable = (node) => { if (!(node instanceof HTMLElement || node instanceof SVGElement)) { return false; } const style = getComputedStyle(node); return ['overflow', 'overflow-x', 'overflow-y'].some((propertyName) => { const value = style.getPropertyValue(propertyName); return value === 'auto' || value === 'scroll'; }); }; const getScrollParent = (node) => { if (!node) { return null; } let currentParent = node.parentElement; while (currentParent) { if (isScrollable(currentParent)) { return currentParent; } currentParent = currentParent.parentElement; } return document.scrollingElement || document.documentElement; }; // React currently throws a warning when using useLayoutEffect on the server. // To get around it, we can conditionally useEffect on the server (no-op) and // useLayoutEffect in the browser. We need useLayoutEffect to ensure the store // subscription callback always has the selector from the latest render commit // available, otherwise a store update may happen between render and the effect, // which may cause missed updates; we also must ensure the store subscription // is created synchronously, otherwise a store update may occur before the // subscription is created and an inconsistent state may be observed const isHopefullyDomEnvironment = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined'; const useIsomorphicLayoutEffect = isHopefullyDomEnvironment ? React.useLayoutEffect : React.useEffect; const clearTimeoutRef = (ref) => { if (ref.current) { clearTimeout(ref.current); ref.current = null; } }; function parseDataTooltipIdSelector(selector) { const match = selector.match(/^\[data-tooltip-id=(['"])((?:\\.|(?!\1).)*)\1\]$/); if (!match) { return null; } return match[2].replace(/\\(['"])/g, '$1'); } function resolveDataTooltipAnchor(targetElement, tooltipId) { let currentElement = targetElement; while (currentElement) { const dataset = currentElement.dataset; if ((dataset === null || dataset === void 0 ? void 0 : dataset.tooltipId) === tooltipId) { return currentElement; } currentElement = currentElement.parentElement; } return null; } 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"}; 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"}; const registry = new Map(); let documentObserver = null; /** * Extract a tooltip ID from a simple `[data-tooltip-id='value']` selector. * Returns null for complex or custom selectors. */ function extractTooltipId(selector) { const match = selector.match(/^\[data-tooltip-id=(['"])((?:\\.|(?!\1).)*)\1\]$/); return match ? match[2].replace(/\\(['"])/g, '$1') : null; } function areAnchorListsEqual(left, right) { if (left.length !== right.length) { return false; } return left.every((anchor, index) => anchor === right[index]); } function readAnchorsForSelector(selector) { try { return { anchors: Array.from(document.querySelectorAll(selector)), error: null, }; } catch (error) { return { anchors: [], error: error instanceof Error ? error : new Error(String(error)), }; } } function notifySubscribers(entry) { entry.subscribers.forEach((subscriber) => subscriber(entry.anchors, entry.error)); } function refreshEntry(selector, entry) { var _a, _b, _c, _d; const nextState = readAnchorsForSelector(selector); const nextErrorMessage = (_b = (_a = nextState.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : null; const previousErrorMessage = (_d = (_c = entry.error) === null || _c === void 0 ? void 0 : _c.message) !== null && _d !== void 0 ? _d : null; if (areAnchorListsEqual(entry.anchors, nextState.anchors) && nextErrorMessage === previousErrorMessage) { return; } const nextEntry = { ...entry, anchors: nextState.anchors, error: nextState.error, }; registry.set(selector, nextEntry); notifySubscribers(nextEntry); } function refreshAllEntries() { registry.forEach((entry, selector) => { refreshEntry(selector, entry); }); } let refreshScheduled = false; let pendingTooltipIds = null; let pendingFullRefresh = false; function scheduleRefresh(affectedTooltipIds) { if (affectedTooltipIds) { if (!pendingTooltipIds) { pendingTooltipIds = new Set(); } affectedTooltipIds.forEach((id) => pendingTooltipIds.add(id)); } else { pendingFullRefresh = true; } if (refreshScheduled) { return; } refreshScheduled = true; const flush = () => { refreshScheduled = false; const fullRefresh = pendingFullRefresh; const ids = pendingTooltipIds; pendingFullRefresh = false; pendingTooltipIds = null; if (fullRefresh) { refreshAllEntries(); } else if (ids && ids.size > 0) { refreshEntriesForTooltipIds(ids); } }; if (typeof requestAnimationFrame === 'function') { requestAnimationFrame(flush); } else { Promise.resolve().then(flush); } } /** * Only refresh entries whose tooltipId is in the affected set, * plus any entries with custom (non-tooltipId) selectors. */ function refreshEntriesForTooltipIds(affectedIds) { registry.forEach((entry, selector) => { if (entry.tooltipId === null || affectedIds.has(entry.tooltipId)) { refreshEntry(selector, entry); } }); } /** * Collect tooltip IDs from mutation records. Returns null when targeted * analysis is not worthwhile (few registry entries, or too many nodes to scan). */ function collectAffectedTooltipIds(records) { var _a; // Targeted refresh only pays off when there are many distinct selectors. // With few entries, full refresh is already cheap — skip the analysis overhead. if (registry.size <= 4) { return null; } const ids = new Set(); for (const record of records) { if (record.type === 'attributes') { const target = record.target; const currentId = (_a = target.getAttribute) === null || _a === void 0 ? void 0 : _a.call(target, 'data-tooltip-id'); if (currentId) ids.add(currentId); if (record.oldValue) ids.add(record.oldValue); continue; } if (record.type === 'childList') { const gatherIds = (nodes) => { var _a, _b; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.nodeType !== Node.ELEMENT_NODE) continue; const el = node; const id = (_a = el.getAttribute) === null || _a === void 0 ? void 0 : _a.call(el, 'data-tooltip-id'); if (id) ids.add(id); // For large subtrees, bail out to full refresh to avoid double-scanning const descendants = (_b = el.querySelectorAll) === null || _b === void 0 ? void 0 : _b.call(el, '[data-tooltip-id]'); if (descendants) { if (descendants.length > 50) { return true; // signal bail-out } for (let j = 0; j < descendants.length; j++) { const descId = descendants[j].getAttribute('data-tooltip-id'); if (descId) ids.add(descId); } } } return false; }; if (gatherIds(record.addedNodes) || gatherIds(record.removedNodes)) { return null; // large mutation — full refresh is cheaper } continue; } } return ids; } function ensureDocumentObserver() { if (documentObserver || typeof MutationObserver === 'undefined') { return; } documentObserver = new MutationObserver((records) => { const affectedIds = collectAffectedTooltipIds(records); scheduleRefresh(affectedIds); }); documentObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-tooltip-id'], attributeOldValue: true, }); } function cleanupDocumentObserverIfUnused() { if (registry.size !== 0 || !documentObserver) { return; } documentObserver.disconnect(); documentObserver = null; } function subscribeAnchorSelector(selector, subscriber) { let entry = registry.get(selector); if (!entry) { const initialState = readAnchorsForSelector(selector); entry = { anchors: initialState.anchors, error: initialState.error, subscribers: new Set(), tooltipId: extractTooltipId(selector), }; registry.set(selector, entry); } entry.subscribers.add(subscriber); ensureDocumentObserver(); subscriber([...entry.anchors], entry.error); return () => { const currentEntry = registry.get(selector); if (!currentEntry) { return; } currentEntry.subscribers.delete(subscriber); if (currentEntry.subscribers.size === 0) { registry.delete(selector); } cleanupDocumentObserverIfUnused(); }; } const getAnchorSelector = ({ id, anchorSelect, imperativeAnchorSelect, }) => { var _a; let selector = (_a = imperativeAnchorSelect !== null && imperativeAnchorSelect !== void 0 ? imperativeAnchorSelect : anchorSelect) !== null && _a !== void 0 ? _a : ''; if (!selector && id) { selector = `[data-tooltip-id='${id.replace(/'/g, "\\'")}']`; } return selector; }; const useTooltipAnchors = ({ id, anchorSelect, imperativeAnchorSelect, activeAnchor, disableTooltip, onActiveAnchorRemoved, trackAnchors, }) => { const [rawAnchorElements, setRawAnchorElements] = React.useState([]); const [selectorError, setSelectorError] = React.useState(null); const warnedSelectorRef = React.useRef(null); const selector = React.useMemo(() => getAnchorSelector({ id, anchorSelect, imperativeAnchorSelect }), [id, anchorSelect, imperativeAnchorSelect]); const anchorElements = React.useMemo(() => rawAnchorElements.filter((anchor) => !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(anchor))), [rawAnchorElements, disableTooltip]); const activeAnchorMatchesSelector = React.useMemo(() => { if (!activeAnchor || !selector) { return false; } try { return activeAnchor.matches(selector); } catch (_a) { return false; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeAnchor, selector, anchorElements]); React.useEffect(() => { if (!selector || !trackAnchors) { setRawAnchorElements([]); setSelectorError(null); return undefined; } return subscribeAnchorSelector(selector, (anchors, error) => { setRawAnchorElements(anchors); setSelectorError(error); }); }, [selector, trackAnchors]); React.useEffect(() => { if (!selectorError || warnedSelectorRef.current === selector) { return; } warnedSelectorRef.current = selector; /* c8 ignore end */ }, [selector, selectorError]); React.useEffect(() => { if (!activeAnchor) { return; } if (!activeAnchor.isConnected) { onActiveAnchorRemoved(); return; } if (!anchorElements.includes(activeAnchor) && !activeAnchorMatchesSelector) { onActiveAnchorRemoved(); } }, [activeAnchor, anchorElements, activeAnchorMatchesSelector, onActiveAnchorRemoved]); return { anchorElements, selector, }; }; /** * Shared document event delegation. * * Instead of N tooltips each calling document.addEventListener(type, handler), * we maintain ONE document listener per event type. When the event fires, * we iterate through all registered handlers for that type. * * This reduces document-level listeners from O(N × eventTypes) to O(eventTypes). */ const handlersByType = new Map(); function getListenerKey(eventType, capture) { return `${eventType}:${capture ? 'capture' : 'bubble'}`; } function getOrCreateListener(eventType, capture) { const key = getListenerKey(eventType, capture); let listener = handlersByType.get(key); if (!listener) { const handlers = new Set(); const dispatch = (event) => { handlers.forEach((handler) => { handler(event); }); }; listener = { handlers, dispatch, eventType, capture }; handlersByType.set(key, listener); document.addEventListener(eventType, dispatch, { capture }); } return listener; } /** * Register a handler for a document-level event type. * Returns an unsubscribe function. */ function addDelegatedEventListener(eventType, handler, options = {}) { const capture = Boolean(options.capture); const key = getListenerKey(eventType, capture); const listener = getOrCreateListener(eventType, capture); listener.handlers.add(handler); return () => { listener.handlers.delete(handler); if (listener.handlers.size === 0) { handlersByType.delete(key); document.removeEventListener(eventType, listener.dispatch, { capture }); } }; } const useTooltipEvents = ({ activeAnchor, anchorElements, anchorSelector, clickable, closeEvents, delayHide, delayShow, disableTooltip, float, globalCloseEvents, handleHideTooltipDelayed, handleShow, handleShowTooltipDelayed, handleTooltipPosition, hoveringTooltip, imperativeModeOnly, lastFloatPosition, openEvents, openOnClick, rendered, setActiveAnchor, show, tooltipHideDelayTimerRef, tooltipRef, tooltipShowDelayTimerRef, updateTooltipPosition, }) => { // Ref-stable debounced handlers — avoids recreating debounce instances on every effect run // eslint-disable-next-line @typescript-eslint/no-unused-vars const debouncedShowRef = React.useRef(debounce((_anchor) => { }, 50)); const debouncedHideRef = React.useRef(debounce(() => { }, 50)); // Cache scroll parents — only recompute when the element actually changes const anchorScrollParentRef = React.useRef(null); const tooltipScrollParentRef = React.useRef(null); const prevAnchorRef = React.useRef(null); const prevTooltipRef = React.useRef(null); if (activeAnchor !== prevAnchorRef.current) { prevAnchorRef.current = activeAnchor; anchorScrollParentRef.current = getScrollParent(activeAnchor); } const currentTooltipEl = tooltipRef.current; if (currentTooltipEl !== prevTooltipRef.current) { prevTooltipRef.current = currentTooltipEl; tooltipScrollParentRef.current = getScrollParent(currentTooltipEl); } // Memoize event config objects — only rebuild when the relevant props change 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); const actualOpenEvents = React.useMemo(() => { const events = openEvents ? { ...openEvents } : { mouseenter: true, focus: true, click: false, dblclick: false, mousedown: false, }; if (!openEvents && openOnClick) { Object.assign(events, { mouseenter: false, focus: false, click: true, }); } if (imperativeModeOnly) { Object.assign(events, { mouseenter: false, focus: false, click: false, dblclick: false, mousedown: false, }); } return events; }, [openEvents, openOnClick, imperativeModeOnly]); const actualCloseEvents = React.useMemo(() => { const events = closeEvents ? { ...closeEvents } : { mouseleave: true, blur: true, click: false, dblclick: false, mouseup: false, }; if (!closeEvents && openOnClick) { Object.assign(events, { mouseleave: false, blur: false, }); } if (imperativeModeOnly) { Object.assign(events, { mouseleave: false, blur: false, click: false, dblclick: false, mouseup: false, }); } return events; }, [closeEvents, openOnClick, imperativeModeOnly]); const actualGlobalCloseEvents = React.useMemo(() => { const events = globalCloseEvents ? { ...globalCloseEvents } : { escape: false, scroll: false, resize: false, clickOutsideAnchor: hasClickEvent || false, }; if (imperativeModeOnly) { Object.assign(events, { escape: false, scroll: false, resize: false, clickOutsideAnchor: false, }); } return events; }, [globalCloseEvents, hasClickEvent, imperativeModeOnly]); // --- Refs for values read inside event handlers (avoids effect deps) --- const activeAnchorRef = React.useRef(activeAnchor); activeAnchorRef.current = activeAnchor; const showRef = React.useRef(show); showRef.current = show; const anchorElementsRef = React.useRef(anchorElements); anchorElementsRef.current = anchorElements; const handleShowRef = React.useRef(handleShow); handleShowRef.current = handleShow; const handleTooltipPositionRef = React.useRef(handleTooltipPosition); handleTooltipPositionRef.current = handleTooltipPosition; const updateTooltipPositionRef = React.useRef(updateTooltipPosition); updateTooltipPositionRef.current = updateTooltipPosition; // --- Handler refs (updated every render, read via ref indirection in effects) --- const resolveAnchorElementRef = React.useRef(() => null); const handleShowTooltipRef = React.useRef(() => { }); const handleHideTooltipRef = React.useRef(() => { }); const dataTooltipId = anchorSelector ? parseDataTooltipIdSelector(anchorSelector) : null; resolveAnchorElementRef.current = (target) => { var _a, _b; if (!(target instanceof Element) || !target.isConnected) { return null; } const targetElement = target; if (dataTooltipId) { const matchedAnchor = resolveDataTooltipAnchor(targetElement, dataTooltipId); if (matchedAnchor && !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(matchedAnchor))) { return matchedAnchor; } } else if (anchorSelector) { try { const matchedAnchor = (_a = (targetElement.matches(anchorSelector) ? targetElement : targetElement.closest(anchorSelector))) !== null && _a !== void 0 ? _a : null; if (matchedAnchor && !(disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(matchedAnchor))) { return matchedAnchor; } } catch (_c) { return null; } } return ((_b = anchorElementsRef.current.find((anchor) => anchor === targetElement || anchor.contains(targetElement))) !== null && _b !== void 0 ? _b : null); }; handleShowTooltipRef.current = (anchor) => { if (!anchor) { return; } if (!anchor.isConnected) { setActiveAnchor(null); return; } if (disableTooltip === null || disableTooltip === void 0 ? void 0 : disableTooltip(anchor)) { return; } if (delayShow && activeAnchorRef.current && anchor !== activeAnchorRef.current) { // Moving to a different anchor while one is already active — defer the anchor // switch until the show delay fires to prevent content/position from updating // before visibility transitions complete. if (tooltipShowDelayTimerRef.current) { clearTimeout(tooltipShowDelayTimerRef.current); } tooltipShowDelayTimerRef.current = setTimeout(() => { setActiveAnchor(anchor); handleShow(true); }, delayShow); } else { setActiveAnchor(anchor); if (delayShow) { handleShowTooltipDelayed(); } else { handleShow(true); } } if (tooltipHideDelayTimerRef.current) { clearTimeout(tooltipHideDelayTimerRef.current); } }; handleHideTooltipRef.current = () => { if (clickable) { handleHideTooltipDelayed(delayHide || 100); } else if (delayHide) { handleHideTooltipDelayed(); } else { handleShow(false); } if (tooltipShowDelayTimerRef.current) { clearTimeout(tooltipShowDelayTimerRef.current); } }; // Update debounced callbacks to always delegate to latest handler refs const debouncedShow = debouncedShowRef.current; const debouncedHide = debouncedHideRef.current; debouncedShow.setCallback((anchor) => handleShowTooltipRef.current(anchor)); debouncedHide.setCallback(() => handleHideTooltipRef.current()); // --- Effect 1: Delegated anchor events + tooltip hover --- // Only re-runs when the set of active event types or interaction mode changes. // Handlers read reactive values (activeAnchor, show, etc.) from refs at invocation // time, so this effect is decoupled from show/hide state changes. React.useEffect(() => { const cleanupFns = []; const addDelegatedListener = (eventType, listener, options) => { cleanupFns.push(addDelegatedEventListener(eventType, listener, options)); }; const activeAnchorContainsTarget = (event) => { var _a; return Boolean((event === null || event === void 0 ? void 0 : event.target) instanceof Node && ((_a = activeAnchorRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target))); }; const debouncedHandleShowTooltip = (anchor) => { debouncedHide.cancel(); debouncedShow(anchor); }; const debouncedHandleHideTooltip = () => { debouncedShow.cancel(); debouncedHide(); }; const addDelegatedHoverOpenListener = () => { addDelegatedListener('mouseover', (event) => { const anchor = resolveAnchorElementRef.current(event.target); if (!anchor) { return; } const relatedAnchor = resolveAnchorElementRef.current(event.relatedTarget); if (relatedAnchor === anchor) { return; } debouncedHandleShowTooltip(anchor); }); }; const addDelegatedHoverCloseListener = () => { addDelegatedListener('mouseout', (event) => { const targetAnchor = resolveAnchorElementRef.current(event.target); if (!targetAnchor && !activeAnchorContainsTarget(event)) { return; } const relatedTarget = event.relatedTarget; const containerAnchor = targetAnchor || activeAnchorRef.current; if (relatedTarget instanceof Node && (containerAnchor === null || containerAnchor === void 0 ? void 0 : containerAnchor.contains(relatedTarget))) { return; } debouncedHandleHideTooltip(); }); }; if (actualOpenEvents.mouseenter) { addDelegatedHoverOpenListener(); } if (actualCloseEvents.mouseleave) { addDelegatedHoverCloseListener(); } if (actualOpenEvents.mouseover) { addDelegatedHoverOpenListener(); } if (actualCloseEvents.mouseout) { addDelegatedHoverCloseListener(); } if (actualOpenEvents.focus) { addDelegatedListener('focusin', (event) => { debouncedHandleShowTooltip(resolveAnchorElementRef.current(event.target)); }); } if (actualOpenEvents.mouseenter || actualOpenEvents.mouseover || actualOpenEvents.focus) { addDelegatedListener('touchstart', (event) => { debouncedHandleShowTooltip(resolveAnchorElementRef.current(event.target)); }); } if (actualCloseEvents.blur) { addDelegatedListener('focusout', (event) => { const targetAnchor = resolveAnchorElementRef.current(event.target); if (!targetAnchor && !activeAnchorContainsTarget(event)) { return; } const relatedTarget = event.relatedTarget; const containerAnchor = targetAnchor || activeAnchorRef.current; if (relatedTarget instanceof Node && (containerAnchor === null || containerAnchor === void 0 ? void 0 : containerAnchor.contains(relatedTarget))) { return; } debouncedHandleHideTooltip(); }); } const regularEvents = ['mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'focus', 'blur']; const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup']; const handleClickOpenTooltipAnchor = (event) => { var _a; const anchor = resolveAnchorElementRef.current((_a = event === null || event === void 0 ? void 0 : event.target) !== null && _a !== void 0 ? _a : null); if (!anchor) { return; } if (showRef.current && activeAnchorRef.current === anchor) { return; } handleShowTooltipRef.current(anchor); }; const handleClickCloseTooltipAnchor = (event) => { if (!showRef.current || !activeAnchorContainsTarget(event)) { return; } handleHideTooltipRef.current(); }; Object.entries(actualOpenEvents).forEach(([event, enabled]) => { if (!enabled || regularEvents.includes(event)) { return; } if (clickEvents.includes(event)) { addDelegatedListener(event, handleClickOpenTooltipAnchor, { capture: true, }); } }); Object.entries(actualCloseEvents).forEach(([event, enabled]) => { if (!enabled || regularEvents.includes(event)) { return; } if (clickEvents.includes(event)) { addDelegatedListener(event, handleClickCloseTooltipAnchor, { capture: true, }); } }); if (float) { addDelegatedListener('pointermove', (event) => { const currentActiveAnchor = activeAnchorRef.current; if (!currentActiveAnchor) { return; } const targetAnchor = resolveAnchorElementRef.current(event.target); if (targetAnchor !== currentActiveAnchor) { return; } const mouseEvent = event; const mousePosition = { x: mouseEvent.clientX, y: mouseEvent.clientY, }; handleTooltipPositionRef.current(mousePosition); lastFloatPosition.current = mousePosition; }); } const tooltipElement = tooltipRef.current; const handleMouseOverTooltip = () => { hoveringTooltip.current = true; }; const handleMouseOutTooltip = () => { hoveringTooltip.current = false; handleHideTooltipRef.current(); }; const addHoveringTooltipListeners = clickable && (actualCloseEvents.mouseout || actualCloseEvents.mouseleave); if (addHoveringTooltipListeners) { tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseover', handleMouseOverTooltip); tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.addEventListener('mouseout', handleMouseOutTooltip); } return () => { cleanupFns.forEach((fn) => fn()); if (addHoveringTooltipListeners) { tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseover', handleMouseOverTooltip); tooltipElement === null || tooltipElement === void 0 ? void 0 : tooltipElement.removeEventListener('mouseout', handleMouseOutTooltip); } debouncedShow.cancel(); debouncedHide.cancel(); }; // `rendered` needs to be a dependency because `tooltipRef` becomes stale when the // tooltip is removed from / added to the DOM. // eslint-disable-next-line react-hooks/exhaustive-deps }, [actualOpenEvents, actualCloseEvents, float, clickable, rendered]); // --- Effect 2: Global close events + auto-update --- // Re-runs when the global close config changes, or when the active anchor changes // (for scroll parent listeners and floating-ui autoUpdate). React.useEffect(() => { const handleScrollResize = () => { handleShowRef.current(false); clearTimeoutRef(tooltipShowDelayTimerRef); }; const tooltipScrollParent = tooltipScrollParentRef.current; const anchorScrollParent = anchorScrollParentRef.current; if (actualGlobalCloseEvents.scroll) { window.addEventListener('scroll', handleScrollResize); anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.addEventListener('scroll', handleScrollResize); tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.addEventListener('scroll', handleScrollResize); } let updateTooltipCleanup = null; if (actualGlobalCloseEvents.resize) { window.addEventListener('resize', handleScrollResize); } else if (activeAnchor && tooltipRef.current) { updateTooltipCleanup = dom.autoUpdate(activeAnchor, tooltipRef.current, () => updateTooltipPositionRef.current(), { ancestorResize: true, elementResize: true, layoutShift: true, }); } const handleEsc = (event) => { if (event.key !== 'Escape') { return; } handleShowRef.current(false); }; if (actualGlobalCloseEvents.escape) { window.addEventListener('keydown', handleEsc); } const handleClickOutsideAnchors = (event) => { var _a, _b; if (!showRef.current) { return; } const target = event.target; if (!(target instanceof Node) || !target.isConnected) { return; } if ((_a = tooltipRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) { return; } if ((_b = activeAnchorRef.current) === null || _b === void 0 ? void 0 : _b.contains(target)) { return; } if (anchorElementsRef.current.some((anchor) => anchor === null || anchor === void 0 ? void 0 : anchor.contains(target))) { return; } handleShowRef.current(false); clearTimeoutRef(tooltipShowDelayTimerRef); }; if (actualGlobalCloseEvents.clickOutsideAnchor) { window.addEventListener('click', handleClickOutsideAnchors); } return () => { if (actualGlobalCloseEvents.scroll) { window.removeEventListener('scroll', handleScrollResize); anchorScrollParent === null || anchorScrollParent === void 0 ? void 0 : anchorScrollParent.removeEventListener('scroll', handleScrollResize); tooltipScrollParent === null || tooltipScrollParent === void 0 ? void 0 : tooltipScrollParent.removeEventListener('scroll', handleScrollResize); } if (actualGlobalCloseEvents.resize) { window.removeEventListener('resize', handleScrollResize); } if (updateTooltipCleanup) { updateTooltipCleanup(); } if (actualGlobalCloseEvents.escape) { window.removeEventListener('keydown', handleEsc); } if (actualGlobalCloseEvents.clickOutsideAnchor) { window.removeEventListener('click', handleClickOutsideAnchors); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [actualGlobalCloseEvents, activeAnchor]); }; // Shared across all tooltip instances — the CSS variable is on :root and never changes per-instance let globalTransitionShowDelay = null; const Tooltip = ({ // props 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, // props handled by controller content, contentWrapperRef, isOpen, defaultIsOpen = false, setIsOpen, previousActiveAnchor, activeAnchor, setActiveAnchor, border, opacity, arrowColor, arrowSize = 8, role = 'tooltip', }) => { var _a; const tooltipRef = React.useRef(null); const tooltipArrowRef = React.useRef(null); const tooltipShowDelayTimerRef = React.useRef(null); const tooltipHideDelayTimerRef = React.useRef(null); const tooltipAutoCloseTimerRef = React.useRef(null); const missedTransitionTimerRef = React.useRef(null); const [computedPosition, setComputedPosition] = React.useState({ tooltipStyles: {}, tooltipArrowStyles: {}, place, }); const [show, setShow] = React.useState(false); const [rendered, setRendered] = React.useState(false); const [imperativeOptions, setImperativeOptions] = React.useState(null); const wasShowing = React.useRef(false); const lastFloatPosition = React.useRef(null); const hoveringTooltip = React.useRef(false); const mounted = React.useRef(false); const virtualElementRef = React.useRef({ getBoundingClientRect: () => ({ x: 0, y: 0, width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, }), }); /** * useLayoutEffect runs before useEffect, * but should be used carefully because of caveats * https://beta.reactjs.org/reference/react/useLayoutEffect#caveats */ useIsomorphicLayoutEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); const handleShow = React.useCallback((value) => { if (!mounted.current) { return; } if (value) { setRendered(true); } /** * wait for the component to render and calculate position * before actually showing */ setTimeout(() => { if (!mounted.current) { return; } setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(value); if (isOpen === undefined) { setShow(value); } }, 10); }, [isOpen, setIsOpen]); /** * Add aria-describedby to activeAnchor when tooltip is active */ React.useEffect(() => { if (!id) return