index.tsx 7.5 KB

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