|
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'; |
19 | 3 |
|
20 | | - .block-tooltip.bp5-popover-target { |
21 | | - display: block; |
22 | | - } |
| 4 | +import styles from './css/Tooltip.module.css'; |
23 | 5 |
|
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'; |
28 | 34 |
|
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'; |
31 | 37 |
|
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 { |
33 | 49 | 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 |
34 | 61 | display?: React.CSSProperties['display']; |
35 | 62 | 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; |
37 | 104 | } |
38 | 105 |
|
| 106 | +// ============================================================================ |
| 107 | +// Main Component |
| 108 | +// ============================================================================ |
| 109 | + |
39 | 110 | 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; |
62 | 128 |
|
| 129 | + // Handle canShow=false: return bare children without tooltip wrapper |
63 | 130 | if (!canShow) { |
64 | 131 | return <>{children}</>; |
65 | 132 | } |
66 | 133 |
|
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; |
83 | 141 |
|
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} |
91 | 150 | > |
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 | + ); |
97 | 169 | }; |
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 | | -`; |
|
0 commit comments