@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 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 { Animated, Easing, TouchableOpacity, View, ViewStyle } from 'react-native';
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
- const placementStyle: {
161
- [key: string]: any;
162
- } =
163
- placement === 'top'
164
- ? { bottom: (anchorSize.height || 0) + offset }
165
- : placement === 'bottom'
166
- ? { top: (anchorSize.height || 0) + offset }
167
- : placement === 'left'
168
- ? { right: (anchorSize.width || 0) + offset }
169
- : { left: (anchorSize.width || 0) + offset };
170
-
171
- const centerVerticalOffset =
172
- (anchorSize.height || 0) / 2 - (tooltipSize.height || 0) / 2;
173
-
174
- const alignStyle: ViewStyle =
175
- placement === 'top' || placement === 'bottom'
176
- ? align === 'start'
177
- ? { left: 0 }
178
- : align === 'end'
179
- ? { right: 0 }
180
- : { alignSelf: 'center' }
181
- : align === 'start'
182
- ? { top: 0 }
183
- : align === 'end'
184
- ? { bottom: 0 }
185
- : { top: centerVerticalOffset };
186
-
187
- const resolvedBackground = Colors.black_17;
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 style={styles.contentTextContainer}>
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: resolvedBackground,
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
- placementStyle,
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 onLayout={event => setAnchorSize(event.nativeEvent.layout)}>
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
- "name": "@momo-kits/animated-tooltip",
3
- "version": "0.154.1-beta.9",
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
- }
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,
10
+ zIndex: 999999,
11
+ elevation: 999,
11
12
  padding: Spacing.M,
12
13
  backgroundColor: Colors.black_17,
13
14
  borderRadius: Radius.S,
14
- width: 256,
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 / 2,
32
+ marginBottom: Spacing.XS,
29
33
  },
30
34
  description: {
31
- marginBottom: Spacing.XS / 2,
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.XS / 2,
41
- marginBottom: Spacing.XS / 2,
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
+ };