react-tooltip
Version:
react tooltip component
1 lines • 107 kB
Source Map (JSON)
{"version":3,"file":"react-tooltip.min.mjs","sources":["../src/utils/handle-style.ts","../src/utils/compute-tooltip-position.ts","../src/utils/debounce.ts","../src/utils/get-scroll-parent.ts","../src/utils/use-isomorphic-layout-effect.ts","../src/utils/clear-timeout-ref.ts","../src/components/Tooltip/anchor-registry.ts","../src/components/Tooltip/use-tooltip-anchors.tsx","../src/components/Tooltip/event-delegation.ts","../src/components/Tooltip/Tooltip.tsx","../src/utils/css-time-to-ms.ts","../src/components/Tooltip/use-tooltip-events.tsx","../src/utils/parse-data-tooltip-id-selector.ts","../src/utils/resolve-data-tooltip-anchor.ts","../src/components/TooltipController/shared-attribute-observer.ts","../src/components/TooltipController/TooltipController.tsx","../src/index.tsx"],"sourcesContent":["// This is the ID for the core styles of ReactTooltip\nconst REACT_TOOLTIP_CORE_STYLES_ID = 'react-tooltip-core-styles'\n// This is the ID for the visual styles of ReactTooltip\nconst REACT_TOOLTIP_BASE_STYLES_ID = 'react-tooltip-base-styles'\n\nconst injected = {\n core: false,\n base: false,\n}\n\n/**\n * Note about `state` parameter:\n * This parameter is used to keep track of the state of the styles\n * into the tests since the const `injected` is not acessible or resettable in the tests\n */\nfunction injectStyle({\n css,\n id = REACT_TOOLTIP_BASE_STYLES_ID,\n type = 'base',\n ref,\n state = {},\n}: {\n css: string\n id?: string\n type?: 'core' | 'base'\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ref?: any\n state?: { [key: string]: boolean }\n}) {\n if (\n !css ||\n typeof document === 'undefined' ||\n (typeof state[type] !== 'undefined' ? state[type] : injected[type])\n ) {\n return\n }\n\n if (\n type === 'core' &&\n typeof process !== 'undefined' && // this validation prevents docs from breaking even with `process?`\n process.env &&\n process.env.REACT_TOOLTIP_DISABLE_CORE_STYLES\n ) {\n return\n }\n\n if (\n type === 'base' &&\n typeof process !== 'undefined' && // this validation prevents docs from breaking even with `process?`\n process.env &&\n process.env.REACT_TOOLTIP_DISABLE_BASE_STYLES\n ) {\n return\n }\n\n if (type === 'core') {\n id = REACT_TOOLTIP_CORE_STYLES_ID\n }\n\n if (!ref) {\n ref = {}\n }\n const { insertAt } = ref\n\n if (document.getElementById(id)) {\n // this could happen in cases the tooltip is imported by multiple js modules\n return\n }\n\n const head = document.head || document.getElementsByTagName('head')[0]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const style: any = document.createElement('style')\n style.id = id\n style.type = 'text/css'\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n\n if (typeof state[type] !== 'undefined') {\n state[type] = true\n } else {\n injected[type] = true // internal global state that jest doesn't have access\n }\n}\n\nexport { injectStyle, injected }\n","import { computePosition, offset, shift, arrow, flip } from '@floating-ui/dom'\nimport type { IComputePositionArgs } from './compute-tooltip-position-types'\n\n// Hoisted constant middlewares — these configs never change\nconst defaultFlip = flip({ fallbackAxisSideDirection: 'start' })\nconst defaultShift = shift({ padding: 5 })\n\nconst computeTooltipPosition = async ({\n elementReference = null,\n tooltipReference = null,\n tooltipArrowReference = null,\n place = 'top',\n offset: offsetValue = 10,\n strategy = 'absolute',\n middlewares = [offset(Number(offsetValue)), defaultFlip, defaultShift],\n border,\n arrowSize = 8,\n}: IComputePositionArgs) => {\n if (!elementReference) {\n // elementReference can be null or undefined and we will not compute the position\n\n // console.error('The reference element for tooltip was not defined: ', elementReference)\n return { tooltipStyles: {}, tooltipArrowStyles: {}, place }\n }\n\n if (tooltipReference === null) {\n return { tooltipStyles: {}, tooltipArrowStyles: {}, place }\n }\n\n const middleware = [...middlewares]\n\n if (tooltipArrowReference) {\n middleware.push(arrow({ element: tooltipArrowReference as HTMLElement, padding: 5 }))\n\n return computePosition(elementReference as HTMLElement, tooltipReference as HTMLElement, {\n placement: place,\n strategy,\n middleware,\n }).then(({ x, y, placement, middlewareData }) => {\n const styles = { left: `${x}px`, top: `${y}px`, border }\n\n /* c8 ignore start */\n const { x: arrowX, y: arrowY } = middlewareData.arrow ?? { x: 0, y: 0 }\n\n const staticSide =\n {\n top: 'bottom',\n right: 'left',\n bottom: 'top',\n left: 'right',\n }[placement.split('-')[0]] ?? 'bottom'\n /* c8 ignore end */\n\n const borderSide = border && {\n borderBottom: border,\n borderRight: border,\n }\n\n let borderWidth = 0\n if (border) {\n const match = `${border}`.match(/(\\d+)px/)\n if (match?.[1]) {\n borderWidth = Number(match[1])\n } else {\n /**\n * this means `border` was set without `width`,\n * or non-px value (such as `medium`, `thick`, ...)\n */\n borderWidth = 1\n }\n }\n\n /* c8 ignore start */\n const arrowStyle = {\n left: arrowX != null ? `${arrowX}px` : '',\n top: arrowY != null ? `${arrowY}px` : '',\n right: '',\n bottom: '',\n ...borderSide,\n [staticSide]: `-${arrowSize / 2 + borderWidth - 1}px`,\n }\n /* c8 ignore end */\n\n return { tooltipStyles: styles, tooltipArrowStyles: arrowStyle, place: placement }\n })\n }\n\n return computePosition(elementReference as HTMLElement, tooltipReference as HTMLElement, {\n placement: 'bottom',\n strategy,\n middleware,\n }).then(({ x, y, placement }) => {\n const styles = { left: `${x}px`, top: `${y}px` }\n\n return { tooltipStyles: styles, tooltipArrowStyles: {}, place: placement }\n })\n}\n\nexport default computeTooltipPosition\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * This function debounce the received function\n * @param { function } \tfunc\t\t\t\tFunction to be debounced\n * @param { number } \t\twait\t\t\t\tTime to wait before execut the function\n * @param { boolean } \timmediate\t\tParam to define if the function will be executed immediately\n */\nconst debounce = <T, A extends any[]>(\n func: (...args: A) => void,\n wait?: number,\n immediate?: boolean,\n) => {\n let timeout: NodeJS.Timeout | null = null\n let currentFunc = func\n\n const debounced = function debounced(this: T, ...args: A): void {\n const later = () => {\n timeout = null\n if (!immediate) {\n currentFunc.apply(this, args)\n }\n }\n\n if (immediate && !timeout) {\n /**\n * there's no need to clear the timeout\n * since we expect it to resolve and set `timeout = null`\n */\n currentFunc.apply(this, args)\n timeout = setTimeout(later, wait)\n }\n\n if (!immediate) {\n if (timeout) {\n clearTimeout(timeout)\n }\n timeout = setTimeout(later, wait)\n }\n }\n\n debounced.cancel = () => {\n /* c8 ignore start */\n if (!timeout) {\n return\n }\n /* c8 ignore end */\n clearTimeout(timeout)\n timeout = null\n }\n\n debounced.setCallback = (newFunc: (...args: A) => void) => {\n currentFunc = newFunc\n }\n\n return debounced\n}\n\nexport default debounce\n","export const isScrollable = (node: Element) => {\n if (!(node instanceof HTMLElement || node instanceof SVGElement)) {\n return false\n }\n const style = getComputedStyle(node)\n return ['overflow', 'overflow-x', 'overflow-y'].some((propertyName) => {\n const value = style.getPropertyValue(propertyName)\n return value === 'auto' || value === 'scroll'\n })\n}\n\nconst getScrollParent = (node: Element | null) => {\n if (!node) {\n return null\n }\n let currentParent = node.parentElement\n while (currentParent) {\n if (isScrollable(currentParent)) {\n return currentParent\n }\n currentParent = currentParent.parentElement\n }\n return document.scrollingElement || document.documentElement\n}\n\nexport default getScrollParent\n","import { useLayoutEffect, useEffect } from 'react'\n\n// React currently throws a warning when using useLayoutEffect on the server.\n// To get around it, we can conditionally useEffect on the server (no-op) and\n// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store\n// subscription callback always has the selector from the latest render commit\n// available, otherwise a store update may happen between render and the effect,\n// which may cause missed updates; we also must ensure the store subscription\n// is created synchronously, otherwise a store update may occur before the\n// subscription is created and an inconsistent state may be observed\nconst isHopefullyDomEnvironment =\n typeof window !== 'undefined' &&\n typeof window.document !== 'undefined' &&\n typeof window.document.createElement !== 'undefined'\n\nconst useIsomorphicLayoutEffect = isHopefullyDomEnvironment ? useLayoutEffect : useEffect\n\nexport default useIsomorphicLayoutEffect\n","const clearTimeoutRef = (ref: React.RefObject<NodeJS.Timeout | null>) => {\n if (ref.current) {\n clearTimeout(ref.current)\n\n ref.current = null\n }\n}\n\nexport default clearTimeoutRef\n","type AnchorRegistrySubscriber = (anchors: Element[], error: Error | null) => void\n\ntype AnchorRegistryEntry = {\n anchors: Element[]\n error: Error | null\n subscribers: Set<AnchorRegistrySubscriber>\n /**\n * When the selector is a simple `[data-tooltip-id='value']` pattern,\n * this holds the extracted tooltip ID so we can skip expensive\n * querySelectorAll calls when the mutation doesn't affect it.\n */\n tooltipId: string | null\n}\n\nconst registry = new Map<string, AnchorRegistryEntry>()\n\nlet documentObserver: MutationObserver | null = null\n\n/**\n * Extract a tooltip ID from a simple `[data-tooltip-id='value']` selector.\n * Returns null for complex or custom selectors.\n */\nfunction extractTooltipId(selector: string): string | null {\n const match = selector.match(/^\\[data-tooltip-id=(['\"])((?:\\\\.|(?!\\1).)*)\\1\\]$/)\n return match ? match[2].replace(/\\\\(['\"])/g, '$1') : null\n}\n\nfunction areAnchorListsEqual(left: Element[], right: Element[]) {\n if (left.length !== right.length) {\n return false\n }\n\n return left.every((anchor, index) => anchor === right[index])\n}\n\nfunction readAnchorsForSelector(selector: string) {\n try {\n return {\n anchors: Array.from(document.querySelectorAll(selector)),\n error: null,\n }\n } catch (error) {\n return {\n anchors: [],\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n}\n\nfunction notifySubscribers(entry: AnchorRegistryEntry) {\n entry.subscribers.forEach((subscriber) => subscriber(entry.anchors, entry.error))\n}\n\nfunction refreshEntry(selector: string, entry: AnchorRegistryEntry) {\n const nextState = readAnchorsForSelector(selector)\n const nextErrorMessage = nextState.error?.message ?? null\n const previousErrorMessage = entry.error?.message ?? null\n\n if (\n areAnchorListsEqual(entry.anchors, nextState.anchors) &&\n nextErrorMessage === previousErrorMessage\n ) {\n return\n }\n\n const nextEntry = {\n ...entry,\n anchors: nextState.anchors,\n error: nextState.error,\n }\n\n registry.set(selector, nextEntry)\n notifySubscribers(nextEntry)\n}\n\nfunction refreshAllEntries() {\n registry.forEach((entry, selector) => {\n refreshEntry(selector, entry)\n })\n}\n\nlet refreshScheduled = false\nlet pendingTooltipIds: Set<string> | null = null\nlet pendingFullRefresh = false\n\nfunction scheduleRefresh(affectedTooltipIds: Set<string> | null) {\n if (affectedTooltipIds) {\n if (!pendingTooltipIds) {\n pendingTooltipIds = new Set()\n }\n affectedTooltipIds.forEach((id) => pendingTooltipIds!.add(id))\n } else {\n pendingFullRefresh = true\n }\n\n if (refreshScheduled) {\n return\n }\n refreshScheduled = true\n\n const flush = () => {\n refreshScheduled = false\n const fullRefresh = pendingFullRefresh\n const ids = pendingTooltipIds\n pendingFullRefresh = false\n pendingTooltipIds = null\n\n if (fullRefresh) {\n refreshAllEntries()\n } else if (ids && ids.size > 0) {\n refreshEntriesForTooltipIds(ids)\n }\n }\n\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(flush)\n } else {\n Promise.resolve().then(flush)\n }\n}\n\n/**\n * Only refresh entries whose tooltipId is in the affected set,\n * plus any entries with custom (non-tooltipId) selectors.\n */\nfunction refreshEntriesForTooltipIds(affectedIds: Set<string>) {\n registry.forEach((entry, selector) => {\n if (entry.tooltipId === null || affectedIds.has(entry.tooltipId)) {\n refreshEntry(selector, entry)\n }\n })\n}\n\n/**\n * Collect tooltip IDs from mutation records. Returns null when targeted\n * analysis is not worthwhile (few registry entries, or too many nodes to scan).\n */\nfunction collectAffectedTooltipIds(records: MutationRecord[]): Set<string> | null {\n // Targeted refresh only pays off when there are many distinct selectors.\n // With few entries, full refresh is already cheap — skip the analysis overhead.\n if (registry.size <= 4) {\n return null\n }\n\n const ids = new Set<string>()\n\n for (const record of records) {\n if (record.type === 'attributes') {\n const target = record.target as Element\n const currentId = target.getAttribute?.('data-tooltip-id')\n if (currentId) ids.add(currentId)\n if (record.oldValue) ids.add(record.oldValue)\n continue\n }\n\n if (record.type === 'childList') {\n const gatherIds = (nodes: NodeList) => {\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i]\n if (node.nodeType !== Node.ELEMENT_NODE) continue\n const el = node as Element\n const id = el.getAttribute?.('data-tooltip-id')\n if (id) ids.add(id)\n // For large subtrees, bail out to full refresh to avoid double-scanning\n const descendants = el.querySelectorAll?.('[data-tooltip-id]')\n if (descendants) {\n if (descendants.length > 50) {\n return true // signal bail-out\n }\n for (let j = 0; j < descendants.length; j++) {\n const descId = descendants[j].getAttribute('data-tooltip-id')\n if (descId) ids.add(descId)\n }\n }\n }\n return false\n }\n if (gatherIds(record.addedNodes) || gatherIds(record.removedNodes)) {\n return null // large mutation — full refresh is cheaper\n }\n continue\n }\n }\n\n return ids\n}\n\nfunction ensureDocumentObserver() {\n if (documentObserver || typeof MutationObserver === 'undefined') {\n return\n }\n\n documentObserver = new MutationObserver((records) => {\n const affectedIds = collectAffectedTooltipIds(records)\n scheduleRefresh(affectedIds)\n })\n\n documentObserver.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['data-tooltip-id'],\n attributeOldValue: true,\n })\n}\n\nfunction cleanupDocumentObserverIfUnused() {\n if (registry.size !== 0 || !documentObserver) {\n return\n }\n\n documentObserver.disconnect()\n documentObserver = null\n}\n\nexport function subscribeAnchorSelector(selector: string, subscriber: AnchorRegistrySubscriber) {\n let entry = registry.get(selector)\n\n if (!entry) {\n const initialState = readAnchorsForSelector(selector)\n entry = {\n anchors: initialState.anchors,\n error: initialState.error,\n subscribers: new Set(),\n tooltipId: extractTooltipId(selector),\n }\n registry.set(selector, entry)\n }\n\n entry.subscribers.add(subscriber)\n ensureDocumentObserver()\n subscriber([...entry.anchors], entry.error)\n\n return () => {\n const currentEntry = registry.get(selector)\n if (!currentEntry) {\n return\n }\n\n currentEntry.subscribers.delete(subscriber)\n if (currentEntry.subscribers.size === 0) {\n registry.delete(selector)\n }\n cleanupDocumentObserverIfUnused()\n }\n}\n\n/** @internal Reset module state between tests */\nexport function resetAnchorRegistry() {\n registry.clear()\n if (documentObserver) {\n documentObserver.disconnect()\n documentObserver = null\n }\n refreshScheduled = false\n pendingTooltipIds = null\n pendingFullRefresh = false\n}\n","import { useEffect, useMemo, useRef, useState } from 'react'\nimport { subscribeAnchorSelector } from './anchor-registry'\n\nconst getAnchorSelector = ({\n id,\n anchorSelect,\n imperativeAnchorSelect,\n}: {\n id?: string\n anchorSelect?: string\n imperativeAnchorSelect?: string\n}) => {\n let selector = imperativeAnchorSelect ?? anchorSelect ?? ''\n if (!selector && id) {\n selector = `[data-tooltip-id='${id.replace(/'/g, \"\\\\'\")}']`\n }\n return selector\n}\n\nconst useTooltipAnchors = ({\n id,\n anchorSelect,\n imperativeAnchorSelect,\n activeAnchor,\n disableTooltip,\n onActiveAnchorRemoved,\n trackAnchors,\n}: {\n id?: string\n anchorSelect?: string\n imperativeAnchorSelect?: string\n activeAnchor: Element | null\n disableTooltip?: (anchorRef: Element | null) => boolean\n onActiveAnchorRemoved: () => void\n trackAnchors: boolean\n}) => {\n const [rawAnchorElements, setRawAnchorElements] = useState<Element[]>([])\n const [selectorError, setSelectorError] = useState<Error | null>(null)\n const warnedSelectorRef = useRef<string | null>(null)\n const selector = useMemo(\n () => getAnchorSelector({ id, anchorSelect, imperativeAnchorSelect }),\n [id, anchorSelect, imperativeAnchorSelect],\n )\n const anchorElements = useMemo(\n () => rawAnchorElements.filter((anchor) => !disableTooltip?.(anchor)),\n [rawAnchorElements, disableTooltip],\n )\n\n const activeAnchorMatchesSelector = useMemo(() => {\n if (!activeAnchor || !selector) {\n return false\n }\n\n try {\n return activeAnchor.matches(selector)\n } catch {\n return false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [activeAnchor, selector, anchorElements])\n\n useEffect(() => {\n if (!selector || !trackAnchors) {\n setRawAnchorElements([])\n setSelectorError(null)\n return undefined\n }\n\n return subscribeAnchorSelector(selector, (anchors, error) => {\n setRawAnchorElements(anchors)\n setSelectorError(error)\n })\n }, [selector, trackAnchors])\n\n useEffect(() => {\n if (!selectorError || warnedSelectorRef.current === selector) {\n return\n }\n warnedSelectorRef.current = selector\n /* c8 ignore start */\n if (!process.env.NODE_ENV || process.env.NODE_ENV !== 'production') {\n console.warn(`[react-tooltip] \"${selector}\" is not a valid CSS selector`)\n }\n /* c8 ignore end */\n }, [selector, selectorError])\n\n useEffect(() => {\n if (!activeAnchor) {\n return\n }\n\n if (!activeAnchor.isConnected) {\n onActiveAnchorRemoved()\n return\n }\n\n if (!anchorElements.includes(activeAnchor) && !activeAnchorMatchesSelector) {\n onActiveAnchorRemoved()\n }\n }, [activeAnchor, anchorElements, activeAnchorMatchesSelector, onActiveAnchorRemoved])\n\n return {\n anchorElements,\n selector,\n }\n}\n\nexport default useTooltipAnchors\n","/**\n * Shared document event delegation.\n *\n * Instead of N tooltips each calling document.addEventListener(type, handler),\n * we maintain ONE document listener per event type. When the event fires,\n * we iterate through all registered handlers for that type.\n *\n * This reduces document-level listeners from O(N × eventTypes) to O(eventTypes).\n */\n\ntype Handler = (event: Event) => void\n\ntype DelegatedListener = {\n handlers: Set<Handler>\n dispatch: (event: Event) => void\n eventType: string\n capture: boolean\n}\n\nconst handlersByType = new Map<string, DelegatedListener>()\n\nfunction getListenerKey(eventType: string, capture: boolean): string {\n return `${eventType}:${capture ? 'capture' : 'bubble'}`\n}\n\nfunction getOrCreateListener(eventType: string, capture: boolean): DelegatedListener {\n const key = getListenerKey(eventType, capture)\n let listener = handlersByType.get(key)\n if (!listener) {\n const handlers = new Set<Handler>()\n const dispatch = (event: Event): void => {\n handlers.forEach((handler) => {\n handler(event)\n })\n }\n listener = { handlers, dispatch, eventType, capture }\n handlersByType.set(key, listener)\n document.addEventListener(eventType, dispatch, { capture })\n }\n return listener\n}\n\n/**\n * Register a handler for a document-level event type.\n * Returns an unsubscribe function.\n */\nexport function addDelegatedEventListener(\n eventType: string,\n handler: Handler,\n options: AddEventListenerOptions = {},\n): () => void {\n const capture = Boolean(options.capture)\n const key = getListenerKey(eventType, capture)\n const listener = getOrCreateListener(eventType, capture)\n listener.handlers.add(handler)\n\n return () => {\n listener.handlers.delete(handler)\n if (listener.handlers.size === 0) {\n handlersByType.delete(key)\n document.removeEventListener(eventType, listener.dispatch, { capture })\n }\n }\n}\n\n/**\n * Reset for testing purposes.\n */\nexport function resetEventDelegation(): void {\n handlersByType.forEach((listener) => {\n document.removeEventListener(listener.eventType, listener.dispatch, {\n capture: listener.capture,\n })\n })\n handlersByType.clear()\n}\n","import React, {\n useEffect,\n useMemo,\n useState,\n useRef,\n useCallback,\n useImperativeHandle,\n memo,\n} from 'react'\nimport { createPortal } from 'react-dom'\nimport clsx from 'clsx'\nimport {\n useIsomorphicLayoutEffect,\n computeTooltipPosition,\n cssTimeToMs,\n clearTimeoutRef,\n} from '../../utils'\nimport type { IComputedPosition } from '../../utils'\nimport coreStyles from './core-styles.module.css'\nimport styles from './styles.module.css'\nimport useTooltipAnchors from './use-tooltip-anchors'\nimport useTooltipEvents from './use-tooltip-events'\nimport type { IPosition, ITooltip, TooltipImperativeOpenOptions } from './TooltipTypes'\n\n// Shared across all tooltip instances — the CSS variable is on :root and never changes per-instance\nlet globalTransitionShowDelay: number | null = null\n\nconst Tooltip = ({\n // props\n forwardRef,\n id,\n className,\n classNameArrow,\n variant = 'dark',\n portalRoot,\n anchorSelect,\n place = 'top',\n offset = 10,\n openOnClick = false,\n positionStrategy = 'absolute',\n middlewares,\n wrapper: WrapperElement,\n delayShow = 0,\n delayHide = 0,\n autoClose,\n float = false,\n hidden = false,\n noArrow = false,\n clickable = false,\n openEvents,\n closeEvents,\n globalCloseEvents,\n imperativeModeOnly,\n style: externalStyles,\n position,\n afterShow,\n afterHide,\n disableTooltip,\n // props handled by controller\n content,\n contentWrapperRef,\n isOpen,\n defaultIsOpen = false,\n setIsOpen,\n previousActiveAnchor,\n activeAnchor,\n setActiveAnchor,\n border,\n opacity,\n arrowColor,\n arrowSize = 8,\n role = 'tooltip',\n}: ITooltip) => {\n const tooltipRef = useRef<HTMLElement>(null)\n const tooltipArrowRef = useRef<HTMLElement>(null)\n const tooltipShowDelayTimerRef = useRef<NodeJS.Timeout | null>(null)\n const tooltipHideDelayTimerRef = useRef<NodeJS.Timeout | null>(null)\n const tooltipAutoCloseTimerRef = useRef<NodeJS.Timeout | null>(null)\n const missedTransitionTimerRef = useRef<NodeJS.Timeout | null>(null)\n const [computedPosition, setComputedPosition] = useState<IComputedPosition>({\n tooltipStyles: {},\n tooltipArrowStyles: {},\n place,\n })\n const [show, setShow] = useState(false)\n const [rendered, setRendered] = useState(false)\n const [imperativeOptions, setImperativeOptions] = useState<TooltipImperativeOpenOptions | null>(\n null,\n )\n const wasShowing = useRef(false)\n const lastFloatPosition = useRef<IPosition | null>(null)\n const hoveringTooltip = useRef(false)\n const mounted = useRef(false)\n const virtualElementRef = useRef({\n getBoundingClientRect: () => ({\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n }),\n })\n\n /**\n * useLayoutEffect runs before useEffect,\n * but should be used carefully because of caveats\n * https://beta.reactjs.org/reference/react/useLayoutEffect#caveats\n */\n useIsomorphicLayoutEffect(() => {\n mounted.current = true\n return () => {\n mounted.current = false\n }\n }, [])\n\n const handleShow = useCallback(\n (value: boolean) => {\n if (!mounted.current) {\n return\n }\n if (value) {\n setRendered(true)\n }\n /**\n * wait for the component to render and calculate position\n * before actually showing\n */\n setTimeout(() => {\n if (!mounted.current) {\n return\n }\n setIsOpen?.(value)\n if (isOpen === undefined) {\n setShow(value)\n }\n }, 10)\n },\n [isOpen, setIsOpen],\n )\n\n /**\n * Add aria-describedby to activeAnchor when tooltip is active\n */\n useEffect(() => {\n if (!id) return\n\n function getAriaDescribedBy(element: Element | null) {\n return element?.getAttribute('aria-describedby')?.split(' ') || []\n }\n\n function removeAriaDescribedBy(element: Element | null) {\n const newDescribedBy = getAriaDescribedBy(element).filter((s) => s !== id)\n if (newDescribedBy.length) {\n element?.setAttribute('aria-describedby', newDescribedBy.join(' '))\n } else {\n element?.removeAttribute('aria-describedby')\n }\n }\n\n if (show) {\n removeAriaDescribedBy(previousActiveAnchor)\n const currentDescribedBy = getAriaDescribedBy(activeAnchor)\n const describedBy = [...new Set([...currentDescribedBy, id])].filter(Boolean).join(' ')\n activeAnchor?.setAttribute('aria-describedby', describedBy)\n } else {\n removeAriaDescribedBy(activeAnchor)\n }\n\n return () => {\n // cleanup aria-describedby when the tooltip is closed\n removeAriaDescribedBy(activeAnchor)\n removeAriaDescribedBy(previousActiveAnchor)\n }\n }, [activeAnchor, show, id, previousActiveAnchor])\n\n /**\n * this replicates the effect from `handleShow()`\n * when `isOpen` is changed from outside\n */\n useEffect(() => {\n if (isOpen === undefined) {\n return () => null\n }\n if (isOpen) {\n setRendered(true)\n }\n const timeout = setTimeout(() => {\n setShow(isOpen)\n }, 10)\n return () => {\n clearTimeout(timeout)\n }\n }, [isOpen])\n\n useEffect(() => {\n if (show === wasShowing.current) {\n return\n }\n clearTimeoutRef(missedTransitionTimerRef)\n wasShowing.current = show\n if (show) {\n afterShow?.()\n } else {\n /**\n * see `onTransitionEnd` on tooltip wrapper\n */\n if (globalTransitionShowDelay === null) {\n const style = getComputedStyle(document.body)\n globalTransitionShowDelay = cssTimeToMs(\n style.getPropertyValue('--rt-transition-show-delay'),\n )\n }\n const transitionShowDelay = globalTransitionShowDelay\n missedTransitionTimerRef.current = setTimeout(() => {\n /**\n * if the tooltip switches from `show === true` to `show === false` too fast\n * the transition never runs, so `onTransitionEnd` callback never gets fired\n */\n setRendered(false)\n setImperativeOptions(null)\n afterHide?.()\n // +25ms just to make sure `onTransitionEnd` (if it gets fired) has time to run\n }, transitionShowDelay + 25)\n }\n }, [afterHide, afterShow, show])\n\n useEffect(() => {\n clearTimeoutRef(tooltipAutoCloseTimerRef)\n\n if (!show || !autoClose || autoClose <= 0) {\n return () => {\n clearTimeoutRef(tooltipAutoCloseTimerRef)\n }\n }\n\n tooltipAutoCloseTimerRef.current = setTimeout(() => {\n handleShow(false)\n }, autoClose)\n\n return () => {\n clearTimeoutRef(tooltipAutoCloseTimerRef)\n }\n }, [activeAnchor, autoClose, handleShow, show])\n\n const handleComputedPosition = useCallback((newComputedPosition: IComputedPosition) => {\n if (!mounted.current) {\n return\n }\n setComputedPosition((oldComputedPosition) => {\n if (\n oldComputedPosition.place === newComputedPosition.place &&\n oldComputedPosition.tooltipStyles.left === newComputedPosition.tooltipStyles.left &&\n oldComputedPosition.tooltipStyles.top === newComputedPosition.tooltipStyles.top &&\n oldComputedPosition.tooltipStyles.border === newComputedPosition.tooltipStyles.border &&\n oldComputedPosition.tooltipArrowStyles.left ===\n newComputedPosition.tooltipArrowStyles.left &&\n oldComputedPosition.tooltipArrowStyles.top === newComputedPosition.tooltipArrowStyles.top &&\n oldComputedPosition.tooltipArrowStyles.right ===\n newComputedPosition.tooltipArrowStyles.right &&\n oldComputedPosition.tooltipArrowStyles.bottom ===\n newComputedPosition.tooltipArrowStyles.bottom &&\n oldComputedPosition.tooltipArrowStyles.borderBottom ===\n newComputedPosition.tooltipArrowStyles.borderBottom &&\n oldComputedPosition.tooltipArrowStyles.borderRight ===\n newComputedPosition.tooltipArrowStyles.borderRight\n ) {\n return oldComputedPosition\n }\n return newComputedPosition\n })\n }, [])\n\n const renderedRef = useRef(rendered)\n renderedRef.current = rendered\n\n const handleShowTooltipDelayed = useCallback(\n (delay = delayShow) => {\n if (tooltipShowDelayTimerRef.current) {\n clearTimeout(tooltipShowDelayTimerRef.current)\n }\n\n if (renderedRef.current) {\n // if the tooltip is already rendered, ignore delay\n handleShow(true)\n return\n }\n\n tooltipShowDelayTimerRef.current = setTimeout(() => {\n handleShow(true)\n }, delay)\n },\n [delayShow, handleShow],\n )\n\n const handleHideTooltipDelayed = useCallback(\n (delay = delayHide) => {\n if (tooltipHideDelayTimerRef.current) {\n clearTimeout(tooltipHideDelayTimerRef.current)\n }\n\n tooltipHideDelayTimerRef.current = setTimeout(() => {\n if (hoveringTooltip.current) {\n return\n }\n handleShow(false)\n }, delay)\n },\n [delayHide, handleShow],\n )\n\n const handleTooltipPosition = useCallback(\n ({ x, y }: IPosition) => {\n virtualElementRef.current.getBoundingClientRect = () => ({\n x,\n y,\n width: 0,\n height: 0,\n top: y,\n left: x,\n right: x,\n bottom: y,\n })\n computeTooltipPosition({\n place: imperativeOptions?.place ?? place,\n offset,\n elementReference: virtualElementRef.current as unknown as Element,\n tooltipReference: tooltipRef.current,\n tooltipArrowReference: tooltipArrowRef.current,\n strategy: positionStrategy,\n middlewares,\n border,\n arrowSize,\n }).then((computedStylesData) => {\n handleComputedPosition(computedStylesData)\n })\n },\n [\n imperativeOptions?.place,\n place,\n offset,\n positionStrategy,\n middlewares,\n border,\n arrowSize,\n handleComputedPosition,\n ],\n )\n\n const updateTooltipPosition = useCallback(() => {\n const actualPosition = imperativeOptions?.position ?? position\n if (actualPosition) {\n // if `position` is set, override regular and `float` positioning\n handleTooltipPosition(actualPosition)\n return\n }\n\n if (float) {\n if (lastFloatPosition.current) {\n /*\n Without this, changes to `content`, `place`, `offset`, ..., will only\n trigger a position calculation after a `mousemove` event.\n\n To see why this matters, comment this line, run `yarn dev` and click the\n \"Hover me!\" anchor.\n */\n handleTooltipPosition(lastFloatPosition.current)\n }\n // if `float` is set, override regular positioning\n return\n }\n\n if (!activeAnchor?.isConnected) {\n return\n }\n\n computeTooltipPosition({\n place: imperativeOptions?.place ?? place,\n offset,\n elementReference: activeAnchor,\n tooltipReference: tooltipRef.current,\n tooltipArrowReference: tooltipArrowRef.current,\n strategy: positionStrategy,\n middlewares,\n border,\n arrowSize,\n }).then((computedStylesData) => {\n if (!mounted.current) {\n // invalidate computed positions after remount\n return\n }\n handleComputedPosition(computedStylesData)\n })\n }, [\n imperativeOptions?.position,\n imperativeOptions?.place,\n position,\n float,\n activeAnchor,\n place,\n offset,\n positionStrategy,\n middlewares,\n border,\n handleTooltipPosition,\n handleComputedPosition,\n arrowSize,\n ])\n\n const handleActiveAnchorRemoved = useCallback(() => {\n setRendered(false)\n handleShow(false)\n setActiveAnchor(null)\n clearTimeoutRef(tooltipShowDelayTimerRef)\n clearTimeoutRef(tooltipHideDelayTimerRef)\n clearTimeoutRef(tooltipAutoCloseTimerRef)\n }, [handleShow, setActiveAnchor])\n\n const shouldTrackAnchors =\n rendered ||\n defaultIsOpen ||\n Boolean(isOpen) ||\n Boolean(activeAnchor) ||\n Boolean(imperativeOptions?.anchorSelect)\n\n const { anchorElements, selector: anchorSelector } = useTooltipAnchors({\n id,\n anchorSelect,\n imperativeAnchorSelect: imperativeOptions?.anchorSelect,\n activeAnchor,\n disableTooltip,\n onActiveAnchorRemoved: handleActiveAnchorRemoved,\n trackAnchors: shouldTrackAnchors,\n })\n\n useTooltipEvents({\n activeAnchor,\n anchorElements,\n anchorSelector,\n clickable,\n closeEvents,\n delayHide,\n delayShow,\n disableTooltip,\n float,\n globalCloseEvents,\n handleHideTooltipDelayed,\n handleShow,\n handleShowTooltipDelayed,\n handleTooltipPosition,\n hoveringTooltip,\n imperativeModeOnly,\n lastFloatPosition,\n openEvents,\n openOnClick,\n rendered,\n setActiveAnchor,\n show,\n tooltipHideDelayTimerRef,\n tooltipRef,\n tooltipShowDelayTimerRef,\n updateTooltipPosition,\n })\n\n const updateTooltipPositionRef = useRef(updateTooltipPosition)\n updateTooltipPositionRef.current = updateTooltipPosition\n\n useEffect(() => {\n if (!rendered) {\n return\n }\n updateTooltipPosition()\n }, [rendered, updateTooltipPosition])\n\n useEffect(() => {\n if (!rendered || !contentWrapperRef?.current) {\n return () => null\n }\n\n let timeoutId: NodeJS.Timeout | null = null\n const contentObserver = new ResizeObserver(() => {\n // Clear any existing timeout to prevent memory leaks\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n timeoutId = setTimeout(() => {\n if (mounted.current) {\n updateTooltipPositionRef.current()\n }\n timeoutId = null\n }, 0)\n })\n contentObserver.observe(contentWrapperRef.current)\n\n return () => {\n contentObserver.disconnect()\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n }\n }, [content, contentWrapperRef, rendered])\n\n useEffect(() => {\n const shouldResolveInitialActiveAnchor = defaultIsOpen || Boolean(isOpen)\n\n if (!shouldResolveInitialActiveAnchor) {\n return\n }\n\n const activeAnchorMatchesImperativeSelector = (() => {\n if (!activeAnchor || !imperativeOptions?.anchorSelect) {\n return false\n }\n\n try {\n return activeAnchor.matches(imperativeOptions.anchorSelect)\n } catch {\n return false\n }\n })()\n\n if (!activeAnchor || !anchorElements.includes(activeAnchor)) {\n /**\n * if there is no active anchor,\n * or if the current active anchor is not amongst the allowed ones,\n * reset it\n */\n if (activeAnchorMatchesImperativeSelector) {\n return\n }\n setActiveAnchor(anchorElements[0] ?? null)\n }\n }, [\n activeAnchor,\n anchorElements,\n defaultIsOpen,\n imperativeOptions?.anchorSelect,\n isOpen,\n rendered,\n setActiveAnchor,\n ])\n\n useEffect(() => {\n if (defaultIsOpen) {\n handleShow(true)\n }\n return () => {\n clearTimeoutRef(tooltipShowDelayTimerRef)\n clearTimeoutRef(tooltipHideDelayTimerRef)\n clearTimeoutRef(tooltipAutoCloseTimerRef)\n clearTimeoutRef(missedTransitionTimerRef)\n }\n }, [defaultIsOpen, handleShow])\n\n useEffect(() => {\n if (tooltipShowDelayTimerRef.current) {\n /**\n * if the delay changes while the tooltip is waiting to show,\n * reset the timer with the new delay\n */\n clearTimeoutRef(tooltipShowDelayTimerRef)\n handleShowTooltipDelayed(delayShow)\n }\n }, [delayShow, handleShowTooltipDelayed])\n\n const actualContent = imperativeOptions?.content ?? content\n const hasContent = actualContent !== null && actualContent !== undefined\n const canShow = show && computedPosition.tooltipStyles.left !== undefined\n\n const tooltipStyle = useMemo(\n () => ({\n ...externalStyles,\n ...computedPosition.tooltipStyles,\n opacity: opacity !== undefined && canShow ? opacity : undefined,\n }),\n [externalStyles, computedPosition.tooltipStyles, opacity, canShow],\n )\n\n const arrowBackground = useMemo(\n () =>\n arrowColor\n ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)`\n : undefined,\n [arrowColor],\n )\n\n const arrowStyle = useMemo(\n () => ({\n ...computedPosition.tooltipArrowStyles,\n background: arrowBackground,\n '--rt-arrow-size': `${arrowSize}px`,\n }),\n [computedPosition.tooltipArrowStyles, arrowBackground, arrowSize],\n )\n\n useImperativeHandle(forwardRef, () => ({\n open: (options) => {\n let imperativeAnchor: Element | null = null\n if (options?.anchorSelect) {\n try {\n imperativeAnchor = document.querySelector(options.anchorSelect)\n } catch {\n if (!process.env.NODE_ENV || process.env.NODE_ENV !== 'production') {\n console.warn(`[react-tooltip] \"${options.anchorSelect}\" is not a valid CSS selector`)\n }\n return\n }\n if (!imperativeAnchor) {\n return\n }\n }\n if (imperativeAnchor) {\n setActiveAnchor(imperativeAnchor)\n }\n setImperativeOptions(options ?? null)\n if (options?.delay) {\n handleShowTooltipDelayed(options.delay)\n } else {\n handleShow(true)\n }\n },\n close: (options) => {\n if (options?.delay) {\n handleHideTooltipDelayed(options.delay)\n } else {\n handleShow(false)\n }\n },\n activeAnchor,\n place: computedPosition.place,\n isOpen: Boolean(rendered && !hidden && hasContent && canShow),\n }))\n\n useEffect(() => {\n return () => {\n // Final cleanup to ensure no memory leaks\n clearTimeoutRef(tooltipShowDelayTimerRef)\n clearTimeoutRef(tooltipHideDelayTimerRef)\n clearTimeoutRef(tooltipAutoCloseTimerRef)\n clearTimeoutRef(missedTransitionTimerRef)\n }\n }, [])\n\n const tooltipNode =\n rendered && !hidden && hasContent ? (\n <WrapperElement\n id={id}\n role={role}\n className={clsx(\n 'react-tooltip',\n coreStyles['tooltip'],\n styles['tooltip'],\n styles[variant],\n className,\n `react-tooltip__place-${computedPosition.place}`,\n coreStyles[canShow ? 'show' : 'closing'],\n canShow ? 'react-tooltip__show' : 'react-tooltip__closing',\n positionStrategy === 'fixed' && coreStyles['fixed'],\n clickable && coreStyles['clickable'],\n )}\n onTransitionEnd={(event: TransitionEvent) => {\n clearTimeoutRef(missedTransitionTimerRef)\n if (show || event.propertyName !== 'opacity') {\n return\n }\n setRendered(false)\n setImperativeOptions(null)\n afterHide?.()\n }}\n style={tooltipStyle}\n ref={tooltipRef}\n >\n <WrapperElement\n className={clsx(\n 'react-tooltip-content-wrapper',\n coreStyles['content'],\n styles['content'],\n )}\n >\n {actualContent}\n </WrapperElement>\n <WrapperElement\n className={clsx(\n 'react-tooltip-arrow',\n coreStyles['arrow'],\n styles['arrow'],\n classNameArrow,\n noArrow && coreStyles['noArrow'],\n )}\n style={arrowStyle}\n ref={tooltipArrowRef}\n />\n </WrapperElement>\n ) : null\n\n if (!tooltipNode) {\n return null\n }\n\n if (portalRoot) {\n return createPortal(tooltipNode, portalRoot)\n }\n\n return tooltipNode\n}\n\nexport default memo(Tooltip)\n","const cssTimeToMs = (time: string): number => {\n const match = time.match(/^([\\d.]+)(m?s)$/)\n if (!match) {\n return 0\n }\n const [, amount, unit] = match\n return Number(amount) * (unit === 'ms' ? 1 : 1000)\n}\n\nexport default cssTimeToMs\n","import { useEffect, useMemo, useRef } from 'react'\nimport type { RefObject } from 'react'\nimport { autoUpdate } from '@floating-ui/dom'\nimport {\n debounce,\n getScrollParent,\n clearTimeoutRef,\n parseDataTooltipIdSelector,\n resolveDataTooltipAnchor,\n} from '../../utils'\nimport type {\n AnchorCloseEvents,\n AnchorOpenEvents,\n GlobalCloseEvents,\n IPosition,\n} from './TooltipTypes'\nimport { addDelegatedEventListener } from './event-delegation'\n\nconst useTooltipEvents = ({\n activeAnchor,\n anchorElements,\n anchorSelector,\n clickable,\n closeEvents,\n delayHide,\n delayShow,\n disableTooltip,\n float,\n globalCloseEvents,\n handleHideTooltipDelayed,\n handleShow,\n handleShowTooltipDelayed,\n handleTooltipPosition,\n hoveringTooltip,\n imperativeModeOnly,\n lastFloatPosition,\n openEvents,\n openOnClick,\n rendered,\n setActiveAnchor,\n show,\n tooltipHideDelayTimerRef,\n tooltipRef,\n tooltipShowDelayTimerRef,\n updateTooltipPosition,\n}: {\n activeAnchor: Element | null\n anchorElements: Element[]\n anchorSelector: string\n clickable: boolean\n closeEvents?: AnchorCloseEvents\n delayHide: number\n delayShow: number\n disableTooltip?: (anchorRef: Element | null) => boolean\n float: boolean\n globalCloseEvents?: GlobalCloseEvents\n handleHideTooltipDelayed: (delay?: number) => void\n handleShow: (value: boolean) => void\n handleShowTooltipDelayed: (delay?: number) => void\n handleTooltipPosition: ({ x, y }: IPosition) => void\n hoveringTooltip: RefObject<boolean>\n imperativeModeOnly?: boolean\n lastFloatPosition: RefObject<IPosition | null>\n openEvents?: AnchorOpenEvents\n openOnClick: boolean\n rendered: boolean\n setActiveAnchor: (anchor: Element | null) => void\n show: boolean\n tooltipHideDelayTimerRef: RefObject<NodeJS.Timeout | null>\n tooltipRef: RefObject<HTMLElement | null>\n tooltipShowDelayTimerRef: RefObject<NodeJS.Timeout | null>\n updateTooltipPosition: () => void\n}) => {\n // Ref-stable debounced handlers — avoids recreating debounce instances on every effect run\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const debouncedShowRef = useRef(debounce((_anchor: Element | null) => {}, 50, true))\n const debouncedHideRef = useRef(debounce(() => {}, 50, true))\n\n // Cache scroll parents — only recompute when the element actually changes\n const anchorScrollParentRef = useRef<Element | null>(null)\n const tooltipScrollParentRef = useRef<Element | null>(null)\n const prevAnchorRef = useRef<Element | null>(null)\n const prevTooltipRef = useRef<HTMLElement | null>(null)\n\n if (activeAnchor !== prevAnchorRef.current) {\n prevAnchorRef.current = activeAnchor\n anchorScrollParentRef.current = getScrollParent(activeAnchor)\n }\n const currentTooltipEl = tooltipRef.current\n if (currentTooltipEl !== prevTooltipRef.current) {\n prevTooltipRef.current = currentTooltipEl\n tooltipScrollParentRef.current = getScrollParent(currentTooltipEl)\n }\n\n // Memoize event config objects — only rebuild when the relevant props change\n const hasClickEvent =\n openOnClick || openEvents?.click || openEvents?.dblclick || openEvents?.mousedown\n const actualOpenEvents: AnchorOpenEvents = useMemo(() => {\n const events: AnchorOpenEvents = openEvents\n ? { ...openEvents }\n : {\n mouseenter: true,\n focus: true,\n click: false,\n dblclick: false,\n mousedown: false,\n }\n if (!openEvents && openOnClick) {\n Object.assign(events, {\n mouseenter: false,\n focus: false,\n click: true,\n })\n }\n if (imperativeModeOnly) {\n Object.assign(events, {\n mouseenter: false,\n focus: false,\n click: false,\n dblclick: false,\n mousedown: false,\n })\n }\n return events\n }, [openEvents, openOnClick, imperativeModeOnly])\n\n const actualCloseEvents: AnchorCloseEvents = useMemo(() => {\n const events: AnchorCloseEvents = closeEvents\n ? { ...closeEvents }\n : {\n mouseleave: true,\n blur: true,\n click: false,\n dblclick: false,\n mouseup: false,\n }\n if (!closeEvents && openOnClick) {\n Object.assign(events, {\n mouseleave: false,\n blur: false,\n })\n }\n if (imperativeModeOnly) {\n Object.assign(events, {\n mouseleave: false,\n blur: false,\n click: false,\n dblclick: false,\n mouseup: false,\n })\n }\n return events\n }, [closeEvents, openOnClick, imperativeModeOnly])\n\n const actualGlobalCloseEvents: GlobalCloseEvents = useMemo(() => {\n const events: GlobalCloseEvents = globalCloseEvents\n ? { ...globalCloseEvents }\n : {\n escape: false,\n scroll: false,\n resize: false,\n clickOutsideAnchor: hasClickEvent || false,\n }\n if (imperativeModeOnly) {\n Object.assign(events, {\n escape: false,\n scroll: false,\n resize: false,\n clickOutsideAnchor: false,\n })\n }\n return events\n }, [globalCloseEvents, hasClickEvent, imperativeModeOnly])\n\n // --- Refs for values read inside event handlers (avoids effect deps) ---\n const activeAnchorRef = useRef(activeAnchor)\n activeAnchorRef.current = activeAnchor\n const showRef = useRef(show)\n showRef.current = show\n const anchorElementsRef = useRef(anchorElements)\n anchorElementsRef.current = anchorElements\n const handleShowRef = useRef(handleShow)\n handleShowRef.current = handleShow\n const handleTooltipPositionRef = useRef(handleTooltipPosition)\n handleTooltipPositionRef.current = handleTooltipPosition\n const updateTooltipPositionRef = useRef(updateTooltipPosition)\n updateTooltipPositionRef.current = updateTooltipPosition\n\n // --- Handler refs (updated every render, read via ref indirection in effects) ---\n const resolveAnchorElementRef = useRef<(target: EventTarget | null) => Element | null>(() => null)\n const handleShowTooltipRef = useRef<(anchor: Element | null) => void>(() => {})\n const handleHideTooltipRef = useRef<() => void>(() => {})\n\n const dataTooltipId = anchorSelector ? parseDataTooltipIdSelector(anchorSelector) : null\n\n resolveAnchorElementRef.current = (target: EventTarget | null) => {\n if (!(target instanceof Element) || !target.isConnected) {\n return null\n }\n\n const targetElement = target\n\n if (dataTooltipId) {\n const matchedAnchor = resolveDataTooltipAnchor(targetElement, dataTooltipId)\n\n if (matchedAnchor && !disableTooltip?.(matchedAnchor)) {\n return matchedAnchor\n }\n } else if (anchorSelector) {\n try {\n const matchedAnchor =\n (targetElement.matches(anchorSelector)\n ? targetElement\n : targetElement.closest(anchorSelector)) ?? null\n\n if (matchedAnchor && !disableTooltip?.(matchedAnchor)) {\n return matchedAnchor\n }\n } catch {\n return null\n }\n }\n\n return (\n anchorElementsRef.current.find(\n (anchor) => anchor === targetElement || anchor.contains(targetElement),\n ) ?? null\n