index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import {
  2. useEffect,
  3. useState,
  4. type FC,
  5. useRef
  6. } from "react";
  7. import {
  8. TouchableWithoutFeedback,
  9. type ViewStyle,
  10. Animated,
  11. Easing,
  12. View
  13. } from "react-native";
  14. import type ITooltipProps from "./type";
  15. import stylesheet, {
  16. getTooltipType,
  17. useStyles
  18. } from "./stylesheet";
  19. import {
  20. NCoreUIKitTheme
  21. } from "../../core/hooks";
  22. import {
  23. X as XIcon
  24. } from "lucide-react-native";
  25. import Button from "../button";
  26. import Text from "../text";
  27. const Tooltip: FC<ITooltipProps> = ({
  28. isCloseOnPressActionButton = true,
  29. location = {
  30. horizontal: "center",
  31. vertical: "top"
  32. },
  33. isCloseOnPress = false,
  34. icon: CustomIcon,
  35. type = "neutral",
  36. customTheme,
  37. isVisible,
  38. onClosed,
  39. subTitle,
  40. children,
  41. onClose,
  42. onOpen,
  43. content,
  44. action,
  45. style,
  46. title
  47. }) => {
  48. const {
  49. radiuses,
  50. colors,
  51. spaces
  52. } = NCoreUIKitTheme.useContext(customTheme);
  53. const [
  54. wrapperSize,
  55. setWrapperSize
  56. ] = useState({
  57. height: 0,
  58. width: 0
  59. });
  60. const [
  61. tooltipSize,
  62. setTooltipSize
  63. ] = useState({
  64. height: 0,
  65. width: 0
  66. });
  67. const [
  68. isInternalVisible,
  69. setIsInternalVisible
  70. ] = useState(false);
  71. const opacityAnim = useRef(new Animated.Value(0)).current;
  72. const currentType = getTooltipType({
  73. type
  74. });
  75. const {
  76. containerObject: containerObjectDynamicStyle,
  77. contentContainer: contentContainerDynamicStyle,
  78. iconContainer: iconContainerDynamicStyle,
  79. container: containerDynamicStyle,
  80. subTitle: subTitleDynamicStyle,
  81. action: actionDynamicStyle,
  82. title: titleDynamicStyle,
  83. caret: caretDynamicStyle
  84. } = useStyles({
  85. currentType,
  86. radiuses,
  87. colors,
  88. spaces,
  89. type
  90. });
  91. const isShow = isVisible !== undefined ? isVisible : isInternalVisible;
  92. useEffect(() => {
  93. if(isShow) {
  94. if(onOpen) onOpen();
  95. Animated.timing(opacityAnim, {
  96. useNativeDriver: true,
  97. easing: Easing.linear,
  98. duration: 200,
  99. toValue: 1
  100. }).start();
  101. } else {
  102. Animated.timing(opacityAnim, {
  103. useNativeDriver: true,
  104. easing: Easing.linear,
  105. duration: 200,
  106. toValue: 0
  107. }).start(({
  108. finished
  109. }) => {
  110. if(finished && onClosed) {
  111. onClosed();
  112. }
  113. });
  114. }
  115. }, [
  116. isShow
  117. ]);
  118. const handleClose = () => {
  119. if(isVisible === undefined) {
  120. setIsInternalVisible(false);
  121. }
  122. if(onClose) onClose();
  123. };
  124. const handleToggle = () => {
  125. if(isVisible === undefined) {
  126. setIsInternalVisible(!isInternalVisible);
  127. }
  128. if(!isShow && onOpen) {
  129. onOpen();
  130. } else if (isShow && onClose) {
  131. onClose();
  132. }
  133. };
  134. const renderIcon = () => {
  135. if(!CustomIcon) {
  136. return null;
  137. }
  138. return <View
  139. style={[
  140. stylesheet.iconContainer,
  141. iconContainerDynamicStyle
  142. ]}
  143. >
  144. <CustomIcon
  145. color={currentType.iconColor}
  146. size={16}
  147. />
  148. </View>;
  149. };
  150. const renderContent = () => {
  151. if(content) {
  152. if(typeof content === "function") {
  153. return content();
  154. }
  155. return content;
  156. }
  157. return <View
  158. style={[
  159. stylesheet.contentContainer,
  160. contentContainerDynamicStyle
  161. ]}
  162. >
  163. {
  164. title ?
  165. <Text
  166. style={{
  167. ...stylesheet.title,
  168. ...titleDynamicStyle
  169. }}
  170. color={currentType.titleColor}
  171. variant="labelMediumSize"
  172. numberOfLines={3}
  173. >
  174. {title}
  175. </Text>
  176. :
  177. null
  178. }
  179. {
  180. subTitle ?
  181. <Text
  182. style={{
  183. ...stylesheet.subTitle,
  184. ...subTitleDynamicStyle
  185. }}
  186. color={currentType.subTitleColor}
  187. variant="labelSmallSize"
  188. numberOfLines={2}
  189. >
  190. {subTitle}
  191. </Text>
  192. :
  193. null
  194. }
  195. </View>;
  196. };
  197. const renderAction = () => {
  198. if(!action) {
  199. return null;
  200. }
  201. return <Button
  202. onPress={() => {
  203. if(isCloseOnPressActionButton) handleClose();
  204. if(action.onPress) {
  205. action.onPress({
  206. closeAnimation: handleClose
  207. });
  208. }
  209. }}
  210. icon={() => {
  211. if(action.title) {
  212. return null;
  213. }
  214. return <XIcon
  215. color={colors.content.text[currentType.actionColor]}
  216. size={16}
  217. />;
  218. }}
  219. style={{
  220. ...action.style,
  221. ...actionDynamicStyle
  222. }}
  223. title={action.title ? action.title : undefined}
  224. spreadBehaviour="free"
  225. isCustomPadding={true}
  226. variant="ghost"
  227. size="small"
  228. type={type}
  229. />;
  230. };
  231. const getTooltipStyle = (): ViewStyle => {
  232. const _style: ViewStyle = {};
  233. if(tooltipSize.width > 0 && wrapperSize.width > 0) {
  234. if(location.vertical === "top") {
  235. _style.bottom = wrapperSize.height + 8;
  236. } else if (location.vertical === "bottom") {
  237. _style.top = wrapperSize.height + 8;
  238. } else {
  239. _style.top = (wrapperSize.height - tooltipSize.height) / 2;
  240. }
  241. if(location.horizontal === "left") {
  242. _style.right = wrapperSize.width + 8;
  243. } else if (location.horizontal === "right") {
  244. _style.left = wrapperSize.width + 8;
  245. } else {
  246. _style.left = (wrapperSize.width - tooltipSize.width) / 2;
  247. }
  248. } else {
  249. _style.opacity = 0;
  250. }
  251. return _style;
  252. };
  253. const getCaretStyle = (): ViewStyle => {
  254. const _style: ViewStyle = {};
  255. if(location.vertical === "top") {
  256. _style.bottom = -4;
  257. } else if (location.vertical === "bottom") {
  258. _style.top = -4;
  259. } else {
  260. _style.top = (tooltipSize.height - 12) / 2;
  261. }
  262. if(location.horizontal === "left") {
  263. _style.right = -4;
  264. } else if (location.horizontal === "right") {
  265. _style.left = -4;
  266. } else {
  267. _style.left = (tooltipSize.width - 12) / 2;
  268. }
  269. if (location.vertical === "top" && location.horizontal === "right") {
  270. _style.left = 12;
  271. } else if (location.vertical === "top" && location.horizontal === "left") {
  272. _style.right = 12;
  273. } else if (location.vertical === "bottom" && location.horizontal === "right") {
  274. _style.left = 12;
  275. } else if (location.vertical === "bottom" && location.horizontal === "left") {
  276. _style.right = 12;
  277. }
  278. return _style;
  279. };
  280. return <View
  281. onLayout={(e) => {
  282. const {
  283. height,
  284. width
  285. } = e.nativeEvent.layout;
  286. setWrapperSize(prev => {
  287. if (prev.height === height && prev.width === width) {
  288. return prev;
  289. }
  290. return {
  291. height,
  292. width
  293. };
  294. });
  295. }}
  296. style={[
  297. stylesheet.wrapper,
  298. style
  299. ]}
  300. >
  301. <TouchableWithoutFeedback
  302. onPress={() => {
  303. if(!isCloseOnPress) {
  304. handleToggle();
  305. } else if(isShow) {
  306. handleClose();
  307. } else {
  308. handleToggle();
  309. }
  310. }}
  311. >
  312. <View>
  313. {children}
  314. </View>
  315. </TouchableWithoutFeedback>
  316. {
  317. isShow || opacityAnim !== undefined ?
  318. <Animated.View
  319. onLayout={(e) => {
  320. const {
  321. height,
  322. width
  323. } = e.nativeEvent.layout;
  324. setTooltipSize(prev => {
  325. if (prev.height === height && prev.width === width) {
  326. return prev;
  327. }
  328. return {
  329. height,
  330. width
  331. };
  332. });
  333. }}
  334. style={[
  335. stylesheet.container,
  336. containerDynamicStyle,
  337. getTooltipStyle(),
  338. {
  339. opacity: opacityAnim
  340. }
  341. ]}
  342. pointerEvents={isShow ? "auto" : "none"}
  343. >
  344. <View
  345. style={[
  346. stylesheet.caret,
  347. caretDynamicStyle,
  348. getCaretStyle()
  349. ]}
  350. />
  351. <View
  352. style={[
  353. stylesheet.containerObject,
  354. containerObjectDynamicStyle
  355. ]}
  356. >
  357. {renderIcon()}
  358. {renderContent()}
  359. {renderAction()}
  360. </View>
  361. </Animated.View>
  362. :
  363. null
  364. }
  365. </View>;
  366. };
  367. export default Tooltip;