// Copyright 2022 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import React, {memo, useEffect, useRef, useState} from 'react'; import GraphTooltipArrow from '../../GraphTooltipArrow'; import GraphTooltipArrowContent from '../../GraphTooltipArrow/Content'; import {DockedGraphTooltip} from '../../GraphTooltipArrow/DockedGraphTooltip'; import {useTooltipContext} from './TooltipContext'; interface MemoizedTooltipProps { contextElement: Element | null; dockedMetainfo: boolean; } export const MemoizedTooltip = memo(function MemoizedTooltip({ contextElement, dockedMetainfo, }: MemoizedTooltipProps): React.JSX.Element | null { const [tooltipRow, setTooltipRow] = useState(null); const [shiftHeld, setShiftHeld] = useState(false); const shiftHeldRef = useRef(false); useEffect(() => { shiftHeldRef.current = shiftHeld; }, [shiftHeld]); const {table, total, totalUnfiltered, profileType, unit, compareAbsolute, tooltipId} = useTooltipContext(); useEffect(() => { const onKeyDown = (e: KeyboardEvent): void => { if (e.key === 'Shift') setShiftHeld(true); }; const onKeyUp = (e: KeyboardEvent): void => { if (e.key === 'Shift') setShiftHeld(false); }; // If the user opens the attribution link, releases Shift in the new tab, // and comes back — our window never saw the keyup, so the frozen state // would stick. Clear it whenever focus leaves the tab. const onBlurOrHide = (): void => setShiftHeld(false); window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); window.addEventListener('blur', onBlurOrHide); document.addEventListener('visibilitychange', onBlurOrHide); return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); window.removeEventListener('blur', onBlurOrHide); document.removeEventListener('visibilitychange', onBlurOrHide); }; }, []); // This component subscribes to tooltip updates through a callback // passed to the TooltipProvider, avoiding the need to lift state useEffect(() => { const handleTooltipUpdate = (event: CustomEvent<{row: number | null}>): void => { // While Shift is held, lock the tooltip to its original frame. if (shiftHeldRef.current) return; setTooltipRow(event.detail.row); }; const eventName = `flame-tooltip-update-${tooltipId}`; // Delay to ensure all DOM updates and React renders are complete // This fixes the race condition in sandwich view const timeoutId = setTimeout(() => { window.addEventListener(eventName as any, handleTooltipUpdate as any); }, 200); return () => { clearTimeout(timeoutId); window.removeEventListener(eventName as any, handleTooltipUpdate as any); }; }, [tooltipId, table]); // Re-render when contextElement becomes available (fixes sandwich view timing issue) useEffect(() => { // Force re-render when contextElement transitions from null to valid element // This ensures tooltips work immediately in sandwich view }, [contextElement, tooltipId]); if (dockedMetainfo) { return ( ); } if (tooltipRow === null) { return null; } // Fix for sandwich view tooltip issue: Don't render tooltip if contextElement is null // This happens when the SVG ref isn't ready yet during initial sandwich view render if (contextElement === null) { return null; } return ( ); });