Files
session-viewer/src/client/components/Tooltip.tsx
teernisse 89ee0cb313 Add Tooltip component and show sensitive message count on auto-redact
Introduce a reusable Tooltip component with delayed hover reveal,
viewport-aware horizontal nudging, and smooth CSS entrance animation.
Supports top/bottom positioning via a data-side attribute.

FilterPanel now wraps the auto-redact checkbox in a Tooltip that
explains what auto-redaction detects. When sensitive messages exist
in the current view, a red pill badge displays the count next to
the label, giving users immediate visibility into how many messages
contain detectable secrets before toggling auto-redact on.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 13:35:23 -05:00

82 lines
2.3 KiB
TypeScript

import React, { useState, useRef, useCallback, useEffect } from "react";
interface Props {
content: React.ReactNode;
children: React.ReactElement;
delayMs?: number;
side?: "top" | "bottom";
}
export function Tooltip({ content, children, delayMs = 150, side = "top" }: Props) {
const [visible, setVisible] = useState(false);
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
const triggerRef = useRef<HTMLElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const show = useCallback(() => {
timerRef.current = setTimeout(() => setVisible(true), delayMs);
}, [delayMs]);
const hide = useCallback(() => {
clearTimeout(timerRef.current);
setVisible(false);
}, []);
// Clean up timer on unmount
useEffect(() => () => clearTimeout(timerRef.current), []);
// Recompute position when visible
useEffect(() => {
if (!visible || !triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
setPosition({
x: rect.left + rect.width / 2,
y: side === "top" ? rect.top : rect.bottom,
});
}, [visible, side]);
// Nudge tooltip horizontally if it overflows the viewport
useEffect(() => {
if (!visible || !tooltipRef.current || !position) return;
const el = tooltipRef.current;
const tooltipRect = el.getBoundingClientRect();
const pad = 8;
if (tooltipRect.left < pad) {
el.style.transform = `translateX(${pad - tooltipRect.left}px)`;
} else if (tooltipRect.right > window.innerWidth - pad) {
el.style.transform = `translateX(${window.innerWidth - pad - tooltipRect.right}px)`;
} else {
el.style.transform = "";
}
}, [visible, position]);
return (
<>
{React.cloneElement(children, {
ref: triggerRef,
onMouseEnter: show,
onMouseLeave: hide,
onFocus: show,
onBlur: hide,
})}
{visible && position && (
<div
ref={tooltipRef}
role="tooltip"
className="tooltip-popup"
style={{
position: "fixed",
left: position.x,
top: side === "top" ? position.y - 8 : position.y + 8,
zIndex: 9999,
}}
data-side={side}
>
{content}
</div>
)}
</>
);
}