@momo-kits/animated-tooltip 0.154.2-beta.1 → 0.154.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PORTAL_SETUP.md +52 -0
- package/TooltipPortal.tsx +125 -0
- package/index.tsx +275 -50
- package/package.json +15 -15
- package/styles.ts +0 -1
package/PORTAL_SETUP.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Tooltip Portal Setup
|
|
2
|
+
|
|
3
|
+
## Important: Required Setup
|
|
4
|
+
|
|
5
|
+
The Tooltip component now uses a **Portal system** to render above all other content, solving zIndex limitations. This requires a one-time setup in your app.
|
|
6
|
+
|
|
7
|
+
### Setup Instructions
|
|
8
|
+
|
|
9
|
+
Wrap your app with `TooltipPortalProvider` and add `TooltipPortalHost` at the root level:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { TooltipPortalProvider, TooltipPortalHost } from '@momo-kits/tooltip';
|
|
14
|
+
import YourApp from './YourApp';
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
return (
|
|
18
|
+
<TooltipPortalProvider>
|
|
19
|
+
<YourApp />
|
|
20
|
+
{/* TooltipPortalHost renders all tooltips at this level */}
|
|
21
|
+
<TooltipPortalHost />
|
|
22
|
+
</TooltipPortalProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default App;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Usage
|
|
30
|
+
|
|
31
|
+
After setup, use Tooltip as normal - **no changes required**:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Tooltip } from '@momo-kits/tooltip';
|
|
35
|
+
|
|
36
|
+
<Tooltip title="Help" description="This is helpful info" placement="top">
|
|
37
|
+
<Button>Press me</Button>
|
|
38
|
+
</Tooltip>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What Changed
|
|
42
|
+
|
|
43
|
+
### For Developers
|
|
44
|
+
- **No API changes** - Tooltip props remain the same
|
|
45
|
+
- **One-time setup** required (Provider + Host)
|
|
46
|
+
- **Better zIndex handling** - Tooltips now render above everything
|
|
47
|
+
|
|
48
|
+
### Technical Details
|
|
49
|
+
- Uses `measureInWindow` for absolute positioning
|
|
50
|
+
- Portal renders tooltips at root level (outside component hierarchy)
|
|
51
|
+
- No ref required from consumer - uses internal ref
|
|
52
|
+
- Backward compatible API
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useMemo,
|
|
7
|
+
useReducer,
|
|
8
|
+
useRef,
|
|
9
|
+
RefObject,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { View, StyleSheet } from 'react-native';
|
|
12
|
+
|
|
13
|
+
interface TooltipPortalContextValue {
|
|
14
|
+
register: (id: string, content: ReactNode) => void;
|
|
15
|
+
unregister: (id: string, immediate?: boolean) => void;
|
|
16
|
+
portals: Map<string, ReactNode>;
|
|
17
|
+
hostRef: RefObject<View | null>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TooltipPortalContext = createContext<TooltipPortalContextValue | null>(
|
|
21
|
+
null,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
|
|
25
|
+
children,
|
|
26
|
+
}) => {
|
|
27
|
+
// Use ref for synchronous updates + reducer for manual re-renders
|
|
28
|
+
const portalsRef = useRef<Map<string, ReactNode>>(new Map());
|
|
29
|
+
const hostRef = useRef<View>(null);
|
|
30
|
+
const [updateCounter, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
|
31
|
+
const pendingUpdateRef = useRef<NodeJS.Timeout | null>(null);
|
|
32
|
+
|
|
33
|
+
const register = useCallback((id: string, content: ReactNode) => {
|
|
34
|
+
portalsRef.current.set(id, content);
|
|
35
|
+
forceUpdate(); // Trigger re-render to show new content
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const unregister = useCallback((id: string, immediate = false) => {
|
|
39
|
+
const hasItem = portalsRef.current.has(id);
|
|
40
|
+
if (!hasItem) return; // Already removed
|
|
41
|
+
|
|
42
|
+
portalsRef.current.delete(id);
|
|
43
|
+
|
|
44
|
+
// Clear any pending updates
|
|
45
|
+
if (pendingUpdateRef.current) {
|
|
46
|
+
clearTimeout(pendingUpdateRef.current);
|
|
47
|
+
pendingUpdateRef.current = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (immediate) {
|
|
51
|
+
forceUpdate();
|
|
52
|
+
} else {
|
|
53
|
+
// Debounce normal updates to avoid excessive re-renders
|
|
54
|
+
pendingUpdateRef.current = setTimeout(() => {
|
|
55
|
+
forceUpdate();
|
|
56
|
+
pendingUpdateRef.current = null;
|
|
57
|
+
}, 0) as unknown as NodeJS.Timeout;
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const value = useMemo(
|
|
62
|
+
() => ({ register, unregister, portals: portalsRef.current, hostRef }),
|
|
63
|
+
[register, unregister, updateCounter], // Include updateCounter to refresh context
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<TooltipPortalContext.Provider value={value}>
|
|
68
|
+
{children}
|
|
69
|
+
</TooltipPortalContext.Provider>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const TooltipPortalHost: React.FC = () => {
|
|
74
|
+
const context = useContext(TooltipPortalContext);
|
|
75
|
+
|
|
76
|
+
if (!context) {
|
|
77
|
+
if (__DEV__) {
|
|
78
|
+
console.warn(
|
|
79
|
+
'TooltipPortalHost must be used within TooltipPortalProvider',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { portals, hostRef } = context;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<View ref={hostRef} style={styles.hostContainer} pointerEvents="box-none">
|
|
89
|
+
{Array.from(portals.entries()).map(([id, content]) => (
|
|
90
|
+
<React.Fragment key={id}>{content}</React.Fragment>
|
|
91
|
+
))}
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const useTooltipPortal = () => {
|
|
97
|
+
const context = useContext(TooltipPortalContext);
|
|
98
|
+
|
|
99
|
+
if (!context) {
|
|
100
|
+
if (__DEV__) {
|
|
101
|
+
console.warn(
|
|
102
|
+
'useTooltipPortal must be used within TooltipPortalProvider. Tooltips may not appear correctly.',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
// Return no-op functions if context is missing
|
|
106
|
+
return {
|
|
107
|
+
register: () => {},
|
|
108
|
+
unregister: () => {},
|
|
109
|
+
hostRef: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return context;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
hostContainer: {
|
|
118
|
+
position: 'absolute',
|
|
119
|
+
top: 0,
|
|
120
|
+
left: 0,
|
|
121
|
+
right: 0,
|
|
122
|
+
bottom: 0,
|
|
123
|
+
zIndex: 9999,
|
|
124
|
+
},
|
|
125
|
+
});
|
package/index.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import styles from './styles';
|
|
27
27
|
import { TooltipPlacement, TooltipProps, TooltipRef } from './types';
|
|
28
28
|
import { IconButton, PrimaryButton, SecondaryButton } from './TooltipButtons';
|
|
29
|
+
import { useTooltipPortal } from './TooltipPortal';
|
|
29
30
|
|
|
30
31
|
const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
31
32
|
{
|
|
@@ -53,6 +54,32 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
53
54
|
const arrowSize = 6;
|
|
54
55
|
const [tooltipSize, setTooltipSize] = useState({ width: 0, height: 0 });
|
|
55
56
|
|
|
57
|
+
// Portal integration
|
|
58
|
+
const portal = useTooltipPortal();
|
|
59
|
+
const portalId = useRef(
|
|
60
|
+
`tooltip-${Math.random().toString(36).substr(2, 9)}`,
|
|
61
|
+
).current;
|
|
62
|
+
|
|
63
|
+
// DEBUG: Check if portal is available
|
|
64
|
+
console.log('[Tooltip] Portal context available:', !!portal, portalId);
|
|
65
|
+
|
|
66
|
+
// Internal ref for anchor - no ref required from consumer
|
|
67
|
+
const anchorRef = useRef<View>(null);
|
|
68
|
+
|
|
69
|
+
// Anchor position from measureInWindow
|
|
70
|
+
const [anchorPosition, setAnchorPosition] = useState({
|
|
71
|
+
x: 0,
|
|
72
|
+
y: 0,
|
|
73
|
+
width: 0,
|
|
74
|
+
height: 0,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Portal host position for relative calculation
|
|
78
|
+
const [hostPosition, setHostPosition] = useState({
|
|
79
|
+
x: 0,
|
|
80
|
+
y: 0,
|
|
81
|
+
});
|
|
82
|
+
|
|
56
83
|
const componentName = 'Tooltip';
|
|
57
84
|
const componentId = useMemo(() => {
|
|
58
85
|
if (accessibilityLabel) {
|
|
@@ -69,10 +96,17 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
69
96
|
).current;
|
|
70
97
|
const showTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
71
98
|
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
72
|
-
const [anchorSize, setAnchorSize] = useState({ width: 0, height: 0 });
|
|
73
99
|
|
|
74
100
|
const currentVisible = isControlled ? !!visible : internalVisible;
|
|
75
101
|
|
|
102
|
+
// DEBUG: Log visibility state
|
|
103
|
+
console.log('[Tooltip] Visibility state:', {
|
|
104
|
+
isControlled,
|
|
105
|
+
visible,
|
|
106
|
+
internalVisible,
|
|
107
|
+
currentVisible
|
|
108
|
+
});
|
|
109
|
+
|
|
76
110
|
const clearTimers = () => {
|
|
77
111
|
if (showTimer.current) {
|
|
78
112
|
clearTimeout(showTimer.current);
|
|
@@ -102,6 +136,100 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
102
136
|
animate(currentVisible);
|
|
103
137
|
}, [currentVisible, animationDuration]);
|
|
104
138
|
|
|
139
|
+
// Preload anchor measurement on mount for faster initial display
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
// Measure immediately on mount to have position ready
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
// Measure host position first
|
|
144
|
+
if (portal.hostRef?.current) {
|
|
145
|
+
portal.hostRef.current.measureInWindow((hostX, hostY) => {
|
|
146
|
+
setHostPosition({ x: hostX, y: hostY });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
anchorRef.current?.measureInWindow((x, y, width, height) => {
|
|
151
|
+
if (x !== 0 || y !== 0 || width !== 0 || height !== 0) {
|
|
152
|
+
setAnchorPosition({ x, y, width, height });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}, 0);
|
|
156
|
+
|
|
157
|
+
return () => clearTimeout(timer);
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
// Continuously measure anchor position when tooltip is visible
|
|
161
|
+
// This handles scroll events and dynamic layout changes
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!currentVisible) {
|
|
164
|
+
// Reset position immediately when hiding
|
|
165
|
+
setAnchorPosition({ x: 0, y: 0, width: 0, height: 0 });
|
|
166
|
+
setHostPosition({ x: 0, y: 0 });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let rafId: number | null = null;
|
|
171
|
+
let lastPosition = { x: 0, y: 0, width: 0, height: 0 };
|
|
172
|
+
let lastHostPosition = { x: 0, y: 0 };
|
|
173
|
+
let isMounted = true; // Track mounted state
|
|
174
|
+
let isActive = true; // Track active state for early cancellation
|
|
175
|
+
|
|
176
|
+
const measureAnchor = () => {
|
|
177
|
+
// Check flags before any work
|
|
178
|
+
if (!isMounted || !isActive || !anchorRef.current) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Measure portal host position first
|
|
183
|
+
if (portal.hostRef?.current) {
|
|
184
|
+
portal.hostRef.current.measureInWindow((hostX, hostY) => {
|
|
185
|
+
if (!isMounted || !isActive) return;
|
|
186
|
+
|
|
187
|
+
if (hostX !== lastHostPosition.x || hostY !== lastHostPosition.y) {
|
|
188
|
+
lastHostPosition = { x: hostX, y: hostY };
|
|
189
|
+
setHostPosition({ x: hostX, y: hostY });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
anchorRef.current.measureInWindow((x, y, width, height) => {
|
|
195
|
+
// Double-check flags in callback
|
|
196
|
+
if (!isMounted || !isActive) return;
|
|
197
|
+
|
|
198
|
+
// Only update if position changed (to avoid unnecessary re-renders)
|
|
199
|
+
if (
|
|
200
|
+
x !== lastPosition.x ||
|
|
201
|
+
y !== lastPosition.y ||
|
|
202
|
+
width !== lastPosition.width ||
|
|
203
|
+
height !== lastPosition.height
|
|
204
|
+
) {
|
|
205
|
+
console.log('[Tooltip] Position updated:', { x, y, width, height });
|
|
206
|
+
lastPosition = { x, y, width, height };
|
|
207
|
+
setAnchorPosition({ x, y, width, height });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Continue polling while visible, mounted, and active
|
|
212
|
+
if (currentVisible && isMounted && isActive) {
|
|
213
|
+
rafId = requestAnimationFrame(measureAnchor);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Start polling
|
|
218
|
+
rafId = requestAnimationFrame(measureAnchor);
|
|
219
|
+
|
|
220
|
+
return () => {
|
|
221
|
+
// Set flags immediately to stop all pending work
|
|
222
|
+
isMounted = false;
|
|
223
|
+
isActive = false;
|
|
224
|
+
|
|
225
|
+
// Cancel any pending animation frame
|
|
226
|
+
if (rafId !== null) {
|
|
227
|
+
cancelAnimationFrame(rafId);
|
|
228
|
+
rafId = null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}, [currentVisible]);
|
|
232
|
+
|
|
105
233
|
useEffect(() => {
|
|
106
234
|
return () => {
|
|
107
235
|
clearTimers();
|
|
@@ -111,7 +239,20 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
111
239
|
|
|
112
240
|
const handleShow = () => {
|
|
113
241
|
clearTimers();
|
|
114
|
-
showTimer.current = setTimeout(() =>
|
|
242
|
+
showTimer.current = setTimeout(() => {
|
|
243
|
+
// Measure host position first
|
|
244
|
+
if (portal.hostRef?.current) {
|
|
245
|
+
portal.hostRef.current.measureInWindow((hostX, hostY) => {
|
|
246
|
+
setHostPosition({ x: hostX, y: hostY });
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Measure anchor position before showing tooltip
|
|
251
|
+
anchorRef.current?.measureInWindow((x, y, width, height) => {
|
|
252
|
+
setAnchorPosition({ x, y, width, height });
|
|
253
|
+
setVisibility(true);
|
|
254
|
+
});
|
|
255
|
+
}, showDelay);
|
|
115
256
|
};
|
|
116
257
|
|
|
117
258
|
const handleHide = () => {
|
|
@@ -166,32 +307,73 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
166
307
|
}),
|
|
167
308
|
};
|
|
168
309
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
310
|
+
// Calculate absolute position based on window coordinates
|
|
311
|
+
// Subtract host position to make it relative to the portal host container
|
|
312
|
+
const getTooltipPosition = useMemo((): {
|
|
313
|
+
top?: number;
|
|
314
|
+
left?: number;
|
|
315
|
+
right?: number;
|
|
316
|
+
bottom?: number;
|
|
317
|
+
} => {
|
|
318
|
+
const { x, y, width, height } = anchorPosition;
|
|
319
|
+
const { width: tooltipWidth, height: tooltipHeight } = tooltipSize;
|
|
320
|
+
|
|
321
|
+
let position: { top?: number; left?: number; right?: number } = {};
|
|
322
|
+
|
|
323
|
+
// Vertical positioning (relative to portal host)
|
|
324
|
+
if (placement === 'top') {
|
|
325
|
+
position.top = y - tooltipHeight - offset - hostPosition.y;
|
|
326
|
+
} else if (placement === 'bottom') {
|
|
327
|
+
position.top = y + height + offset - hostPosition.y;
|
|
328
|
+
} else {
|
|
329
|
+
// For left/right placement, calculate vertical center alignment
|
|
330
|
+
if (align === 'start') {
|
|
331
|
+
position.top = y - hostPosition.y;
|
|
332
|
+
} else if (align === 'end') {
|
|
333
|
+
position.top = y + height - tooltipHeight - hostPosition.y;
|
|
334
|
+
} else {
|
|
335
|
+
// center
|
|
336
|
+
position.top = y + height / 2 - tooltipHeight / 2 - hostPosition.y;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Horizontal positioning (relative to portal host)
|
|
341
|
+
if (placement === 'left') {
|
|
342
|
+
position.left = x - tooltipWidth - offset - hostPosition.x;
|
|
343
|
+
} else if (placement === 'right') {
|
|
344
|
+
position.left = x + width + offset - hostPosition.x;
|
|
345
|
+
} else {
|
|
346
|
+
// For top/bottom placement, calculate horizontal alignment
|
|
347
|
+
if (align === 'start') {
|
|
348
|
+
position.left = x - hostPosition.x;
|
|
349
|
+
} else if (align === 'end') {
|
|
350
|
+
position.left = x + width - tooltipWidth - hostPosition.x;
|
|
351
|
+
} else {
|
|
352
|
+
// center
|
|
353
|
+
position.left = x + width / 2 - tooltipWidth / 2 - hostPosition.x;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return position;
|
|
358
|
+
}, [anchorPosition, tooltipSize, placement, align, offset, hostPosition]);
|
|
359
|
+
|
|
360
|
+
const placementStyle = getTooltipPosition; // This IS the memoized position object!
|
|
361
|
+
|
|
362
|
+
// DEBUG: Log values to troubleshoot positioning
|
|
363
|
+
if (__DEV__ && currentVisible) {
|
|
364
|
+
console.log('[Tooltip Debug]', {
|
|
365
|
+
anchorPosition,
|
|
366
|
+
tooltipSize,
|
|
367
|
+
placement,
|
|
368
|
+
align,
|
|
369
|
+
placementStyle,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
179
372
|
|
|
180
373
|
const centerVerticalOffset =
|
|
181
|
-
(
|
|
182
|
-
|
|
183
|
-
const alignStyle: ViewStyle =
|
|
184
|
-
placement === 'top' || placement === 'bottom'
|
|
185
|
-
? align === 'start'
|
|
186
|
-
? { left: 0 }
|
|
187
|
-
: align === 'end'
|
|
188
|
-
? { right: 0 }
|
|
189
|
-
: { alignSelf: 'center' }
|
|
190
|
-
: align === 'start'
|
|
191
|
-
? { top: 0 }
|
|
192
|
-
: align === 'end'
|
|
193
|
-
? { bottom: 0 }
|
|
194
|
-
: { top: centerVerticalOffset };
|
|
374
|
+
(anchorPosition.height || 0) / 2 - (tooltipSize.height || 0) / 2;
|
|
375
|
+
|
|
376
|
+
const alignStyle: ViewStyle = {}; // No longer needed - position calculated in getTooltipPosition
|
|
195
377
|
|
|
196
378
|
const resolvedTextColor = Colors.black_01;
|
|
197
379
|
|
|
@@ -391,28 +573,67 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
391
573
|
}
|
|
392
574
|
};
|
|
393
575
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
576
|
+
// Register tooltip with portal when visible
|
|
577
|
+
// Note: content is created fresh each time to ensure up-to-date styles/props
|
|
578
|
+
useEffect(() => {
|
|
579
|
+
let isActive = true; // Track if effect is still active
|
|
580
|
+
|
|
581
|
+
if (!currentVisible) {
|
|
582
|
+
portal.unregister(portalId, true); // Immediate cleanup when hiding
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Only render tooltip if anchor has been measured (not initial 0,0,0,0)
|
|
587
|
+
const hasMeasurement =
|
|
588
|
+
anchorPosition.x !== 0 ||
|
|
589
|
+
anchorPosition.y !== 0 ||
|
|
590
|
+
anchorPosition.width !== 0 ||
|
|
591
|
+
anchorPosition.height !== 0;
|
|
592
|
+
|
|
593
|
+
if (!hasMeasurement) {
|
|
594
|
+
console.log('[Tooltip] Waiting for anchor measurement...');
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
console.log('[Tooltip] Registering with portal, position:', placementStyle);
|
|
599
|
+
|
|
600
|
+
const content = (
|
|
601
|
+
<Animated.View
|
|
602
|
+
pointerEvents="auto"
|
|
603
|
+
style={[
|
|
604
|
+
{
|
|
605
|
+
maxWidth: SCREEN_WIDTH - Spacing.M * 2,
|
|
606
|
+
},
|
|
607
|
+
containerStyle,
|
|
608
|
+
styles.tooltip,
|
|
609
|
+
placementStyle,
|
|
610
|
+
alignStyle,
|
|
611
|
+
{
|
|
612
|
+
opacity: animatedValue,
|
|
613
|
+
transform: [translate],
|
|
614
|
+
},
|
|
615
|
+
]}
|
|
616
|
+
onLayout={event => {
|
|
617
|
+
if (isActive) {
|
|
618
|
+
setTooltipSize(event.nativeEvent.layout);
|
|
619
|
+
}
|
|
620
|
+
}}
|
|
621
|
+
>
|
|
622
|
+
{renderContent()}
|
|
623
|
+
<View style={getArrowStyle()} />
|
|
624
|
+
</Animated.View>
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
portal.register(portalId, content);
|
|
628
|
+
|
|
629
|
+
return () => {
|
|
630
|
+
isActive = false; // Mark as inactive immediately
|
|
631
|
+
portal.unregister(portalId, true); // Always use immediate cleanup on unmount
|
|
632
|
+
};
|
|
633
|
+
// Only depend on position-affecting values
|
|
634
|
+
// Content (title, description, buttons) updates via React reconciliation automatically
|
|
635
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
636
|
+
}, [currentVisible, anchorPosition, placement, align]);
|
|
416
637
|
|
|
417
638
|
return (
|
|
418
639
|
<View
|
|
@@ -420,13 +641,17 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
420
641
|
style={styles.container}
|
|
421
642
|
accessibilityLabel={componentId}
|
|
422
643
|
>
|
|
423
|
-
<View
|
|
644
|
+
<View ref={anchorRef} style={{ alignSelf: 'flex-start' }}>
|
|
424
645
|
{children}
|
|
425
646
|
</View>
|
|
426
|
-
{tooltipNode}
|
|
427
647
|
</View>
|
|
428
648
|
);
|
|
429
649
|
});
|
|
430
650
|
|
|
431
651
|
export { Tooltip };
|
|
432
652
|
export type { TooltipProps, TooltipPlacement, TooltipRef };
|
|
653
|
+
export {
|
|
654
|
+
TooltipPortalProvider,
|
|
655
|
+
TooltipPortalHost,
|
|
656
|
+
useTooltipPortal,
|
|
657
|
+
} from './TooltipPortal';
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
2
|
+
"name": "@momo-kits/animated-tooltip",
|
|
3
|
+
"version": "0.154.2-beta.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "index.tsx",
|
|
6
|
+
"dependencies": {},
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"@momo-kits/foundation": "latest",
|
|
9
|
+
"react": "*",
|
|
10
|
+
"react-native": "*"
|
|
11
|
+
},
|
|
12
|
+
"license": "MoMo",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"registry": "https://registry.npmjs.org/"
|
|
15
|
+
}
|
|
16
|
+
}
|