diff --git a/README.md b/README.md
index 65b03ad..148e609 100644
--- a/README.md
+++ b/README.md
@@ -2,41 +2,44 @@
Tooltip for React Native using React Native Reanimated and Modal
-![Demo 1](./demo/1.gif)
+![Demo 1](./demo.gif)
## Installation
```sh
-npm install react-native-reanimated react-native-reanimated-tooltip
+npm install react-native-reanimated @gorhom/portal react-native-reanimated-tooltip
```
## Usage
```ts
+import { PortalProvider } from '@gorhom/portal';
import React from 'react';
import { Text, Button } from 'react-native';
import { Tooltip } from 'react-native-reanimated-tooltip';
import { FadeOut, FadeIn } from 'react-native-reanimated';
const [visible, setVisible] = React.useState(false);
-Tooltip
- }
- visible={visible}
- onPress={() => {
- setVisible(false);
- }}
- entering={FadeIn}
- exiting={FadeOut}
->
-
+
```
@@ -44,14 +47,6 @@ const [visible, setVisible] = React.useState(false);
Check [TooltipProps](https://github.com/johankasperi/react-native-reanimated-tooltip/blob/efd333ae9dea7d1705a8828f2a82ba65338956f2/src/Tooltip.tsx#L29)
-## Demo
-
-![Demo 2](./demo/2.gif)
-![Demo 3](./demo/3.gif)
-![Demo 4](./demo/4.gif)
-![Demo 5](./demo/5.gif)
-![Demo 6](./demo/6.gif)
-
## Contributing
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
diff --git a/demo.gif b/demo.gif
new file mode 100644
index 0000000..36b910a
Binary files /dev/null and b/demo.gif differ
diff --git a/demo/1.gif b/demo/1.gif
deleted file mode 100644
index 6d65e11..0000000
Binary files a/demo/1.gif and /dev/null differ
diff --git a/demo/2.gif b/demo/2.gif
deleted file mode 100644
index 3a06f54..0000000
Binary files a/demo/2.gif and /dev/null differ
diff --git a/demo/3.gif b/demo/3.gif
deleted file mode 100644
index c728255..0000000
Binary files a/demo/3.gif and /dev/null differ
diff --git a/demo/4.gif b/demo/4.gif
deleted file mode 100644
index 3b52282..0000000
Binary files a/demo/4.gif and /dev/null differ
diff --git a/demo/5.gif b/demo/5.gif
deleted file mode 100644
index 79db411..0000000
Binary files a/demo/5.gif and /dev/null differ
diff --git a/demo/6.gif b/demo/6.gif
deleted file mode 100644
index c68e53e..0000000
Binary files a/demo/6.gif and /dev/null differ
diff --git a/example/package.json b/example/package.json
index 296b5c6..736062f 100644
--- a/example/package.json
+++ b/example/package.json
@@ -9,6 +9,7 @@
"web": "expo start --web"
},
"dependencies": {
+ "@gorhom/portal": "^1.0.14",
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
"react": "18.2.0",
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 800cfee..fbe15de 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,3 +1,4 @@
+import { PortalProvider } from '@gorhom/portal';
import React from 'react';
import { StyleSheet, View, Text, Button } from 'react-native';
@@ -5,59 +6,54 @@ import { FadeIn, FadeOut } from 'react-native-reanimated';
import { Tooltip } from 'react-native-reanimated-tooltip';
export default function App() {
- const [activeTooltip, setActiveTooltip] = React.useState<1 | 2 | undefined>(
- 1
- );
+ const [tooltip1Active, setTooltip1Active] = React.useState(false);
+ const [tooltip2Active, setTooltip2Active] = React.useState(false);
return (
-
-
-
- Tooltip
- Body
- >
- }
- visible={activeTooltip === 1}
- onClose={() => {
- setActiveTooltip(2);
- }}
- entering={FadeIn}
- exiting={FadeOut.duration(1000)}
- >
- {
- setActiveTooltip(1);
- }}
- />
-
-
-
-
- Tooltip 2
- Body 2
- >
- }
- visible={activeTooltip === 2}
- onClose={() => {
- setActiveTooltip(undefined);
- }}
- entering={FadeIn}
- exiting={FadeOut}
- >
- {
- setActiveTooltip(2);
- }}
- />
-
+
+
+
+
+ Tooltip
+ Body
+ >
+ }
+ visible={tooltip1Active}
+ entering={FadeIn}
+ exiting={FadeOut}
+ >
+ {
+ setTooltip1Active(!tooltip1Active);
+ }}
+ />
+
+
+
+
+ Tooltip 2
+ Body 2
+ >
+ }
+ visible={tooltip2Active}
+ entering={FadeIn}
+ exiting={FadeOut}
+ >
+ {
+ setTooltip2Active(!tooltip2Active);
+ }}
+ />
+
+
-
+
);
}
diff --git a/package.json b/package.json
index 54ef8c5..a9d3d60 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"devDependencies": {
"@commitlint/config-conventional": "^17.0.2",
"@evilmartians/lefthook": "^1.5.0",
+ "@gorhom/portal": "^1.0.14",
"@react-native/eslint-config": "^0.72.2",
"@release-it/conventional-changelog": "^5.0.0",
"@types/jest": "^28.1.2",
@@ -75,9 +76,10 @@
"@types/react": "17.0.21"
},
"peerDependencies": {
+ "@gorhom/portal": "^1",
"react": "*",
"react-native": "*",
- "react-native-reanimated": "^2.0.0"
+ "react-native-reanimated": "^2"
},
"workspaces": [
"example"
diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx
index 4fbba19..10b764b 100644
--- a/src/Tooltip.tsx
+++ b/src/Tooltip.tsx
@@ -1,44 +1,43 @@
-import React, {
- type PropsWithChildren,
- useCallback,
- useEffect,
- useMemo,
- useState,
- useRef,
-} from 'react';
+import React, { type PropsWithChildren, useCallback } from 'react';
import {
- Modal,
StyleSheet,
- TouchableWithoutFeedback,
View,
type ColorValue,
type StyleProp,
type ViewStyle,
- useWindowDimensions,
} from 'react-native';
import Animated, {
- FadeOut,
measure,
- runOnJS,
- runOnUI,
type BaseAnimationBuilder,
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
+ runOnUI,
+ useDerivedValue,
} from 'react-native-reanimated';
+import { Portal } from '@gorhom/portal';
+import type { PortalProps } from '@gorhom/portal/lib/typescript/components/portal/types';
+import type { MeasuredDimensions } from 'react-native-reanimated';
import { Pointer } from './Pointer';
export interface TooltipProps {
+ portalHostName?: PortalProps['hostName'];
+
/** To show the tooltip. */
visible?: boolean;
- /** Style of the view parent view */
+ /** Style parent view wrapping your element */
style?: StyleProp;
+ /** Style of view wrapping the tooltip */
+ containerStyle?: StyleProp;
+
+ /** Style of the tooltip */
+ tooltipStyle?: StyleProp;
+
/** Component to be rendered as the display container. */
content?: React.ReactElement<{}>;
- /** Passes style object to tooltip container */
- containerStyle?: StyleProp;
+
/**
* Reanimated entering animation
* https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations/
@@ -58,209 +57,204 @@ export interface TooltipProps {
pointerSize?: number;
/** Pointer color */
pointerColor?: ColorValue;
-
- /** Callback when the is closed. */
- onClose?: () => void;
}
export const Tooltip = React.memo((props: PropsWithChildren) => {
const {
+ portalHostName,
visible = false,
style,
containerStyle,
+ content,
+ tooltipStyle,
entering,
exiting,
withPointer = true,
pointerStyle,
pointerSize = withPointer ? 8 : 0,
pointerColor = styles.defaultTooltip.backgroundColor,
- onClose,
+ children,
} = props;
- const [modalVisible, setModalVisible] = useState(visible);
- const [contentVisible, setContentVisible] = useState(visible);
-
- useEffect(() => {
- if (visible) {
- setModalVisible(true);
- }
- setContentVisible(visible);
- }, [visible]);
-
const element = useAnimatedRef();
const backdrop = useAnimatedRef();
const tooltip = useAnimatedRef();
- const pointerLayout = useSharedValue<{
- x?: number;
- y?: number;
- isDown?: boolean;
- }>({
- x: undefined,
- y: undefined,
- isDown: undefined,
- });
+ const elementDimensions = useSharedValue(null);
+ const backdropDimensions = useSharedValue(null);
+ const tooltipDimensions = useSharedValue(null);
- const tooltipLayout = useSharedValue<{ x?: number; y?: number }>({
- y: undefined,
- x: undefined,
+ const pointPosition = useDerivedValue<
+ | {
+ x: number;
+ y: number;
+ topHalfOfViewport: boolean;
+ }
+ | undefined
+ >(() => {
+ if (elementDimensions.value && backdropDimensions.value) {
+ const topHalfOfViewport =
+ elementDimensions.value.pageY + elementDimensions.value.height / 2 >=
+ backdropDimensions.value.height / 2;
+ const x =
+ elementDimensions.value.pageX + elementDimensions.value.width / 2;
+ const y =
+ elementDimensions.value.pageY +
+ (topHalfOfViewport ? -pointerSize : elementDimensions.value.height);
+
+ return { x, y, topHalfOfViewport };
+ }
+ return undefined;
});
- const setTooltipPosition = useCallback(() => {
- 'worklet';
- const elementDimensions = measure(element);
- const backdropDimensions = measure(backdrop);
- const tooltipDimensions = measure(tooltip);
+ const onElementLayout = useCallback(() => {
+ const measureWorklet = () => {
+ 'worklet';
+ elementDimensions.value = measure(element);
+ };
+ runOnUI(measureWorklet)();
+ }, [element, elementDimensions]);
+
+ const onBackdropLayout = useCallback(() => {
+ const measureWorklet = () => {
+ 'worklet';
+ backdropDimensions.value = measure(backdrop);
+ };
+ runOnUI(measureWorklet)();
+ }, [backdrop, backdropDimensions]);
+
+ const onTooltipLayout = useCallback(() => {
+ const measureWorklet = () => {
+ 'worklet';
+ tooltipDimensions.value = measure(tooltip);
+ };
+ runOnUI(measureWorklet)();
+ }, [tooltip, tooltipDimensions]);
- if (elementDimensions && backdropDimensions && tooltipDimensions) {
- const pointerDown =
- elementDimensions.pageY + elementDimensions.height / 2 >=
- backdropDimensions.height / 2;
- const pointX = elementDimensions.pageX + elementDimensions.width / 2;
- const pointY =
- elementDimensions.pageY +
- (pointerDown ? -pointerSize : elementDimensions.height);
- pointerLayout.value = {
- x: pointX,
- y: pointY,
- isDown: pointerDown,
+ const containerAnimatedStyle = useAnimatedStyle(() => {
+ if (pointPosition.value) {
+ return {
+ position: 'absolute',
+ top: pointPosition.value.y,
+ left: 0,
+ right: 0,
+ backgroundColor: 'transparent',
};
+ }
+ return {
+ position: 'absolute',
+ top: -10000,
+ left: 0,
+ right: 0,
+ };
+ });
- let tooltipX = pointX - tooltipDimensions.width / 2;
+ const tooltipAnimatedStyle = useAnimatedStyle(() => {
+ if (
+ backdropDimensions.value &&
+ tooltipDimensions.value &&
+ pointPosition.value
+ ) {
+ let tooltipX = pointPosition.value.x - tooltipDimensions.value.width / 2;
const tooltipOutsideRight =
- tooltipX + tooltipDimensions.width > backdropDimensions.width;
+ tooltipX + tooltipDimensions.value.width >
+ backdropDimensions.value.width;
if (tooltipOutsideRight) {
- tooltipX = backdropDimensions.width - tooltipDimensions.width;
+ tooltipX =
+ backdropDimensions.value.width - tooltipDimensions.value.width;
}
const tooltipOutsideLeft = tooltipX < 0;
if (tooltipOutsideLeft) {
tooltipX = 0;
}
- const tooltipY =
- pointY + (pointerDown ? -tooltipDimensions.height : pointerSize);
- tooltipLayout.value = {
- y: tooltipY,
- x: tooltipX,
+ return {
+ position: 'absolute',
+ top: pointPosition.value.topHalfOfViewport
+ ? -tooltipDimensions.value.height
+ : pointerSize,
+ left: tooltipX,
};
}
- }, [backdrop, element, pointerLayout, pointerSize, tooltip, tooltipLayout]);
-
- const setPositionIfVisible = useCallback(() => {
- if (contentVisible) {
- runOnUI(setTooltipPosition)();
- }
- }, [contentVisible, setTooltipPosition]);
-
- const { fontScale, width } = useWindowDimensions();
- const prevFontScale = useRef(fontScale);
- const prevWidth = useRef(width);
- useEffect(() => {
- if (prevFontScale.current !== fontScale || prevWidth.current !== width) {
- prevFontScale.current = fontScale;
- prevWidth.current = width;
- setPositionIfVisible();
- }
- }, [fontScale, setPositionIfVisible, width]);
-
- const tooltipPosition = useAnimatedStyle(
- () => ({
- position: 'absolute',
- opacity: tooltipLayout.value.x === undefined ? 0 : 1,
- top: tooltipLayout.value.y,
- left: tooltipLayout.value.x,
- }),
- []
- );
-
- const pointerPosition = useAnimatedStyle(
- () => ({
+ return {
position: 'absolute',
- opacity: pointerLayout.value.x === undefined ? 0 : 1,
- top: pointerLayout.value.y,
- left: pointerLayout.value.x,
- }),
- []
- );
-
- const pointerTransform = useAnimatedStyle(
- () => ({
- marginLeft: -pointerSize,
- transform: [
- {
- rotate: pointerLayout.value.isDown ? '0deg' : '180deg',
- },
- ],
- }),
- []
- );
-
- const onPressCallback = useCallback(() => {
- setContentVisible(false);
+ top: -10000,
+ };
}, []);
- const exitingWithCallback = useMemo(() => {
- const close = () => {
- onClose && onClose();
- setModalVisible(false);
- };
- const worklet = () => {
- 'worklet';
- onClose && runOnJS(close)();
+ const pointerAnimatedStyle = useAnimatedStyle(() => {
+ if (pointPosition.value) {
+ return {
+ position: 'absolute',
+ top: 0,
+ left: pointPosition.value.x,
+ marginLeft: -pointerSize,
+ transform: [
+ {
+ rotate: pointPosition.value.topHalfOfViewport ? '0deg' : '180deg',
+ },
+ ],
+ };
+ }
+ return {
+ position: 'absolute',
+ top: -10000,
};
-
- return exiting
- ? // @ts-ignore For some reason `bob build` throws an error here
- exiting.withCallback(worklet)
- : FadeOut.duration(1).withCallback(worklet);
- }, [exiting, onClose]);
+ }, []);
return (
-
- {props.children}
-
- {contentVisible ? (
-
-
- <>
+
+ {children}
+
+ {visible ? (
+ <>
+
+
+
-
-
- {props.content}
-
-
+
+ {content}
+
{withPointer ? (
-
-
-
-
-
-
+
+
) : null}
- >
-
-
+
+
+ >
) : null}
-
+
);
});
const styles = StyleSheet.create({
backdrop: {
- flex: 1,
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
},
defaultTooltip: {
backgroundColor: '#F3F2F7',
diff --git a/yarn.lock b/yarn.lock
index 6e34f33..02647cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2437,6 +2437,18 @@ __metadata:
languageName: node
linkType: hard
+"@gorhom/portal@npm:^1.0.14":
+ version: 1.0.14
+ resolution: "@gorhom/portal@npm:1.0.14"
+ dependencies:
+ nanoid: ^3.3.1
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ checksum: 227bb96a2db854ab29bb9da8d4f3823c7f7448358de459709dd1b78522110da564c9a8734c6bc7d7153ed7c99320e0fb5d60b420c2ebb75ecaf2f0d757f410f9
+ languageName: node
+ linkType: hard
+
"@graphql-typed-document-node/core@npm:^3.1.0":
version: 3.2.0
resolution: "@graphql-typed-document-node/core@npm:3.2.0"
@@ -12818,7 +12830,7 @@ __metadata:
languageName: node
linkType: hard
-"nanoid@npm:^3.3.6":
+"nanoid@npm:^3.3.1, nanoid@npm:^3.3.6":
version: 3.3.7
resolution: "nanoid@npm:3.3.7"
bin:
@@ -14698,6 +14710,7 @@ __metadata:
dependencies:
"@babel/core": ^7.20.0
"@expo/webpack-config": ^18.0.1
+ "@gorhom/portal": ^1.0.14
babel-loader: ^8.1.0
babel-plugin-module-resolver: ^5.0.0
expo: ~49.0.15
@@ -14716,6 +14729,7 @@ __metadata:
dependencies:
"@commitlint/config-conventional": ^17.0.2
"@evilmartians/lefthook": ^1.5.0
+ "@gorhom/portal": ^1.0.14
"@react-native/eslint-config": ^0.72.2
"@release-it/conventional-changelog": ^5.0.0
"@types/jest": ^28.1.2
@@ -14735,9 +14749,10 @@ __metadata:
release-it: ^15.0.0
typescript: ^5.0.2
peerDependencies:
+ "@gorhom/portal": ^1
react: "*"
react-native: "*"
- react-native-reanimated: ^2.0.0
+ react-native-reanimated: ^2
languageName: unknown
linkType: soft