Skip to content

Commit ea2020e

Browse files
committed
[ui components] Tooltip: Replace Blueprint with Radix
[INTERNAL_BRANCH=dish/cloud-tooltip]
1 parent 02a8ad8 commit ea2020e

35 files changed

Lines changed: 745 additions & 326 deletions

js_modules/dagster-ui/packages/ui-components/.storybook/preview.js

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1+
import {withThemeByClassName} from '@storybook/addon-themes';
2+
import {MemoryRouter} from 'react-router-dom';
3+
import {createGlobalStyle} from 'styled-components';
4+
15
import {
6+
Colors,
27
FontFamily,
3-
GlobalGeistMono,
48
GlobalDialogStyle,
9+
GlobalGeist,
10+
GlobalGeistMono,
511
GlobalPopoverStyle,
612
GlobalSuggestStyle,
7-
GlobalTooltipStyle,
813
GlobalThemeStyle,
9-
Colors,
10-
GlobalGeist,
1114
Toaster,
1215
} from '../src';
1316

14-
import {withThemeByClassName} from '@storybook/addon-themes';
15-
16-
import {MemoryRouter} from 'react-router-dom';
17-
18-
import {createGlobalStyle} from 'styled-components';
19-
2017
import '@blueprintjs/core/lib/css/blueprint.css';
2118
import '@blueprintjs/select/lib/css/blueprint-select.css';
2219
import '@blueprintjs/popover2/lib/css/blueprint-popover2.css';
@@ -73,7 +70,6 @@ export const decorators = [
7370
<GlobalThemeStyle />
7471
<GlobalGeist />
7572
<GlobalGeistMono />
76-
<GlobalTooltipStyle />
7773
<GlobalPopoverStyle />
7874
<GlobalDialogStyle />
7975
<GlobalSuggestStyle />

js_modules/dagster-ui/packages/ui-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"styled-components": "^5.3.3"
3535
},
3636
"dependencies": {
37+
"@radix-ui/react-tooltip": "^1.1.6",
3738
"@react-hook/resize-observer": "^1.2.6",
3839
"amator": "^1.1.0",
3940
"clsx": "^2.1.1",

js_modules/dagster-ui/packages/ui-components/src/components/Tooltip.tsx

Lines changed: 151 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,169 @@
1-
// eslint-disable-next-line no-restricted-imports
2-
import {Tooltip2, Tooltip2Props} from '@blueprintjs/popover2';
3-
import deepmerge from 'deepmerge';
4-
import * as React from 'react';
5-
import styled, {createGlobalStyle, css} from 'styled-components';
6-
7-
import {Colors} from './Color';
8-
import {FontFamily} from './styles';
9-
10-
export const GlobalTooltipStyle = createGlobalStyle`
11-
.dagster-tooltip .bp5-popover-content {
12-
font-family: ${FontFamily.default};
13-
font-size: 12px;
14-
line-height: 16px;
15-
background: ${Colors.tooltipBackground()};
16-
color: ${Colors.tooltipText()};
17-
padding: 8px 16px;
18-
}
1+
import * as RadixTooltip from '@radix-ui/react-tooltip';
2+
import clsx from 'clsx';
193

20-
.block-tooltip.bp5-popover-target {
21-
display: block;
22-
}
4+
import styles from './css/Tooltip.module.css';
235

24-
.dagster-tooltip-bare .bp5-popover-content {
25-
padding: 0;
26-
}
27-
`;
6+
const placementMap: Record<PlacementType, {side: SideType; align: AlignType}> = {
7+
top: {side: 'top', align: 'center'},
8+
'top-start': {side: 'top', align: 'start'},
9+
'top-end': {side: 'top', align: 'end'},
10+
bottom: {side: 'bottom', align: 'center'},
11+
'bottom-start': {side: 'bottom', align: 'start'},
12+
'bottom-end': {side: 'bottom', align: 'end'},
13+
left: {side: 'left', align: 'center'},
14+
'left-start': {side: 'left', align: 'start'},
15+
'left-end': {side: 'left', align: 'end'},
16+
right: {side: 'right', align: 'center'},
17+
'right-start': {side: 'right', align: 'start'},
18+
'right-end': {side: 'right', align: 'end'},
19+
};
20+
21+
type PlacementType =
22+
| 'top'
23+
| 'bottom'
24+
| 'left'
25+
| 'right'
26+
| 'top-start'
27+
| 'top-end'
28+
| 'bottom-start'
29+
| 'bottom-end'
30+
| 'left-start'
31+
| 'left-end'
32+
| 'right-start'
33+
| 'right-end';
2834

29-
// Overwrite arrays instead of concatting them.
30-
const overwriteMerge = (destination: any[], source: any[]) => source;
35+
type AlignType = 'start' | 'center' | 'end';
36+
type SideType = 'top' | 'bottom' | 'left' | 'right';
3137

32-
interface Props extends Tooltip2Props {
38+
interface ModifierConfig {
39+
offset?: {
40+
enabled?: boolean;
41+
options?: {
42+
offset?: [number, number];
43+
};
44+
};
45+
[key: string]: any;
46+
}
47+
48+
interface Props {
3349
children: React.ReactNode;
50+
content: React.ReactNode;
51+
52+
// Blueprint API compatibility
53+
placement?: PlacementType;
54+
position?: PlacementType; // Blueprint alias for placement
55+
modifiers?: ModifierConfig;
56+
popoverClassName?: string;
57+
minimal?: boolean; // Ignored but accepted for compatibility
58+
isOpen?: boolean; // Blueprint controlled state (alias for open)
59+
60+
// Custom Dagster props
3461
display?: React.CSSProperties['display'];
3562
canShow?: boolean;
36-
useDisabledButtonTooltipFix?: boolean;
63+
64+
// Radix passthrough props
65+
open?: boolean;
66+
defaultOpen?: boolean;
67+
onOpenChange?: (open: boolean) => void;
68+
delayDuration?: number;
69+
disableHoverableContent?: boolean;
70+
}
71+
72+
// ============================================================================
73+
// Placement Mapping (Blueprint → Radix)
74+
// ============================================================================
75+
76+
function mapPlacementToRadix(placement?: PlacementType): {
77+
side: SideType;
78+
align: AlignType;
79+
} {
80+
if (!placement) {
81+
return {side: 'top', align: 'center'};
82+
}
83+
84+
return placementMap[placement] || {side: 'top', align: 'center'};
85+
}
86+
87+
// ============================================================================
88+
// Offset Extraction (Blueprint modifiers → Radix sideOffset)
89+
// ============================================================================
90+
91+
function extractSideOffset(modifiers?: ModifierConfig): number {
92+
const defaultOffset = 8; // Blueprint default was [0, 8]
93+
94+
if (!modifiers?.offset?.enabled) {
95+
return defaultOffset;
96+
}
97+
98+
const offset = modifiers.offset.options?.offset;
99+
if (Array.isArray(offset) && offset.length >= 2) {
100+
return offset[1]; // Blueprint uses [skidding, distance], we want distance
101+
}
102+
103+
return defaultOffset;
37104
}
38105

106+
// ============================================================================
107+
// Main Component
108+
// ============================================================================
109+
39110
export const Tooltip = (props: Props) => {
40-
const {useDisabledButtonTooltipFix = false, children, display, canShow = true, ...rest} = props;
41-
42-
const [isOpen, setIsOpen] = React.useState<undefined | boolean>(undefined);
43-
44-
const divRef = React.useRef<HTMLDivElement>(null);
45-
46-
React.useLayoutEffect(() => {
47-
let listener: null | ((e: MouseEvent) => void) = null;
48-
if (isOpen && useDisabledButtonTooltipFix) {
49-
listener = (e: MouseEvent) => {
50-
if (!divRef.current?.contains(e.target as HTMLDivElement)) {
51-
setIsOpen(false);
52-
}
53-
};
54-
document.body.addEventListener('mousemove', listener);
55-
}
56-
return () => {
57-
if (listener) {
58-
document.body.removeEventListener('mousemove', listener);
59-
}
60-
};
61-
}, [isOpen, useDisabledButtonTooltipFix]);
111+
const {
112+
children,
113+
content,
114+
placement,
115+
position, // Blueprint alias
116+
modifiers,
117+
popoverClassName = '',
118+
canShow = true,
119+
isOpen, // Blueprint controlled state
120+
open,
121+
defaultOpen,
122+
onOpenChange,
123+
delayDuration,
124+
disableHoverableContent,
125+
minimal: _minimal, // Accepted but ignored
126+
...rest
127+
} = props;
62128

129+
// Handle canShow=false: return bare children without tooltip wrapper
63130
if (!canShow) {
64131
return <>{children}</>;
65132
}
66133

67-
const styledTooltip = (
68-
<StyledTooltip
69-
isOpen={isOpen}
70-
{...rest}
71-
minimal
72-
$display={display}
73-
popoverClassName={`dagster-tooltip ${props.popoverClassName}`}
74-
modifiers={deepmerge(
75-
{offset: {enabled: true, options: {offset: [0, 8]}}},
76-
props.modifiers || {},
77-
{arrayMerge: overwriteMerge},
78-
)}
79-
>
80-
{children}
81-
</StyledTooltip>
82-
);
134+
// Determine placement (position is an alias for placement in Blueprint)
135+
const effectivePlacement = placement || position;
136+
const {side, align} = mapPlacementToRadix(effectivePlacement);
137+
const sideOffset = extractSideOffset(modifiers);
138+
139+
// Use isOpen if provided (Blueprint compatibility), otherwise use open
140+
const effectiveOpen = isOpen !== undefined ? isOpen : open;
83141

84-
if (useDisabledButtonTooltipFix) {
85-
return (
86-
<div
87-
ref={divRef}
88-
onMouseEnter={() => {
89-
setIsOpen(true);
90-
}}
142+
return (
143+
<RadixTooltip.Provider delayDuration={100}>
144+
<RadixTooltip.Root
145+
open={effectiveOpen}
146+
defaultOpen={defaultOpen}
147+
onOpenChange={onOpenChange}
148+
delayDuration={delayDuration}
149+
disableHoverableContent={disableHoverableContent}
91150
>
92-
{styledTooltip}
93-
</div>
94-
);
95-
}
96-
return styledTooltip;
151+
<RadixTooltip.Trigger asChild>
152+
<span>{children}</span>
153+
</RadixTooltip.Trigger>
154+
<RadixTooltip.Portal>
155+
<RadixTooltip.Content
156+
{...rest}
157+
className={clsx(styles.tooltip, popoverClassName)}
158+
side={side}
159+
align={align}
160+
sideOffset={sideOffset}
161+
collisionPadding={8}
162+
>
163+
{content}
164+
</RadixTooltip.Content>
165+
</RadixTooltip.Portal>
166+
</RadixTooltip.Root>
167+
</RadixTooltip.Provider>
168+
);
97169
};
98-
99-
interface StyledTooltipProps extends React.ComponentProps<typeof Tooltip2> {
100-
$display: React.CSSProperties['display'];
101-
children: React.ReactNode;
102-
}
103-
104-
const StyledTooltip = styled(Tooltip2)<StyledTooltipProps>`
105-
${({$display}) =>
106-
$display
107-
? css`
108-
&& {
109-
display: ${$display};
110-
}
111-
`
112-
: null}
113-
`;

js_modules/dagster-ui/packages/ui-components/src/components/__stories__/Tooltip.stories.tsx

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,8 @@ import {Box} from '../Box';
44
import {Button} from '../Button';
55
import {Checkbox} from '../Checkbox';
66
import {Colors} from '../Color';
7-
import {CustomTooltipProvider} from '../CustomTooltipProvider';
8-
import {Group} from '../Group';
97
import {Icon} from '../Icon';
10-
import {GlobalTooltipStyle, Tooltip} from '../Tooltip';
11-
12-
const SOLID_STYLES: React.CSSProperties = {
13-
background: Colors.backgroundDefault(),
14-
transform: 'translate(0,0)',
15-
border: `1px solid ${Colors.accentYellow()}`,
16-
color: Colors.textDefault(),
17-
fontSize: '12px',
18-
padding: 6,
19-
};
20-
21-
const JOB_STYLES: React.CSSProperties = {
22-
background: Colors.backgroundDefault(),
23-
border: `1px solid ${Colors.borderDefault()}`,
24-
color: Colors.textDefault(),
25-
fontSize: '15px',
26-
padding: 3,
27-
};
8+
import {Tooltip} from '../Tooltip';
289

2910
// eslint-disable-next-line import/no-default-export
3011
export default {
@@ -40,10 +21,7 @@ export const Default = () => {
4021
);
4122

4223
return (
43-
<Group spacing={8} direction="column">
44-
<CustomTooltipProvider />
45-
<GlobalTooltipStyle />
46-
24+
<Box flex={{direction: 'column', gap: 8, alignItems: 'flex-start'}}>
4725
<p style={{color: Colors.textLight()}}>
4826
Use the <code>Tooltip</code> component to attach additional explanations, descriptions, and
4927
context to controls, icons, etc.
@@ -75,44 +53,7 @@ export const Default = () => {
7553
>
7654
Tooltip with Block Content
7755
</Tooltip>
78-
79-
<hr />
80-
81-
<p style={{color: Colors.textLight()}}>
82-
Use the <code>data-tooltip</code> attribute to expand truncated job, op names, etc. on
83-
hover. These are highly stylable via <code>data-tooltip-style</code> so they can look like
84-
boxes / nodes expanding in place to reveal their full text. There is no per-component render
85-
cost to these annotations so they can be used in cases when thousands of nodes are rendered.
86-
<br />
87-
<br />
88-
These tooltips automatically appear only when the content is truncated or when content
89-
contains a <code></code>
90-
</p>
91-
{['short_solid', 'long_solid_name_here'].map((name) => (
92-
<div
93-
key={name}
94-
data-tooltip={name}
95-
data-tooltip-style={JSON.stringify(SOLID_STYLES)}
96-
style={{
97-
width: '100px',
98-
overflow: 'hidden',
99-
textOverflow: 'ellipsis',
100-
position: 'relative',
101-
...SOLID_STYLES,
102-
}}
103-
>
104-
{name}
105-
</div>
106-
))}
107-
108-
<span
109-
data-tooltip="fetch_from_redshift_cloud_prod"
110-
data-tooltip-style={JSON.stringify(JOB_STYLES)}
111-
style={JOB_STYLES}
112-
>
113-
fetch_from_redshift…
114-
</span>
115-
</Group>
56+
</Box>
11657
);
11758
};
11859

0 commit comments

Comments
 (0)