@momo-kits/animated-tooltip 0.154.1-beta.9 → 0.154.1-tooltip.22
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.tsx +22 -0
- package/PortalContext.tsx +77 -0
- package/index.tsx +84 -36
- package/package.json +15 -15
- package/styles.ts +10 -10
- package/types.ts +7 -2
package/Portal.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FC, ReactNode, useEffect, useRef } from 'react';
|
|
2
|
+
import { usePortal } from './PortalContext';
|
|
3
|
+
|
|
4
|
+
type PortalProps = {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const Portal: FC<PortalProps> = ({ children }) => {
|
|
9
|
+
const { addPortal, removePortal } = usePortal();
|
|
10
|
+
const keyRef = useRef(`portal-${Math.random().toString(36).substr(2, 9)}`);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const key = keyRef.current;
|
|
14
|
+
addPortal(key, children);
|
|
15
|
+
|
|
16
|
+
return () => {
|
|
17
|
+
removePortal(key);
|
|
18
|
+
};
|
|
19
|
+
}, [children, addPortal, removePortal]);
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { View, StyleSheet } from 'react-native';
|
|
9
|
+
|
|
10
|
+
type PortalContextValue = {
|
|
11
|
+
addPortal: (key: string, element: ReactNode) => void;
|
|
12
|
+
removePortal: (key: string) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PortalContext = createContext<PortalContextValue | null>(null);
|
|
16
|
+
|
|
17
|
+
export const PortalProvider: React.FC<{ children: ReactNode }> = ({
|
|
18
|
+
children,
|
|
19
|
+
}) => {
|
|
20
|
+
const [portals, setPortals] = useState<Map<string, ReactNode>>(new Map());
|
|
21
|
+
|
|
22
|
+
const addPortal = useCallback((key: string, element: ReactNode) => {
|
|
23
|
+
setPortals(prev => {
|
|
24
|
+
const next = new Map(prev);
|
|
25
|
+
next.set(key, element);
|
|
26
|
+
return next;
|
|
27
|
+
});
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const removePortal = useCallback((key: string) => {
|
|
31
|
+
setPortals(prev => {
|
|
32
|
+
const next = new Map(prev);
|
|
33
|
+
next.delete(key);
|
|
34
|
+
return next;
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<PortalContext.Provider value={{ addPortal, removePortal }}>
|
|
40
|
+
<View style={styles.container}>
|
|
41
|
+
{children}
|
|
42
|
+
<View
|
|
43
|
+
style={styles.portalHost}
|
|
44
|
+
pointerEvents="box-none"
|
|
45
|
+
collapsable={false}
|
|
46
|
+
renderToHardwareTextureAndroid
|
|
47
|
+
shouldRasterizeIOS
|
|
48
|
+
>
|
|
49
|
+
{Array.from(portals.values())}
|
|
50
|
+
</View>
|
|
51
|
+
</View>
|
|
52
|
+
</PortalContext.Provider>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const styles = StyleSheet.create({
|
|
57
|
+
container: {
|
|
58
|
+
flex: 1,
|
|
59
|
+
},
|
|
60
|
+
portalHost: {
|
|
61
|
+
position: 'absolute',
|
|
62
|
+
top: 0,
|
|
63
|
+
left: 0,
|
|
64
|
+
right: 0,
|
|
65
|
+
bottom: 0,
|
|
66
|
+
zIndex: 999999,
|
|
67
|
+
elevation: 999,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const usePortal = () => {
|
|
72
|
+
const context = useContext(PortalContext);
|
|
73
|
+
if (!context) {
|
|
74
|
+
throw new Error('usePortal must be used within PortalProvider');
|
|
75
|
+
}
|
|
76
|
+
return context;
|
|
77
|
+
};
|
package/index.tsx
CHANGED
|
@@ -7,7 +7,14 @@ import React, {
|
|
|
7
7
|
useRef,
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Animated,
|
|
12
|
+
Easing,
|
|
13
|
+
TouchableOpacity,
|
|
14
|
+
useWindowDimensions,
|
|
15
|
+
View,
|
|
16
|
+
ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
11
18
|
import {
|
|
12
19
|
Colors,
|
|
13
20
|
Icon,
|
|
@@ -19,6 +26,7 @@ import {
|
|
|
19
26
|
import styles from './styles';
|
|
20
27
|
import { TooltipPlacement, TooltipProps, TooltipRef } from './types';
|
|
21
28
|
import { IconButton, PrimaryButton, SecondaryButton } from './TooltipButtons';
|
|
29
|
+
import { Portal } from './Portal';
|
|
22
30
|
|
|
23
31
|
const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
24
32
|
{
|
|
@@ -36,11 +44,13 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
36
44
|
accessibilityLabel,
|
|
37
45
|
onVisibleChange,
|
|
38
46
|
onPressClose,
|
|
47
|
+
containerStyle,
|
|
39
48
|
},
|
|
40
49
|
ref,
|
|
41
50
|
) {
|
|
42
51
|
const app = useContext<any>(MiniAppContext);
|
|
43
52
|
const screen = useContext<any>(ScreenContext);
|
|
53
|
+
const SCREEN_WIDTH = useWindowDimensions().width;
|
|
44
54
|
const arrowSize = 6;
|
|
45
55
|
const [tooltipSize, setTooltipSize] = useState({ width: 0, height: 0 });
|
|
46
56
|
|
|
@@ -61,6 +71,8 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
61
71
|
const showTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
62
72
|
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
63
73
|
const [anchorSize, setAnchorSize] = useState({ width: 0, height: 0 });
|
|
74
|
+
const [anchorPosition, setAnchorPosition] = useState({ x: 0, y: 0 });
|
|
75
|
+
const anchorRef = useRef<View>(null);
|
|
64
76
|
|
|
65
77
|
const currentVisible = isControlled ? !!visible : internalVisible;
|
|
66
78
|
|
|
@@ -91,6 +103,13 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
91
103
|
|
|
92
104
|
useEffect(() => {
|
|
93
105
|
animate(currentVisible);
|
|
106
|
+
// Measure vị trí absolute của anchor khi tooltip visible
|
|
107
|
+
if (currentVisible && anchorRef.current) {
|
|
108
|
+
anchorRef.current.measureInWindow((x, y, width, height) => {
|
|
109
|
+
setAnchorPosition({ x, y });
|
|
110
|
+
setAnchorSize({ width, height });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
94
113
|
}, [currentVisible, animationDuration]);
|
|
95
114
|
|
|
96
115
|
useEffect(() => {
|
|
@@ -157,34 +176,54 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
157
176
|
}),
|
|
158
177
|
};
|
|
159
178
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
// Tính toán vị trí absolute của tooltip
|
|
180
|
+
const getTooltipPosition = (): ViewStyle => {
|
|
181
|
+
const { x, y } = anchorPosition;
|
|
182
|
+
const { width: anchorWidth, height: anchorHeight } = anchorSize;
|
|
183
|
+
const { width: tooltipWidth, height: tooltipHeight } = tooltipSize;
|
|
184
|
+
|
|
185
|
+
let top = 0;
|
|
186
|
+
let left = 0;
|
|
187
|
+
|
|
188
|
+
// Tính theo placement
|
|
189
|
+
if (placement === 'top') {
|
|
190
|
+
top = y - tooltipHeight - offset;
|
|
191
|
+
left = x;
|
|
192
|
+
} else if (placement === 'bottom') {
|
|
193
|
+
top = y + anchorHeight + offset;
|
|
194
|
+
left = x;
|
|
195
|
+
} else if (placement === 'left') {
|
|
196
|
+
top = y;
|
|
197
|
+
left = x - tooltipWidth - offset;
|
|
198
|
+
} else {
|
|
199
|
+
// right
|
|
200
|
+
top = y;
|
|
201
|
+
left = x + anchorWidth + offset;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Adjust theo align
|
|
205
|
+
if (placement === 'top' || placement === 'bottom') {
|
|
206
|
+
if (align === 'center') {
|
|
207
|
+
left = x + anchorWidth / 2 - tooltipWidth / 2;
|
|
208
|
+
} else if (align === 'end') {
|
|
209
|
+
left = x + anchorWidth - tooltipWidth;
|
|
210
|
+
}
|
|
211
|
+
// align === 'start' thì giữ nguyên left = x
|
|
212
|
+
} else {
|
|
213
|
+
// left hoặc right
|
|
214
|
+
if (align === 'center') {
|
|
215
|
+
top = y + anchorHeight / 2 - tooltipHeight / 2;
|
|
216
|
+
} else if (align === 'end') {
|
|
217
|
+
top = y + anchorHeight - tooltipHeight;
|
|
218
|
+
}
|
|
219
|
+
// align === 'start' thì giữ nguyên top = y
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { top, left };
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const tooltipPosition = getTooltipPosition();
|
|
226
|
+
|
|
188
227
|
const resolvedTextColor = Colors.black_01;
|
|
189
228
|
|
|
190
229
|
const renderButtons = () => {
|
|
@@ -275,7 +314,12 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
275
314
|
return (
|
|
276
315
|
<View style={styles.content}>
|
|
277
316
|
<View style={styles.contentHeader}>
|
|
278
|
-
<View
|
|
317
|
+
<View
|
|
318
|
+
style={{
|
|
319
|
+
minWidth: 100,
|
|
320
|
+
maxWidth: SCREEN_WIDTH - Spacing.M * 2 - 48,
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
279
323
|
{title ? (
|
|
280
324
|
<Text
|
|
281
325
|
typography={'header_s_semibold'}
|
|
@@ -319,7 +363,7 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
319
363
|
{
|
|
320
364
|
width: size * 2,
|
|
321
365
|
height: size * 2,
|
|
322
|
-
backgroundColor:
|
|
366
|
+
backgroundColor: Colors.black_17,
|
|
323
367
|
transform: [{ rotate: '45deg' }],
|
|
324
368
|
borderRadius: size / 2,
|
|
325
369
|
},
|
|
@@ -381,14 +425,17 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
381
425
|
const tooltipNode = (
|
|
382
426
|
<Animated.View
|
|
383
427
|
pointerEvents="auto"
|
|
428
|
+
collapsable={false}
|
|
384
429
|
style={[
|
|
430
|
+
{
|
|
431
|
+
maxWidth: SCREEN_WIDTH - Spacing.M * 2,
|
|
432
|
+
},
|
|
433
|
+
containerStyle,
|
|
385
434
|
styles.tooltip,
|
|
386
|
-
|
|
387
|
-
alignStyle,
|
|
435
|
+
tooltipPosition,
|
|
388
436
|
{
|
|
389
437
|
opacity: animatedValue,
|
|
390
438
|
transform: [translate],
|
|
391
|
-
backgroundColor: resolvedBackground,
|
|
392
439
|
},
|
|
393
440
|
]}
|
|
394
441
|
onLayout={event => setTooltipSize(event.nativeEvent.layout)}
|
|
@@ -404,13 +451,14 @@ const Tooltip = forwardRef<TooltipRef, TooltipProps>(function Tooltip(
|
|
|
404
451
|
style={styles.container}
|
|
405
452
|
accessibilityLabel={componentId}
|
|
406
453
|
>
|
|
407
|
-
<View
|
|
454
|
+
<View ref={anchorRef}>
|
|
408
455
|
{children}
|
|
409
456
|
</View>
|
|
410
|
-
{tooltipNode}
|
|
457
|
+
{currentVisible && <Portal>{tooltipNode}</Portal>}
|
|
411
458
|
</View>
|
|
412
459
|
);
|
|
413
460
|
});
|
|
414
461
|
|
|
415
462
|
export { Tooltip };
|
|
463
|
+
export { PortalProvider } from './PortalContext';
|
|
416
464
|
export type { TooltipProps, TooltipPlacement, TooltipRef };
|
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.1-tooltip.22",
|
|
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
|
+
}
|
package/styles.ts
CHANGED
|
@@ -7,11 +7,15 @@ export default StyleSheet.create({
|
|
|
7
7
|
},
|
|
8
8
|
tooltip: {
|
|
9
9
|
position: 'absolute',
|
|
10
|
-
zIndex:
|
|
10
|
+
zIndex: 999999,
|
|
11
|
+
elevation: 999,
|
|
11
12
|
padding: Spacing.M,
|
|
12
13
|
backgroundColor: Colors.black_17,
|
|
13
14
|
borderRadius: Radius.S,
|
|
14
|
-
|
|
15
|
+
shadowColor: '#000',
|
|
16
|
+
shadowOffset: { width: 0, height: 4 },
|
|
17
|
+
shadowOpacity: 0.3,
|
|
18
|
+
shadowRadius: 8,
|
|
15
19
|
},
|
|
16
20
|
text: {
|
|
17
21
|
color: Colors.black_01,
|
|
@@ -25,20 +29,19 @@ export default StyleSheet.create({
|
|
|
25
29
|
flexDirection: 'column',
|
|
26
30
|
},
|
|
27
31
|
title: {
|
|
28
|
-
marginBottom: Spacing.XS
|
|
32
|
+
marginBottom: Spacing.XS,
|
|
29
33
|
},
|
|
30
34
|
description: {
|
|
31
|
-
marginBottom: Spacing.
|
|
35
|
+
marginBottom: Spacing.M,
|
|
32
36
|
},
|
|
33
37
|
buttonsRow: {
|
|
34
38
|
flexDirection: 'row',
|
|
35
39
|
flexWrap: 'wrap',
|
|
36
40
|
justifyContent: 'flex-end',
|
|
37
|
-
marginTop: Spacing.M,
|
|
38
41
|
},
|
|
39
42
|
button: {
|
|
40
|
-
marginRight: Spacing.
|
|
41
|
-
marginBottom: Spacing.
|
|
43
|
+
marginRight: Spacing.XXS,
|
|
44
|
+
marginBottom: Spacing.XXS,
|
|
42
45
|
},
|
|
43
46
|
textButton: {},
|
|
44
47
|
iconButton: {},
|
|
@@ -53,9 +56,6 @@ export default StyleSheet.create({
|
|
|
53
56
|
contentHeader: {
|
|
54
57
|
flexDirection: 'row',
|
|
55
58
|
},
|
|
56
|
-
contentTextContainer: {
|
|
57
|
-
flex: 1,
|
|
58
|
-
},
|
|
59
59
|
closeIconSpacing: {
|
|
60
60
|
marginLeft: Spacing.S,
|
|
61
61
|
},
|
package/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {ReactNode} from 'react';
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ViewStyle } from 'react-native';
|
|
2
3
|
|
|
3
4
|
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
4
5
|
|
|
@@ -77,5 +78,9 @@ export type TooltipProps = {
|
|
|
77
78
|
* Triggered when the close button (X) is pressed.
|
|
78
79
|
*/
|
|
79
80
|
onPressClose?: () => void;
|
|
80
|
-
};
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Tooltip container style.
|
|
84
|
+
*/
|
|
85
|
+
containerStyle?: ViewStyle;
|
|
86
|
+
};
|