index.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import {
  2. useEffect,
  3. type FC,
  4. useRef
  5. } from "react";
  6. import {
  7. TouchableWithoutFeedback,
  8. Animated,
  9. Easing,
  10. View
  11. } from "react-native";
  12. import type IToastProps from "./type";
  13. import stylesheet, {
  14. getToastType,
  15. useStyles
  16. } from "./stylesheet";
  17. import {
  18. NCoreUIKitTheme
  19. } from "../../core/hooks";
  20. import {
  21. useSafeAreaInsets
  22. } from "react-native-safe-area-context";
  23. import {
  24. X as XIcon
  25. } from "lucide-react-native";
  26. import {
  27. Portal
  28. } from "../../helpers/portalize";
  29. import Button from "../button";
  30. import Text from "../text";
  31. const Toast: FC<IToastProps> = ({
  32. isCloseOnPressActionButton = true,
  33. closeAnimationDelay = 350,
  34. openAnimationDelay = 200,
  35. contentContainerStyle,
  36. autoCloseDelay = 3000,
  37. isCloseOnPress = true,
  38. icon: CustomIcon,
  39. type = "neutral",
  40. isShowAction,
  41. customTheme,
  42. onClosed,
  43. subTitle,
  44. children,
  45. action,
  46. style,
  47. title,
  48. id
  49. }) => {
  50. const {
  51. radiuses,
  52. colors,
  53. spaces
  54. } = NCoreUIKitTheme.useContext(customTheme);
  55. const {
  56. bottom
  57. } = useSafeAreaInsets();
  58. const TRANSFORM_DISTANCE = 75;
  59. const transformAnim = useRef(new Animated.Value(TRANSFORM_DISTANCE)).current;
  60. const opacityAnim = useRef(new Animated.Value(0)).current;
  61. const currentType = getToastType({
  62. type
  63. });
  64. const {
  65. contentContainer: contentContainerDynamicStyle,
  66. containerObject: containerObjectDynamicStyle,
  67. iconContainer: iconContainerDynamicStyle,
  68. container: containerDynamicStyle,
  69. subTitle: subTitleDynamicStyle,
  70. action: actionDynamicStyle,
  71. title: titleDynamicStyle
  72. } = useStyles({
  73. safeAreaBottom: bottom,
  74. currentType,
  75. radiuses,
  76. subTitle,
  77. spaces,
  78. colors,
  79. type
  80. });
  81. useEffect(() => {
  82. Animated.parallel([
  83. Animated.timing(opacityAnim, {
  84. duration: openAnimationDelay,
  85. useNativeDriver: true,
  86. easing: Easing.linear,
  87. toValue: 1
  88. }),
  89. Animated.timing(transformAnim, {
  90. duration: openAnimationDelay,
  91. useNativeDriver: true,
  92. easing: Easing.linear,
  93. toValue: 0
  94. })
  95. ]).start();
  96. setTimeout(() => {
  97. closeAnimation();
  98. }, autoCloseDelay);
  99. }, []);
  100. const closeAnimation = (_onClosed?: (props: {
  101. id: string;
  102. }) => void) => {
  103. Animated.parallel([
  104. Animated.timing(transformAnim, {
  105. duration: closeAnimationDelay,
  106. toValue: TRANSFORM_DISTANCE,
  107. useNativeDriver: true,
  108. easing: Easing.linear
  109. }),
  110. Animated.timing(opacityAnim, {
  111. duration: closeAnimationDelay,
  112. useNativeDriver: true,
  113. easing: Easing.linear,
  114. toValue: 0
  115. })
  116. ]).start(({
  117. finished
  118. }) => {
  119. if(finished) {
  120. if(onClosed) onClosed({
  121. id
  122. });
  123. if(_onClosed) _onClosed({
  124. id
  125. });
  126. }
  127. });
  128. };
  129. const renderIcon = () => {
  130. if(!CustomIcon) {
  131. return null;
  132. }
  133. return <View
  134. style={[
  135. stylesheet.iconContainer,
  136. iconContainerDynamicStyle
  137. ]}
  138. >
  139. <CustomIcon
  140. color={currentType.iconColor}
  141. size={18}
  142. />
  143. </View>;
  144. };
  145. const renderContent = () => {
  146. return <View
  147. style={[
  148. contentContainerStyle,
  149. stylesheet.contentContainer,
  150. contentContainerDynamicStyle
  151. ]}
  152. >
  153. <Text
  154. color={currentType.titleColor}
  155. numberOfLines={3}
  156. style={{
  157. ...stylesheet.title,
  158. ...titleDynamicStyle
  159. }}
  160. >
  161. {title}
  162. </Text>
  163. {
  164. subTitle ?
  165. <Text
  166. color={currentType.subTitleColor}
  167. variant="labelSmallSize"
  168. numberOfLines={2}
  169. style={{
  170. ...stylesheet.subTitle,
  171. ...subTitleDynamicStyle
  172. }}
  173. >
  174. {subTitle}
  175. </Text>
  176. :
  177. null
  178. }
  179. </View>;
  180. };
  181. const renderAction = () => {
  182. if(!isShowAction) {
  183. return null;
  184. }
  185. return <Button
  186. title={action && action.title ? action.title : undefined}
  187. isCustomPadding={true}
  188. spreadBehaviour="free"
  189. onPress={() => {
  190. if(isCloseOnPressActionButton) closeAnimation();
  191. if(action && action.onPress) action.onPress({
  192. closeAnimation: ({
  193. onClosed: _onClosed
  194. }) => closeAnimation(_onClosed)
  195. });
  196. }}
  197. icon={() => {
  198. if(action?.title) {
  199. return null;
  200. }
  201. return <XIcon
  202. color={colors.content.text[currentType.actionColor]}
  203. size={18}
  204. />;
  205. }}
  206. style={{
  207. ...action?.style,
  208. ...actionDynamicStyle
  209. }}
  210. variant="ghost"
  211. size="small"
  212. type={type}
  213. />;
  214. };
  215. const renderContainer = () => {
  216. return <View
  217. style={[
  218. stylesheet.containerObject,
  219. containerObjectDynamicStyle
  220. ]}
  221. >
  222. {renderIcon()}
  223. {renderContent()}
  224. {renderAction()}
  225. </View>;
  226. };
  227. return <Portal
  228. name="toast-system"
  229. >
  230. <Animated.View
  231. style={[
  232. style,
  233. stylesheet.container,
  234. containerDynamicStyle,
  235. {
  236. opacity: opacityAnim,
  237. transform: [
  238. {
  239. translateY: transformAnim
  240. }
  241. ]
  242. }
  243. ]}
  244. >
  245. <TouchableWithoutFeedback
  246. onPress={() => {
  247. if(isCloseOnPress) {
  248. closeAnimation();
  249. }
  250. }}
  251. >
  252. {children ? children : renderContainer()}
  253. </TouchableWithoutFeedback>
  254. </Animated.View>
  255. </Portal>;
  256. };
  257. export default Toast;