react-tooltip
Version:
react tooltip component
1 lines • 129 kB
Source Map (JSON)
{"version":3,"file":"react-tooltip.mjs","sources":["../src/utils/handle-style.ts","../src/utils/compute-tooltip-position.ts","../src/utils/css-time-to-ms.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/utils/parse-data-tooltip-id-selector.ts","../src/utils/resolve-data-tooltip-anchor.ts","../src/components/Tooltip/anchor-registry.ts","../src/components/Tooltip/use-tooltip-anchors.tsx","../src/components/Tooltip/event-delegation.ts","../src/components/Tooltip/use-tooltip-events.tsx","../src/components/Tooltip/Tooltip.tsx","../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","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","/* 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","function parseDataTooltipIdSelector(selector: string) {\n const match = selector.match(/^\\[data-tooltip-id=(['\"])((?:\\\\.|(?!\\1).)*)\\1\\]$/)\n\n if (!match) {\n return null\n }\n\n return match[2].replace(/\\\\(['\"])/g, '$1')\n}\n\nexport default parseDataTooltipIdSelector\n","function resolveDataTooltipAnchor(targetElement: Element, tooltipId: string) {\n let currentElement: Element | null = targetElement\n\n while (currentElement) {\n const dataset = (currentElement as Element & { dataset?: DOMStringMap }).dataset\n if (dataset?.tooltipId === tooltipId) {\n return currentElement\n }\n currentElement = currentElement.parentElement\n }\n\n return null\n}\n\nexport default resolveDataTooltipAnchor\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 { 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 )\n }\n\n handleShowTooltipRef.current = (anchor: Element | null) => {\n if (!anchor) {\n return\n }\n if (!anchor.isConnected) {\n setActiveAnchor(null)\n return\n }\n if (disableTooltip?.(anchor)) {\n return\n }\n if (delayShow && activeAnchorRef.current && anchor !== activeAnchorRef.current) {\n // Moving to a different anchor while one is already active — defer the anchor\n // switch until the show delay fires to prevent content/position from updating\n // before visibility transitions complete.\n if (tooltipShowDelayTimerRef.current) {\n clearTimeout(tooltipShowDelayTimerRef.current)\n }\n tooltipShowDelayTimerRef.current = setTimeout(() => {\n setActiveAnchor(anchor)\n handleShow(true)\n }, delayShow)\n } else {\n setActiveAnchor(anchor)\n if (delayShow) {\n handleShowTooltipDelayed()\n } else {\n handleShow(true)\n }\n }\n\n if (tooltipHideDelayTimerRef.current) {\n clearTimeout(tooltipHideDelayTimerRef.current)\n }\n }\n\n handleHideTooltipRef.current = () => {\n if (clickable) {\n handleHideTooltipDelayed(delayHide || 100)\n } else if (delayHide) {\n handleHideTooltipDelayed()\n } else {\n handleShow(false)\n }\n\n if (tooltipShowDelayTimerRef.current) {\n clearTimeout(tooltipShowDelayTimerRef.current)\n }\n }\n\n // Update debounced callbacks to always delegate to latest handler refs\n const debouncedShow = debouncedShowRef.current\n const debouncedHide = debouncedHideRef.current\n debouncedShow.setCallback((anchor: Element | null) => handleShowTooltipRef.current(anchor))\n debouncedHide.setCallback(() => handleHideTooltipRef.current())\n\n // --- Effect 1: Delegated anchor events + tooltip hover ---\n // Only re-runs when the set of active event types or interaction mode changes.\n // Handlers read reactive values (activeAnchor, show, etc.) from refs at invocation\n // time, so this effect is decoupled from show/hide state changes.\n useEffect(() => {\n const cleanupFns: (() => void)[] = []\n\n const addDelegatedListener = (\n eventType: string,\n listener: (event: Event) => void,\n options?: AddEventListenerOptions,\n ) => {\n cleanupFns.push(addDelegatedEventListener(eventType, listener, options))\n }\n\n const activeAnchorContainsTarget = (event?: Event): boolean =>\n Boolean(event?.target instanceof Node && activeAnchorRef.current?.contains(event.target))\n\n const debouncedHandleShowTooltip = (anchor: Element | null) => {\n debouncedHide.cancel()\n debouncedShow(anchor)\n }\n const debouncedHandleHideTooltip = () => {\n debouncedShow.cancel()\n debouncedHide()\n }\n\n const addDelegatedHoverOpenListener = () => {\n addDelegatedListener('mouseover', (event) => {\n const anchor = resolveAnchorElementRef.current(event.target)\n if (!anchor) {\n return\n }\n const relatedAnchor = resolveAnchorElementRef.current((event as MouseEvent).relatedTarget)\n if (relatedAnchor === anchor) {\n return\n }\n debouncedHandleShowTooltip(anchor)\n })\n }\n\n const addDelegatedHoverCloseListener = () => {\n addDelegatedListener('mouseout', (event) => {\n const targetAnchor = resolveAnchorElementRef.current(event.target)\n if (!targetAnchor && !activeAnchorContainsTarget(event)) {\n return\n }\n const relatedTarget = (event as MouseEvent).relatedTarget\n const containerAnchor = targetAnchor || activeAnchorRef.current\n if (relatedTarget instanceof Node && containerAnchor?.contains(relatedTarget)) {\n return\n }\n debouncedHandleHideTooltip()\n })\n }\n\n if (actualOpenEvents.mouseenter) {\n addDelegatedHoverOpenListener()\n }\n if (actualCloseEvents.mouseleave) {\n addDelegatedHoverCloseListener()\n }\n if (actualOpenEvents.mouseover) {\n addDelegatedHoverOpenListener()\n }\n if (actualCloseEvents.mouseout) {\n addDelegatedHoverCloseListener()\n }\n if (actualOpenEvents.focus) {\n addDelegatedListener('focusin', (event) => {\n debouncedHandleShowTooltip(resolveAnchorElementRef.current(event.target))\n })\n }\n if (actualOpenEvents.mouseenter || actualOpenEvents.mouseover || actualOpenEvents.focus) {\n addDelegatedListener('touchstart', (event) => {\n debouncedHandleShowTooltip(resolveAnchorElementRef.current(event.target))\n })\n }\n if (actualCloseEvents.blur) {\n addDelegatedListener('focusout', (event) => {\n const targetAnchor = resolveAnchorElementRef.current(event.target)\n if (!targetAnchor && !activeAnchorContainsTarget(event)) {\n return\n }\n const relatedTarget = (event as FocusEvent).relatedTarget\n const containerAnchor = targetAnchor || activeAnchorRef.current\n if (relatedTarget instanceof Node && containerAnchor?.contains(relatedTarget)) {\n return\n }\n debouncedHandleHideTooltip()\n })\n }\n\n const regularEvents = ['mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'focus', 'blur']\n const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup']\n\n const handleClickOpenTooltipAnchor = (event?: Event) => {\n const anchor = resolveAnchorElementRef.current(event?.target ?? null)\n if (!anchor) {\n return\n }\n if (showRef.current && activeAnchorRef.current === anchor) {\n return\n }\n handleShowTooltipRef.current(anchor)\n }\n const handleClickCloseTooltipAnchor = (event?: Event) => {\n if (!showRef.current || !activeAnchorContainsTarget(event)) {\n return\n }\n handleHideTooltipRef.current()\n }\n\n Object.entries(actualOpenEvents).forEach(([event, enabled]) => {\n if (!enabled || regularEvents.includes(event)) {\n return\n }\n if (clickEvents.includes(event)) {\n addDelegatedListener(event, handleClickOpenTooltipAnchor as (event: Event) => void, {\n capture: true,\n })\n }\n })\n\n Object.entries(actualCloseEvents).forEach(([event, enabled]) => {\n if (!enabled || regularEvents.includes(event)) {\n return\n }\n if (clickEvents.includes(event)) {\n addDelegatedListener(event, handleClickCloseTooltipAnchor as (event: Event) => void, {\n capture: true,\n })\n }\n })\n\n if (float) {\n addDelegatedListener('pointermove', (event) => {\n const currentActiveAnchor = activeAnchorRef.current\n if (!currentActiveAnchor) {\n return\n }\n const targetAnchor = resolveAnchorElementRef.current(event.target)\n if (targetAnchor !== currentActiveAnchor) {\n return\n }\n const mouseEvent = event as MouseEvent\n const mousePosition = {\n x: mouseEvent.clientX,\n y: mouseEvent.clientY,\n }\n handleTooltipPositionRef.current(mousePosition)\n lastFloatPosition.current = mousePosition\n })\n }\n\n const tooltipElement = tooltipRef.current\n const handleMouseOverTooltip = () => {\n hoveringTooltip.current = true\n }\n const handleMouseOutTooltip = () => {\n hoveringTooltip.current = false\n handleHideTooltipRef.current()\n }\n\n const addHoveringTooltipListeners =\n clickable && (actualCloseEvents.mouseout || actualCloseEvents.mouseleave)\n if (addHoveringTooltipListeners) {\n tooltipElement?.addEventListener('mouseover', handleMouseOverTooltip)\n tooltipElement?.addEventListener('mouseout', handleMouseOutTooltip)\n }\n\n return () => {\n cleanupFns.forEach((fn) => fn())\n if (addHoveringTooltipListeners) {\n tooltipElement?.removeEventListener('mouseover', handleMouseOverTooltip)\n tooltipElement?.removeEventListener('mouseout', handleMouseOutTooltip)\n }\n debouncedShow.cancel()\n debouncedHide.cancel()\n }\n // `rendered` needs to be a dependency because `tooltipRef` becomes stale when the\n // tooltip is removed from / added to the DOM.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [actualOpenEvents, actualCloseEvents, float, clickable, rendered])\n\n // --- Effect 2: Global close events + auto-update ---\n // Re-runs when the global close config changes, or when the active anchor changes\n // (for scroll parent listeners and floating-ui autoUpdate).\n useEffect(() => {\n const handleScrollResize = () => {\n handleShowRef.current(false)\n clearTimeoutRef(tooltipShowDelayTimerRef)\n }\n\n const tooltipScrollParent = tooltipScrollParentRef.current\n const anchorScrollParent = anchorScrollParentRef.current\n\n if (actualGlobalCloseEvents.scroll) {\n window.addEventListener('scroll', handleScrollResize)\n anchorScrollParent?.addEventListener('scroll', handleScrollResize)\n tooltipScrollParent?.addEventListener('scroll', handleScrollResize)\n }\n let updateTooltipCleanup: null | (() => void) = null\n if (actualGlobalCloseEvents.resize) {\n window.addEventListener('resize', handleScrollResize)\n } else if (activeAnchor && tooltipRef.current) {\n updateTooltipCleanup = autoUpdate(\n activeAnchor as HTMLElement,\n tooltipRef.current as HTMLElement,\n () => updateTooltipPositionRef.current(),\n {\n ancestorResize: true,\n elementResize: true,\n layoutShift: true,\n },\n )\n }\n\n const handleEsc = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') {\n return\n }\n handleShowRef.current(false)\n }\n if (actualGlobalCloseEvents.escape) {\n window.addEventListener('keydown', handleEsc)\n }\n\n const handleClickOutsideAnchors = (event: Event) => {\n if (!showRef.current) {\n return\n }\n const target = (event as MouseEvent).target\n if (!(target instanceof Node) || !target.isConnected) {\n return\n }\n if (tooltipRef.current?.contains(target)) {\n return\n }\n if (activeAnchorRef.current?.contains(target)) {\n return\n }\n if (anchorElementsRef.current.some((anchor) => anchor?.contains(target))) {\n return\n }\n handleShowRef.current(false)\n clearTimeoutRef(tooltipShowDelayTimerRef)\n }\n\n if (actualGlobalCloseEvents.clickOutsideAnchor) {\n window.addEventListener('click', handleClickOutsideAnchors)\n }\n\n return () => {\n if (actualGlobalCloseEvents.scroll) {\n window.removeEventListener('scroll', handleScrollResize)\n anchorScrollParent?.removeEventListener('scroll', handleScrollResize)\n tooltipScrollParent?.removeEventListener('scroll', handleScrollResize)\n }\n if (actualGlobalCloseEvents.resize) {\n window.removeEventListener('resize', handleScrollResize)\n }\n if (updateTooltipCleanup) {\n updateTooltipCleanup()\n }\n if (actualGlobalCloseEvents.escape) {\n window.removeEventListener('keydown', handleEsc)\n }\n if (actualGlobalCloseEvents.clickOutsideAnchor) {\n window.removeEventListener('click', handleClickOutsideAnchors)\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [actualGlobalCloseEvents, activeAnchor])\n}\n\nexport default useTooltipEvents\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 oldComputedPositio