Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/junior-dashboard/e2e/conversations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ test("opens a conversation in the built dashboard", async ({ page }) => {
await expect(
page.getByRole("heading", { name: "Checkout latency triage" }),
).toBeVisible();

const costMetric = page
.getByRole("main")
.getByText("$0.03", { exact: true })
.first();
await costMetric.hover();
const costTooltip = page.getByRole("tooltip");
await expect(costTooltip).toBeVisible();
await costTooltip
.getByText("$0.0332", { exact: true })
.click({ clickCount: 3 });
await page.waitForTimeout(200);
await expect(costTooltip).toBeVisible();
expect(
await page.evaluate(() => window.getSelection()?.toString()),
).toContain("$0.0332");

const containerBounds = () =>
page.locator("main > div").evaluate((element) => {
const bounds = element.getBoundingClientRect();
Expand Down
75 changes: 66 additions & 9 deletions packages/junior-dashboard/src/client/components/Metric.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
useEffect,
useId,
useRef,
useState,
Expand Down Expand Up @@ -26,6 +27,8 @@ type TooltipPosition = {
width: number;
};

const TOOLTIP_CLOSE_DELAY_MS = 150;

function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
Expand Down Expand Up @@ -105,16 +108,23 @@ export function MetricValue(props: {
}) {
const tooltipId = useId();
const triggerRef = useRef<HTMLSpanElement>(null);
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const triggerFocusedRef = useRef(false);
const triggerHoveredRef = useRef(false);
const tooltipHoveredRef = useRef(false);
const [position, setPosition] = useState<TooltipPosition | null>(null);
const tooltip = props.tooltip?.filter((line) => line.value.trim());
const tooltipColumns = props.tooltipColumns
?.map((column) => column.filter((line) => line.value.trim()))
.filter((column) => column.length);
if (!tooltip?.length && !tooltipColumns?.length) {
return <span className={props.className}>{props.children}</span>;
}
const clearPendingClose = () => {
if (closeTimerRef.current === null) return;
clearTimeout(closeTimerRef.current);
closeTimerRef.current = null;
};

const showTooltip = () => {
clearPendingClose();
if (!triggerRef.current) return;
setPosition(
tooltipPosition(
Expand All @@ -125,7 +135,34 @@ export function MetricValue(props: {
),
);
};
const hideTooltip = () => setPosition(null);
const scheduleTooltipClose = () => {
clearPendingClose();
closeTimerRef.current = setTimeout(() => {
closeTimerRef.current = null;
if (
triggerFocusedRef.current ||
triggerHoveredRef.current ||
tooltipHoveredRef.current
) {
return;
}
setPosition(null);
}, TOOLTIP_CLOSE_DELAY_MS);
};

useEffect(
() => () => {
if (closeTimerRef.current !== null) {
clearTimeout(closeTimerRef.current);
}
},
[],
);

if (!tooltip?.length && !tooltipColumns?.length) {
return <span className={props.className}>{props.children}</span>;
}

const tooltipStyle: CSSProperties | undefined = position
? {
left: position.left,
Expand All @@ -139,19 +176,39 @@ export function MetricValue(props: {
<span
aria-describedby={position ? tooltipId : undefined}
className="border-b border-dotted border-white/20 outline-none transition-colors hover:border-white/45 focus-visible:border-white/45"
onBlur={hideTooltip}
onFocus={showTooltip}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
onBlur={() => {
triggerFocusedRef.current = false;
scheduleTooltipClose();
}}
onFocus={() => {
triggerFocusedRef.current = true;
showTooltip();
}}
onMouseEnter={() => {
triggerHoveredRef.current = true;
showTooltip();
}}
onMouseLeave={() => {
triggerHoveredRef.current = false;
scheduleTooltipClose();
}}
ref={triggerRef}
tabIndex={0}
>
{props.children}
</span>
{position ? (
<span
className="pointer-events-none fixed z-30 rounded-lg border border-white/15 bg-[#050505] px-3 py-2 text-left text-[0.76rem] font-normal leading-relaxed text-dashboard-text-muted shadow-xl shadow-black/35"
className="pointer-events-auto fixed z-30 select-text rounded-lg border border-white/15 bg-[#050505] px-3 py-2 text-left text-[0.76rem] font-normal leading-relaxed text-dashboard-text-muted shadow-xl shadow-black/35"
id={tooltipId}
onMouseEnter={() => {
tooltipHoveredRef.current = true;
clearPendingClose();
}}
onMouseLeave={() => {
tooltipHoveredRef.current = false;
scheduleTooltipClose();
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selection drag dismisses open tooltips

Low Severity

Tooltip leave handlers clear the hover ref and schedule close as soon as the pointer exits the surface. A drag-select that briefly leaves the bounds therefore dismisses the tooltip after the grace period and drops the selection, which undercuts selecting longer cost or usage breakdown text.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 811d7c2. Configure here.

role="tooltip"
style={tooltipStyle}
>
Expand Down
63 changes: 58 additions & 5 deletions packages/junior-dashboard/src/client/components/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type ReactElement,
type ReactNode,
useCallback,
useEffect,
useId,
useLayoutEffect,
useRef,
Expand All @@ -25,15 +26,55 @@ type Position = {

const VIEWPORT_GAP = 8;
const ANCHOR_GAP = 10;
const CLOSE_DELAY_MS = 150;

/** Show dashboard details beside an element while keeping the surface on-screen. */
export function Tooltip({ children, content, label }: TooltipProps) {
const anchorRef = useRef<Element | null>(null);
const tooltipRef = useRef<HTMLDivElement | null>(null);
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const anchorFocusedRef = useRef(false);
const anchorHoveredRef = useRef(false);
const tooltipHoveredRef = useRef(false);
const tooltipId = useId();
const [open, setOpen] = useState(false);
const [position, setPosition] = useState<Position | null>(null);

const clearPendingClose = () => {
if (closeTimerRef.current === null) return;
clearTimeout(closeTimerRef.current);
closeTimerRef.current = null;
};

const showTooltip = () => {
clearPendingClose();
setOpen(true);
};

const scheduleTooltipClose = () => {
clearPendingClose();
closeTimerRef.current = setTimeout(() => {
closeTimerRef.current = null;
if (
anchorFocusedRef.current ||
anchorHoveredRef.current ||
tooltipHoveredRef.current
) {
return;
}
setOpen(false);
}, CLOSE_DELAY_MS);
};

useEffect(
() => () => {
if (closeTimerRef.current !== null) {
clearTimeout(closeTimerRef.current);
}
},
[],
);

const updatePosition = useCallback(() => {
const anchor = anchorRef.current;
const tooltip = tooltipRef.current;
Expand Down Expand Up @@ -86,29 +127,33 @@ export function Tooltip({ children, content, label }: TooltipProps) {
(
childProps.onBlur as ((event: FocusEvent<Element>) => void) | undefined
)?.(event);
setOpen(false);
anchorFocusedRef.current = false;
scheduleTooltipClose();
},
onFocus(event: FocusEvent<Element>) {
(
childProps.onFocus as ((event: FocusEvent<Element>) => void) | undefined
)?.(event);
setOpen(true);
anchorFocusedRef.current = true;
showTooltip();
},
onPointerEnter(event: PointerEvent<Element>) {
(
childProps.onPointerEnter as
| ((event: PointerEvent<Element>) => void)
| undefined
)?.(event);
setOpen(true);
anchorHoveredRef.current = true;
showTooltip();
},
onPointerLeave(event: PointerEvent<Element>) {
(
childProps.onPointerLeave as
| ((event: PointerEvent<Element>) => void)
| undefined
)?.(event);
setOpen(false);
anchorHoveredRef.current = false;
scheduleTooltipClose();
},
ref(node: Element | null) {
anchorRef.current = node;
Expand All @@ -121,8 +166,16 @@ export function Tooltip({ children, content, label }: TooltipProps) {
{open && typeof document !== "undefined"
? createPortal(
<div
className="pointer-events-none fixed z-50 min-w-36 max-w-64 rounded-md border border-white/15 bg-dashboard-surface-raised px-3 py-2 font-mono text-[0.68rem] leading-relaxed text-dashboard-text-muted shadow-2xl shadow-black/70"
className="pointer-events-auto fixed z-50 min-w-36 max-w-64 select-text rounded-md border border-white/15 bg-dashboard-surface-raised px-3 py-2 font-mono text-[0.68rem] leading-relaxed text-dashboard-text-muted shadow-2xl shadow-black/70"
id={tooltipId}
onPointerEnter={() => {
tooltipHoveredRef.current = true;
clearPendingClose();
}}
onPointerLeave={() => {
tooltipHoveredRef.current = false;
scheduleTooltipClose();
}}
ref={tooltipRef}
role="tooltip"
style={{
Expand Down
Loading