diff --git a/README.md b/README.md
index 3ae1ca2..30a72c8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
-# react-native-good-tooltip
+# React Native Good Tooltip
-This tooltip was created with the best UX in mind.
+Tooltips do not interrupt the user's flow. I'm not positive about the flow of using the app after touching the tooltip to close it.
+So this component doesn't use Modal.
## Installation
@@ -10,15 +11,57 @@ npm install react-native-good-tooltip
## Usage
+**⚠️Warning⚠️️**
+This component will need to be used with styles (z-Index and Overflow).
+You will immediately see the need for zIndex if you use "bottom" placement.
+Please refer to the example project and video.
+
+```tsx
+import Tooltip from 'react-native-good-tooltip';
-```js
-import { multiply } from 'react-native-good-tooltip';
// ...
-const result = await multiply(3, 7);
+
+ {/* your component */}
+
```
+## Video
+
+
+## Props
+
+
+## Props
+| Prop | Type | Default | Description |
+|------------------------|----------------------------------------------------------------------|----------------------------------------|-----------------------------------------------------------------------------------------------|
+| `placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | **required** | The position of the tooltip relative to the anchor. |
+| `anchor` | `'center' \| 'left' \| 'right' \| 'top' \| 'bottom'` | `'center'` | The alignment of the tooltip relative to the anchor. |
+| `text` | `string \| React.ReactElement` | **required** | The content of the tooltip. |
+| `isVisible` | `boolean` | **required** | Whether the tooltip is visible. |
+| `offset` | `{ position?: { x?: number, y?: number }, arrow?: { x?: number, y?: number } }` | `undefined` | The offset for the tooltip and arrow position. |
+| `arrowElement` | `React.ReactElement` | `undefined` | Custom arrow element. |
+| `styles` | `{color?: ColorValue,containerStyle?: ViewStyle,tooltipStyle?: ViewStyle,arrowSize?: { width?: number, height?: number },closeSize?: { width?: number, height?: number} }` | `undefined` | Custom styles for the tooltip. |
+| `children` | `React.ReactElement` | `undefined` | The element to which the tooltip is anchored. |
+| `onPress` | `() => void` | `undefined` | Function to call when the tooltip is pressed. |
+| `onVisibleChange` | `(isVisible: boolean) => void` | `undefined` | Function to call when the visibility of the tooltip changes. |
+| `disableAutoHide` | `boolean` | `false` | Whether to disable the auto-hide feature. |
+| `delayShowTime` | `number` | `0` | The delay time before showing the tooltip. |
+| `autoHideTime` | `number` | `5000` | The time after which the tooltip will automatically hide. |
+| `requiredConfirmation` | `boolean` | `false` | Whether the tooltip requires confirmation to hide. |
## Contributing
diff --git a/assets/close.png b/assets/close.png
new file mode 100644
index 0000000..81b467b
Binary files /dev/null and b/assets/close.png differ
diff --git a/example/src/App.tsx b/example/src/App.tsx
index c90d28b..73244aa 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,30 +1,194 @@
-import { useState, useEffect } from 'react';
-import { StyleSheet, View, Text } from 'react-native';
-import { multiply } from 'react-native-good-tooltip';
+import { FlatList, SafeAreaView, StyleSheet, Text, View } from 'react-native';
+import Tooltip from 'react-native-good-tooltip';
export default function App() {
- const [result, setResult] = useState();
-
- useEffect(() => {
- multiply(3, 7).then(setResult);
- }, []);
+ const data = [
+ 'In FlatList',
+ 'zIndex must be specified using',
+ 'CellRendererComponent.',
+ ];
return (
-
- Result: {result}
-
+
+ {/* Header*/}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This Tooltip component does not use modal, so you must specify
+ z-Index. Especially if the placement is bottom, you need to be
+ especially careful.
+
+
+
+
+ This Tooltip component does not use modal, so you must specify
+ z-Index. Especially if the placement is bottom, you need to be
+ especially careful.
+
+
+
+
+ {/* Body*/}
+ {
+ return (
+
+ );
+ }}
+ renderItem={({ item, index }) => (
+ {
+ if (index % 3 === 0) return 'right';
+ if (index % 2 === 0) return 'center';
+
+ return 'left';
+ })()}
+ text={'List Item Tooltip ' + index}
+ >
+
+ {`${item}`}
+
+
+ )}
+ ListFooterComponent={() => (
+
+
+
+
+ There may be situations where you need to add the overflow:
+ 'visible' style.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ />
+
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
+ marginVertical: 8,
+ marginHorizontal: 16,
},
box: {
- width: 60,
- height: 60,
- marginVertical: 20,
+ width: 50,
+ height: 50,
+ },
+ message: {
+ padding: 8,
},
});
diff --git a/package.json b/package.json
index eec9bea..8f94fe7 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"scripts": {
"example": "yarn workspace react-native-good-tooltip-example",
"test": "jest",
+ "postinstall": "npx patch-package",
"typecheck": "tsc",
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"clean": "del-cli lib",
@@ -181,5 +182,8 @@
"create-react-native-library": {
"type": "library",
"version": "0.41.0"
+ },
+ "dependencies": {
+ "react-native-anchor-point": "^1.0.6"
}
}
diff --git a/patches/react-native-anchor-point+1.0.6.patch b/patches/react-native-anchor-point+1.0.6.patch
new file mode 100644
index 0000000..fc086c3
--- /dev/null
+++ b/patches/react-native-anchor-point+1.0.6.patch
@@ -0,0 +1,15 @@
+diff --git a/node_modules/react-native-anchor-point/index.ts b/node_modules/react-native-anchor-point/index.ts
+index f789fa7..a1a97c6 100644
+--- a/node_modules/react-native-anchor-point/index.ts
++++ b/node_modules/react-native-anchor-point/index.ts
+@@ -33,8 +33,10 @@ export const withAnchorPoint = (transform: TransformsStyle, anchorPoint: Point,
+ shiftTranslateX.push({
+ translateX: size.width * (anchorPoint.x - defaultAnchorPoint.x),
+ });
++ // @ts-ignore
+ injectedTransform = [...shiftTranslateX, ...injectedTransform];
+ // shift after rotation
++ // @ts-ignore
+ injectedTransform.push({
+ translateX: size.width * (defaultAnchorPoint.x - anchorPoint.x),
+ });
diff --git a/src/functions.ts b/src/functions.ts
new file mode 100644
index 0000000..5f10376
--- /dev/null
+++ b/src/functions.ts
@@ -0,0 +1,69 @@
+export const getAnchorPoint = (placement: string, anchor: string) => {
+ let x = 0;
+ let y = 0;
+
+ // vertical
+ if (placement === 'top' && anchor === 'left') {
+ x = 0;
+ y = 1;
+ }
+ if (placement === 'top' && anchor === 'right') {
+ x = 1;
+ y = 1;
+ }
+ if (placement === 'top' && anchor === 'center') {
+ x = 0.5;
+ y = 1;
+ }
+
+ if (placement === 'bottom' && anchor === 'left') {
+ x = 0;
+ y = 0;
+ }
+ if (placement === 'bottom' && anchor === 'center') {
+ x = 0.5;
+ y = 0;
+ }
+ if (placement === 'bottom' && anchor === 'right') {
+ x = 1;
+ y = 0;
+ }
+
+ // horizontal
+ if (placement === 'left' && anchor === 'top') {
+ x = 1;
+ y = 0;
+ }
+ if (placement === 'left' && anchor === 'center') {
+ x = 1;
+ y = 0.5;
+ }
+ if (placement === 'left' && anchor === 'bottom') {
+ x = 1;
+ y = 1;
+ }
+ if (placement === 'right' && anchor === 'top') {
+ x = 0;
+ y = 0;
+ }
+ if (placement === 'right' && anchor === 'center') {
+ x = 0;
+ y = 0.5;
+ }
+ if (placement === 'right' && anchor === 'bottom') {
+ x = 0;
+ y = 1;
+ }
+
+ return { x, y };
+};
+
+export const createArrowShape = (width: number, height: number) => {
+ return {
+ borderLeftWidth: width / 2,
+ borderRightWidth: width / 2,
+ borderBottomWidth: height,
+ borderLeftColor: 'transparent',
+ borderRightColor: 'transparent',
+ };
+};
diff --git a/src/index.tsx b/src/index.tsx
index 9e42cf3..b74e165 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,3 +1,397 @@
-export function multiply(a: number, b: number): Promise {
- return Promise.resolve(a * b);
+import React, { useEffect, useState } from 'react';
+import {
+ Animated,
+ Dimensions,
+ Platform,
+ Text,
+ TouchableOpacity,
+ View,
+ type ColorValue,
+ type LayoutChangeEvent,
+ type ViewStyle,
+ Image,
+} from 'react-native';
+import { withAnchorPoint } from 'react-native-anchor-point';
+import { createArrowShape, getAnchorPoint } from './functions';
+
+// DEFAULT VALUES
+const SIDE_ARROW_INSET = 12;
+const CLOSE_ICON_SIZE = { width: 16, height: 16 };
+const ARROW_SIZE = { width: 10, height: 6 };
+const TOOLTIP_STYLE = {
+ width: Dimensions.get('window').width * 0.3,
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+};
+
+interface ToolTipProps {
+ placement: 'top' | 'bottom' | 'left' | 'right';
+ anchor?: 'center' | 'left' | 'right' | 'top' | 'bottom';
+ offset?: {
+ position?: {
+ x?: number;
+ y?: number;
+ };
+ arrow?: {
+ x?: number;
+ y?: number;
+ };
+ };
+ arrowElement?: React.ReactElement;
+ styles?: {
+ color?: ColorValue | 'primary';
+ containerStyle?: ViewStyle;
+ tooltipStyle?: ViewStyle;
+ arrowSize?: {
+ width?: number;
+ height?: number;
+ };
+ closeSize?: {
+ width?: number;
+ height?: number;
+ };
+ };
+ text: string | React.ReactElement;
+ children?: React.ReactElement;
+ isVisible: boolean;
+ onPress?: () => void;
+ onVisibleChange?: (isVisible: boolean) => void;
+ disableAutoHide?: boolean;
+ delayShowTime?: number;
+ autoHideTime?: number;
+ requiredConfirmation?: boolean;
}
+
+const Tooltip = ({
+ isVisible,
+ anchor = 'center',
+ styles = {
+ tooltipStyle: TOOLTIP_STYLE,
+ arrowSize: ARROW_SIZE,
+ closeSize: CLOSE_ICON_SIZE,
+ },
+ text,
+ children,
+ placement,
+ onPress,
+ offset,
+ arrowElement,
+ onVisibleChange,
+ disableAutoHide = false,
+ delayShowTime = 0,
+ autoHideTime = 5000,
+ requiredConfirmation = false,
+}: ToolTipProps) => {
+ const [currentIsVisible, setCurrentIsVisible] = useState(false);
+ const [tooltipPosition, setTooltipPosition] = useState({
+ top: 0,
+ left: Platform.OS === 'android' && placement === 'right' ? -4 : 0,
+ });
+ const [tooltipSize, setTooltipSize] = useState({ width: 0, height: 0 });
+ const animatedValue = new Animated.Value(0);
+
+ const isVerticalPlacement = placement === 'top' || placement === 'bottom';
+ const tooltipColor = (() => {
+ if (styles?.color) return styles?.color;
+
+ return '#3Eb489';
+ })();
+
+ const arrowStyle = {
+ ...createArrowShape(
+ styles?.arrowSize?.width || ARROW_SIZE.width,
+ styles?.arrowSize?.height || ARROW_SIZE.height
+ ),
+ borderBottomColor: tooltipColor,
+ };
+
+ useEffect(() => {
+ if (
+ autoHideTime > 0 &&
+ disableAutoHide === false &&
+ requiredConfirmation === false
+ ) {
+ setTimeout(() => {
+ // 기본적으로 5초 뒤 사라짐 처리
+ setCurrentIsVisible(false);
+ }, autoHideTime);
+ }
+ }, [autoHideTime, disableAutoHide, requiredConfirmation]);
+
+ useEffect(() => {
+ if (onVisibleChange) {
+ onVisibleChange(isVisible);
+ }
+ }, [isVisible, onVisibleChange]);
+
+ useEffect(() => {
+ setTimeout(() => {
+ setCurrentIsVisible(isVisible);
+ }, delayShowTime);
+ }, [delayShowTime, isVisible]);
+
+ const handleVerticalTooltipLayout = (event: LayoutChangeEvent) => {
+ const { width, height } = event.nativeEvent.layout;
+ if (placement === 'top') {
+ setTooltipPosition({ top: -height, left: 0 });
+ }
+ setTooltipSize({
+ width,
+ height,
+ });
+ };
+
+ const handleHorizontalTooltipLayout = (event: LayoutChangeEvent) => {
+ const { width, height } = event.nativeEvent.layout;
+
+ const newLeft = width - 4;
+ if (placement === 'left') {
+ setTooltipPosition({ top: 0, left: -newLeft });
+ }
+ setTooltipSize({
+ width,
+ height,
+ });
+ };
+
+ const renderVerticalArrow = () => (
+ {
+ if (anchor === 'left') return 'flex-start';
+ if (anchor === 'right') return 'flex-end';
+ return 'center';
+ })(),
+ },
+ styles?.containerStyle,
+ ]}
+ >
+ {
+ let x = SIDE_ARROW_INSET + (offset?.arrow?.x || 0);
+
+ if (!isVerticalPlacement) {
+ x = offset?.arrow?.x || 0;
+ }
+ if (anchor === 'center') {
+ x = offset?.arrow?.x || 0;
+ }
+ if (anchor === 'right') {
+ x = -x;
+ }
+
+ return x;
+ })(),
+ top: (() => {
+ let y = SIDE_ARROW_INSET + (offset?.arrow?.y || 0);
+
+ if (isVerticalPlacement) {
+ y = offset?.arrow?.y || 0;
+ }
+ if (anchor === 'center') {
+ y = offset?.arrow?.y || 0;
+ }
+ if (anchor === 'bottom') {
+ y = -y;
+ }
+
+ // 은찬: AOS에서 간혈적으로 박스와 화살표 사이에 틈이 보이는 이슈가 있어서 0.1 추가
+ return y + (Platform.OS === 'android' ? 0.1 : 0);
+ })(),
+ transform: (() => {
+ if (placement === 'bottom') return [{ rotate: '0deg' }];
+ if (placement === 'top') return [{ rotate: '180deg' }];
+ if (placement === 'left') return [{ rotate: '90deg' }];
+ if (placement === 'right') return [{ rotate: '-90deg' }];
+ return undefined;
+ })(),
+ }}
+ >
+ {arrowElement === undefined && arrowElement}
+
+
+ );
+
+ const renderHorizontalArrow = () => (
+ {
+ let left = 0;
+ if (placement === 'left') {
+ left = -2;
+ }
+ if (placement === 'right') {
+ left = 2;
+ }
+
+ // 은찬: AOS에서 간혈적으로 박스와 화살표 사이에 틈이 보이는 이슈가 있어서 0.1 추가
+ return left + (Platform.OS === 'android' ? 0.1 : 0);
+ })(),
+ top: (() => {
+ return 0;
+ })(),
+ transform: (() => {
+ if (placement === 'bottom') return [{ rotate: '0deg' }];
+ if (placement === 'top') return [{ rotate: '180deg' }];
+ if (placement === 'left') return [{ rotate: '90deg' }];
+ if (placement === 'right') return [{ rotate: '-90deg' }];
+ return undefined;
+ })(),
+ }}
+ >
+ {arrowElement === undefined && arrowElement}
+
+ );
+
+ const transformsStyle = (() => {
+ const { x, y } = getAnchorPoint(placement, anchor);
+
+ if (currentIsVisible) {
+ Animated.spring(animatedValue, {
+ toValue: 1,
+ speed: 6,
+ useNativeDriver: true,
+ }).start();
+ } else {
+ animatedValue.setValue(1);
+ Animated.timing(animatedValue, {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: true,
+ }).start();
+ }
+
+ return withAnchorPoint(
+ { transform: [{ scale: animatedValue }] },
+ { x, y },
+ { width: tooltipSize.width, height: tooltipSize.height }
+ );
+ })();
+
+ const renderTooltipContent = () => (
+ // View Overflow 영역에 있는 Tooltip을 선택하기 위해 TouchableOpacity 사용
+ {
+ setCurrentIsVisible((prevState) => !prevState);
+ onPress && onPress();
+ }}
+ style={{
+ backgroundColor: tooltipColor,
+ borderRadius: 10,
+ ...(styles?.tooltipStyle || TOOLTIP_STYLE),
+ }}
+ >
+
+ {/* content */}
+ {typeof text !== 'string' ? (
+ text
+ ) : (
+
+ )}
+
+ {requiredConfirmation && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+
+ const renderVerticalTooltip = () => (
+ {
+ if (anchor === 'left') return 'flex-start';
+ if (anchor === 'right') return 'flex-end';
+ return 'center';
+ })(),
+ top: tooltipPosition.top + (offset?.position?.y || 0),
+ left: tooltipPosition.left + (offset?.position?.x || 0),
+ }}
+ >
+
+ {placement === 'top' && renderTooltipContent()}
+ {renderVerticalArrow()}
+ {placement === 'bottom' && renderTooltipContent()}
+
+
+ );
+ const renderHorizontalTooltip = () => (
+ // 툴팁 & 화살표 정렬 기준 ( top, center, bottom )
+ {
+ if (anchor === 'top') return 'flex-start';
+ if (anchor === 'bottom') return 'flex-end';
+ return 'center';
+ })(),
+ }}
+ >
+
+ {placement === 'left' && renderTooltipContent()}
+ {renderHorizontalArrow()}
+ {placement === 'right' && renderTooltipContent()}
+
+
+ );
+
+ return (
+
+ {placement === 'top' && renderVerticalTooltip()}
+ {isVerticalPlacement ? (
+ children
+ ) : (
+
+ {placement === 'left' && renderHorizontalTooltip()}
+ {children}
+ {placement === 'right' && renderHorizontalTooltip()}
+
+ )}
+ {placement === 'bottom' && renderVerticalTooltip()}
+
+ );
+};
+
+export default Tooltip;