Skip to content

Commit ee48c65

Browse files
barviancursoragent
andcommitted
Migrate tooltips from Radix to Base UI.
Drop @radix-ui/react-tooltip now that InfoTooltip, LifecycleTooltip, and the benchmark tooltip share Base UI's Provider / Positioner / Popup anatomy. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ed82dca commit ee48c65

7 files changed

Lines changed: 121 additions & 484 deletions

File tree

app/benchmark/components/Tooltip.tsx

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import * as React from "react";
2-
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1+
'use client';
2+
3+
import { isValidElement, type ReactElement, type ReactNode } from "react";
4+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
35

46
interface TooltipProps {
5-
children: React.ReactNode;
6-
content: React.ReactNode;
7+
children: ReactNode;
8+
content: ReactNode;
79
className?: string;
810
delayDuration?: number;
911
side?: "top" | "right" | "bottom" | "left";
@@ -18,20 +20,34 @@ const Tooltip = ({
1820
side = "top",
1921
align = "center",
2022
}: TooltipProps) => {
23+
const trigger: ReactElement = isValidElement(children) ? (
24+
children
25+
) : (
26+
<button type="button">{children}</button>
27+
);
28+
2129
return (
22-
<TooltipPrimitive.Provider delayDuration={delayDuration}>
30+
<TooltipPrimitive.Provider delay={delayDuration}>
2331
<TooltipPrimitive.Root>
24-
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
32+
<TooltipPrimitive.Trigger render={trigger} />
2533
<TooltipPrimitive.Portal>
26-
<TooltipPrimitive.Content
27-
className={`z-50 overflow-hidden rounded-md bg-gray-900 px-3 py-2 text-sm text-white shadow-md animate-fade-in data-[state=closed]:animate-fade-out ${className}`}
28-
sideOffset={5}
34+
<TooltipPrimitive.Positioner
35+
className="z-50"
36+
sideOffset={14}
2937
side={side}
3038
align={align}
3139
>
32-
{content}
33-
<TooltipPrimitive.Arrow className="fill-gray-900" />
34-
</TooltipPrimitive.Content>
40+
<TooltipPrimitive.Popup
41+
className={`rounded-md bg-gray-900 px-3 py-2 text-sm text-white shadow-md outline-none origin-[var(--transform-origin)] transition-[opacity,transform] duration-150 ease-out data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 data-[instant]:transition-none ${className}`}
42+
>
43+
{content}
44+
<TooltipPrimitive.Arrow className="flex data-[side=bottom]:top-[-7px] data-[side=left]:right-[-11px] data-[side=left]:rotate-90 data-[side=right]:left-[-11px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-7px] data-[side=top]:rotate-180">
45+
<svg width="16" height="8" viewBox="0 0 16 8" fill="none" aria-hidden="true">
46+
<path d="M0 8L8 0L16 8" className="fill-gray-900" />
47+
</svg>
48+
</TooltipPrimitive.Arrow>
49+
</TooltipPrimitive.Popup>
50+
</TooltipPrimitive.Positioner>
3551
</TooltipPrimitive.Portal>
3652
</TooltipPrimitive.Root>
3753
</TooltipPrimitive.Provider>

app/components/ui/InfoTooltip.tsx

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

3-
import * as Tooltip from '@radix-ui/react-tooltip';
43
import type { ReactNode } from 'react';
4+
import { Tooltip } from '@base-ui/react/tooltip';
55

66
import { cn } from './cn';
77

@@ -17,42 +17,55 @@ type InfoTooltipProps = {
1717
className?: string;
1818
};
1919

20+
/** Open chevron: fill covers the popup’s inset ring at the join; stroke only the slants. */
21+
function TooltipCaret() {
22+
return (
23+
<Tooltip.Arrow className="group flex will-change-transform data-[side=bottom]:top-[-7px] data-[side=left]:right-[-11px] data-[side=right]:left-[-11px] data-[side=top]:bottom-[-7px]">
24+
<svg
25+
width="16"
26+
height="8"
27+
viewBox="0 0 16 8"
28+
aria-hidden="true"
29+
className="block origin-center fill-background stroke-black/[0.04] group-data-[side=bottom]:rotate-180 group-data-[side=left]:-rotate-90 group-data-[side=right]:rotate-90 dark:fill-[#1a1a1a] dark:stroke-[color-mix(in_srgb,#1a1a1a,white_8%)]"
30+
>
31+
<path d="M0 0 L6.2 6 Q8 8 9.8 6 L16 0" strokeWidth="1" strokeLinejoin="round" />
32+
</svg>
33+
</Tooltip.Arrow>
34+
);
35+
}
36+
2037
/**
2138
* Small "ⓘ" affordance that reveals a short explanation on hover or focus.
2239
*
2340
* The trigger is a real <button> (not a role-button span), so keyboard and
24-
* screen-reader users get it for free; Radix handles Escape, focus, and
41+
* screen-reader users get it for free; Base UI handles Escape, focus, and
2542
* pointer/keyboard parity. Content sits at z-[140] — above the Modal (120) and
26-
* Select popper (130), see the z-index ladder in globals.css — so it stays
43+
* Select popper (130), see the overlay layer scale in globals.css — so it stays
2744
* usable inside dialogs. Self-contained Provider so callers can drop it in
2845
* anywhere without wiring one up.
2946
*/
3047
export function InfoTooltip({ label, children, side = 'top', className }: InfoTooltipProps) {
3148
return (
32-
<Tooltip.Provider delayDuration={150} skipDelayDuration={300}>
49+
<Tooltip.Provider delay={150} timeout={300}>
3350
<Tooltip.Root>
34-
<Tooltip.Trigger asChild>
35-
<button
36-
type="button"
37-
aria-label={label}
38-
className={cn(
39-
'inline-flex h-4 w-4 shrink-0 cursor-help items-center justify-center rounded-full border border-bds-gray-20 text-[10px] font-semibold leading-none text-bds-gray-50 outline-none transition-colors hover:border-base-blue hover:text-base-blue focus-visible:ring-2 focus-visible:ring-base-blue/40 dark:border-white/20',
40-
className,
41-
)}
42-
>
43-
<span aria-hidden="true">i</span>
44-
</button>
51+
<Tooltip.Trigger
52+
type="button"
53+
aria-label={label}
54+
closeOnClick={false}
55+
className={cn(
56+
'inline-flex h-4 w-4 shrink-0 cursor-help items-center justify-center rounded-full border border-bds-gray-20 text-[10px] font-semibold leading-none text-bds-gray-50 outline-none transition-colors hover:border-base-blue hover:text-base-blue focus-visible:ring-2 focus-visible:ring-base-blue/40 dark:border-white/20',
57+
className,
58+
)}
59+
>
60+
<span aria-hidden="true">i</span>
4561
</Tooltip.Trigger>
4662
<Tooltip.Portal>
47-
<Tooltip.Content
48-
side={side}
49-
sideOffset={6}
50-
collisionPadding={12}
51-
className="z-[140] max-w-[18rem] rounded-lg border border-bds-gray-10 bg-background px-3 py-2 text-[12px] leading-relaxed text-bds-gray-70 shadow-lg dark:border-white/10 dark:bg-[#1a1a1a]"
52-
>
53-
{children}
54-
<Tooltip.Arrow className="fill-white dark:fill-[#1a1a1a]" />
55-
</Tooltip.Content>
63+
<Tooltip.Positioner side={side} sideOffset={14} collisionPadding={12} className="z-[140]">
64+
<Tooltip.Popup className="max-w-[18rem] origin-[var(--transform-origin)] rounded-lg bg-background px-3 py-2 text-[12px] leading-relaxed text-bds-gray-70 shadow-lg outline-none ring-1 ring-black/[0.04] will-change-transform transition-[opacity,transform] duration-150 ease-out data-[ending-style]:opacity-0 data-[ending-style]:[transform:scale(0.97)] data-[instant]:transition-none data-[starting-style]:opacity-0 data-[starting-style]:[transform:scale(0.97)] dark:bg-[#1a1a1a] dark:ring-inset dark:ring-[color-mix(in_srgb,#1a1a1a,white_8%)] motion-reduce:transition-none">
65+
{children}
66+
<TooltipCaret />
67+
</Tooltip.Popup>
68+
</Tooltip.Positioner>
5669
</Tooltip.Portal>
5770
</Tooltip.Root>
5871
</Tooltip.Provider>

app/globals.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* 120 modals (components/ui/Modal.tsx)
1212
* 130 poppers over modals (components/ui/Select.tsx,
1313
* vibenet/demos/_shared/AccountSwitcher.tsx)
14+
* 140 tooltips over poppers (components/ui/InfoTooltip.tsx)
1415
*
1516
* A popper below the modal layer still opens, but paints behind the panel —
1617
* which reads as "the dropdown does nothing". */

app/upgrades/components/LifecycleTooltip.tsx

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import * as Tooltip from '@radix-ui/react-tooltip';
3+
import { Tooltip } from '@base-ui/react/tooltip';
44

55
import { cn } from '../../components/ui/cn';
66

@@ -21,59 +21,68 @@ type LifecycleTooltipProps = {
2121

2222
export function LifecycleTooltip({ entries, summary, size, showLabels }: LifecycleTooltipProps) {
2323
return (
24-
<Tooltip.Provider delayDuration={150}>
24+
<Tooltip.Provider delay={150}>
2525
<Tooltip.Root>
26-
<Tooltip.Trigger asChild>
27-
<span
28-
role="button"
29-
aria-label={`Lifecycle status: ${summary}`}
30-
tabIndex={0}
31-
className={cn(
32-
'border-bds-gray-10 focus-visible:ring-base-blue/30 inline-flex items-center rounded-full border bg-white/80 outline-none transition-colors focus-visible:ring-2 dark:border-white/10 dark:bg-white/10',
33-
size === 'sm' ? 'gap-1 px-2 py-1' : 'gap-1.5 px-2.5 py-1.5',
34-
)}
35-
>
36-
{entries.map((entry) => (
37-
<span key={entry.networkKey} className="inline-flex items-center gap-1">
38-
<span
39-
className={cn(
40-
'rounded-full ring-2 ring-black/5 dark:ring-white/10',
41-
size === 'sm' ? 'h-2 w-2' : 'h-2.5 w-2.5',
42-
entry.dotClassName,
43-
)}
44-
/>
45-
{showLabels ? (
46-
<span className="font-mono text-[11px] font-medium uppercase leading-[14px] tracking-[0px] text-[#787878] md:text-[12px] md:leading-[16px]">
47-
{entry.networkKey.slice(0, 1)}
48-
</span>
49-
) : null}
50-
</span>
51-
))}
52-
</span>
26+
<Tooltip.Trigger
27+
type="button"
28+
aria-label={`Lifecycle status: ${summary}`}
29+
className={cn(
30+
'border-bds-gray-10 focus-visible:ring-base-blue/30 inline-flex items-center rounded-full border bg-white/80 outline-none transition-colors focus-visible:ring-2 dark:border-white/10 dark:bg-white/10',
31+
size === 'sm' ? 'gap-1 px-2 py-1' : 'gap-1.5 px-2.5 py-1.5',
32+
)}
33+
>
34+
{entries.map((entry) => (
35+
<span key={entry.networkKey} className="inline-flex items-center gap-1">
36+
<span
37+
className={cn(
38+
'rounded-full ring-2 ring-black/5 dark:ring-white/10',
39+
size === 'sm' ? 'h-2 w-2' : 'h-2.5 w-2.5',
40+
entry.dotClassName,
41+
)}
42+
/>
43+
{showLabels ? (
44+
<span className="font-mono text-[11px] font-medium uppercase leading-[14px] tracking-[0px] text-[#787878] md:text-[12px] md:leading-[16px]">
45+
{entry.networkKey.slice(0, 1)}
46+
</span>
47+
) : null}
48+
</span>
49+
))}
5350
</Tooltip.Trigger>
5451
<Tooltip.Portal>
55-
<Tooltip.Content
52+
<Tooltip.Positioner
5653
side="top"
5754
align="center"
58-
sideOffset={8}
55+
sideOffset={14}
5956
collisionPadding={16}
60-
className="border-bds-gray-10 pointer-events-none z-[12000] w-max max-w-[min(20rem,calc(100vw-2rem))] rounded-lg border bg-background px-3 py-2 shadow-lg dark:border-white/10"
57+
className="z-[12000]"
6158
>
62-
<span className="block space-y-1">
63-
{entries.map((entry) => (
64-
<span key={entry.network} className="flex items-baseline justify-between gap-4">
65-
<span className="text-[11px] font-medium leading-[14px] tracking-[0px] text-foreground md:text-[12px] md:leading-[16px] dark:text-white">
66-
{entry.network}
59+
<Tooltip.Popup className="pointer-events-none w-max max-w-[min(20rem,calc(100vw-2rem))] origin-[var(--transform-origin)] rounded-lg bg-background px-3 py-2 shadow-lg outline-none ring-1 ring-black/[0.04] will-change-transform transition-[opacity,transform] duration-150 ease-out data-[ending-style]:opacity-0 data-[ending-style]:[transform:scale(0.97)] data-[instant]:transition-none data-[starting-style]:opacity-0 data-[starting-style]:[transform:scale(0.97)] dark:bg-[#1a1a1a] dark:ring-inset dark:ring-[color-mix(in_srgb,#1a1a1a,white_8%)] motion-reduce:transition-none">
60+
<span className="block space-y-1">
61+
{entries.map((entry) => (
62+
<span key={entry.network} className="flex items-baseline justify-between gap-4">
63+
<span className="text-[11px] font-medium leading-[14px] tracking-[0px] text-foreground md:text-[12px] md:leading-[16px] dark:text-white">
64+
{entry.network}
65+
</span>
66+
<span className="text-right font-mono text-[11px] font-medium leading-[14px] tracking-[0px] text-[#787878] md:text-[12px] md:leading-[16px]">
67+
{entry.state}
68+
{entry.date ? ` · ${entry.date}` : ''}
69+
</span>
6770
</span>
68-
<span className="text-right font-mono text-[11px] font-medium leading-[14px] tracking-[0px] text-[#787878] md:text-[12px] md:leading-[16px]">
69-
{entry.state}
70-
{entry.date ? ` · ${entry.date}` : ''}
71-
</span>
72-
</span>
73-
))}
74-
</span>
75-
<Tooltip.Arrow width={10} height={5} className="fill-background" />
76-
</Tooltip.Content>
71+
))}
72+
</span>
73+
<Tooltip.Arrow className="group flex will-change-transform data-[side=bottom]:top-[-7px] data-[side=left]:right-[-11px] data-[side=right]:left-[-11px] data-[side=top]:bottom-[-7px]">
74+
<svg
75+
width="16"
76+
height="8"
77+
viewBox="0 0 16 8"
78+
aria-hidden="true"
79+
className="block origin-center fill-background stroke-black/[0.04] group-data-[side=bottom]:rotate-180 group-data-[side=left]:-rotate-90 group-data-[side=right]:rotate-90 dark:fill-[#1a1a1a] dark:stroke-[color-mix(in_srgb,#1a1a1a,white_8%)]"
80+
>
81+
<path d="M0 0 L6.2 6 Q8 8 9.8 6 L16 0" strokeWidth="1" strokeLinejoin="round" />
82+
</svg>
83+
</Tooltip.Arrow>
84+
</Tooltip.Popup>
85+
</Tooltip.Positioner>
7786
</Tooltip.Portal>
7887
</Tooltip.Root>
7988
</Tooltip.Provider>

0 commit comments

Comments
 (0)