Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

Notification.tsx 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import { Theme } from '@mui/material';
  2. import React, { isValidElement, useCallback, useContext } from 'react';
  3. import { useTranslation } from 'react-i18next';
  4. import { keyframes } from 'tss-react';
  5. import { makeStyles } from 'tss-react/mui';
  6. import Icon from '../../../base/icons/components/Icon';
  7. import {
  8. IconCheck,
  9. IconCloseLarge,
  10. IconInfo,
  11. IconMessage,
  12. IconUser,
  13. IconUsers,
  14. IconWarningCircle
  15. } from '../../../base/icons/svg';
  16. import Message from '../../../base/react/components/web/Message';
  17. import { withPixelLineHeight } from '../../../base/styles/functions.web';
  18. import { NOTIFICATION_ICON, NOTIFICATION_TYPE } from '../../constants';
  19. import { INotificationProps } from '../../types';
  20. import { NotificationsTransitionContext } from '../NotificationsTransition';
  21. interface IProps extends INotificationProps {
  22. /**
  23. * Callback invoked when the user clicks to dismiss the notification.
  24. */
  25. onDismissed: Function;
  26. }
  27. /**
  28. * Secondary colors for notification icons.
  29. *
  30. * @type {{error, info, normal, success, warning}}
  31. */
  32. const useStyles = makeStyles()((theme: Theme) => {
  33. return {
  34. container: {
  35. backgroundColor: theme.palette.ui10,
  36. padding: '8px 16px 8px 20px',
  37. display: 'flex',
  38. position: 'relative' as const,
  39. borderRadius: `${theme.shape.borderRadius}px`,
  40. boxShadow: '0px 6px 20px rgba(0, 0, 0, 0.25)',
  41. marginBottom: theme.spacing(2),
  42. '&:last-of-type': {
  43. marginBottom: 0
  44. },
  45. animation: `${keyframes`
  46. 0% {
  47. opacity: 0;
  48. transform: translateX(-80%);
  49. }
  50. 100% {
  51. opacity: 1;
  52. transform: translateX(0);
  53. }
  54. `} 0.2s forwards ease`,
  55. '&.unmount': {
  56. animation: `${keyframes`
  57. 0% {
  58. opacity: 1;
  59. transform: translateX(0);
  60. }
  61. 100% {
  62. opacity: 0;
  63. transform: translateX(-80%);
  64. }
  65. `} 0.2s forwards ease`
  66. }
  67. },
  68. ribbon: {
  69. width: '4px',
  70. height: 'calc(100% - 16px)',
  71. position: 'absolute' as const,
  72. left: 0,
  73. top: '8px',
  74. borderRadius: '4px',
  75. '&.normal': {
  76. backgroundColor: theme.palette.action01
  77. },
  78. '&.error': {
  79. backgroundColor: theme.palette.iconError
  80. },
  81. '&.success': {
  82. backgroundColor: theme.palette.success01
  83. },
  84. '&.warning': {
  85. backgroundColor: theme.palette.warning01
  86. }
  87. },
  88. content: {
  89. display: 'flex',
  90. alignItems: 'flex-start',
  91. padding: '8px 0',
  92. flex: 1,
  93. maxWidth: '100%'
  94. },
  95. textContainer: {
  96. display: 'flex',
  97. flexDirection: 'column' as const,
  98. justifyContent: 'space-between',
  99. color: theme.palette.text04,
  100. flex: 1,
  101. margin: '0 8px',
  102. // maxWidth: 100% minus the icon on left (20px) minus the close icon on the right (20px) minus the margins
  103. maxWidth: 'calc(100% - 40px - 16px)',
  104. maxHeight: '150px'
  105. },
  106. title: {
  107. ...withPixelLineHeight(theme.typography.bodyShortBold)
  108. },
  109. description: {
  110. ...withPixelLineHeight(theme.typography.bodyShortRegular),
  111. overflow: 'auto',
  112. overflowWrap: 'break-word',
  113. '&:not(:empty)': {
  114. marginTop: theme.spacing(1)
  115. }
  116. },
  117. actionsContainer: {
  118. display: 'flex',
  119. width: '100%',
  120. '&:not(:empty)': {
  121. marginTop: theme.spacing(2)
  122. }
  123. },
  124. action: {
  125. border: 0,
  126. outline: 0,
  127. backgroundColor: 'transparent',
  128. color: theme.palette.action01,
  129. ...withPixelLineHeight(theme.typography.bodyShortBold),
  130. marginRight: theme.spacing(3),
  131. padding: 0,
  132. cursor: 'pointer',
  133. '&:last-of-type': {
  134. marginRight: 0
  135. },
  136. '&.destructive': {
  137. color: theme.palette.textError
  138. }
  139. },
  140. closeIcon: {
  141. cursor: 'pointer'
  142. }
  143. };
  144. });
  145. const Notification = ({
  146. appearance = NOTIFICATION_TYPE.NORMAL,
  147. customActionHandler,
  148. customActionNameKey,
  149. customActionType,
  150. description,
  151. descriptionArguments,
  152. descriptionKey,
  153. hideErrorSupportLink,
  154. icon,
  155. onDismissed,
  156. title,
  157. titleArguments,
  158. titleKey,
  159. uid
  160. }: IProps) => {
  161. const { classes, cx, theme } = useStyles();
  162. const { t } = useTranslation();
  163. const { unmounting } = useContext(NotificationsTransitionContext);
  164. const ICON_COLOR = {
  165. error: theme.palette.iconError,
  166. normal: theme.palette.action01,
  167. success: theme.palette.success01,
  168. warning: theme.palette.warning01
  169. };
  170. const onDismiss = useCallback(() => {
  171. onDismissed(uid);
  172. }, [ uid ]);
  173. // eslint-disable-next-line react/no-multi-comp
  174. const renderDescription = useCallback(() => {
  175. const descriptionArray = [];
  176. descriptionKey
  177. && descriptionArray.push(t(descriptionKey, descriptionArguments));
  178. description && descriptionArray.push(description);
  179. // Keeping in mind that:
  180. // - Notifications that use the `translateToHtml` function get an element-based description array with one entry
  181. // - Message notifications receive string-based description arrays that might need additional parsing
  182. // We look for ready-to-render elements, and if present, we roll with them
  183. // Otherwise, we use the Message component that accepts a string `text` prop
  184. const shouldRenderHtml = descriptionArray.length === 1 && isValidElement(descriptionArray[0]);
  185. // the id is used for testing the UI
  186. return (
  187. <p
  188. className = { classes.description }
  189. data-testid = { descriptionKey } >
  190. {shouldRenderHtml ? descriptionArray : <Message text = { descriptionArray.join(' ') } />}
  191. </p>
  192. );
  193. }, [ description, descriptionArguments, descriptionKey, classes ]);
  194. const _onOpenSupportLink = () => {
  195. window.open(interfaceConfig.SUPPORT_URL, '_blank', 'noopener');
  196. };
  197. const mapAppearanceToButtons = useCallback((): {
  198. content: string; onClick: () => void; testId?: string; type?: string; }[] => {
  199. switch (appearance) {
  200. case NOTIFICATION_TYPE.ERROR: {
  201. const buttons = [
  202. {
  203. content: t('dialog.dismiss'),
  204. onClick: onDismiss
  205. }
  206. ];
  207. if (!hideErrorSupportLink && interfaceConfig.SUPPORT_URL) {
  208. buttons.push({
  209. content: t('dialog.contactSupport'),
  210. onClick: _onOpenSupportLink
  211. });
  212. }
  213. return buttons;
  214. }
  215. case NOTIFICATION_TYPE.WARNING:
  216. return [
  217. {
  218. content: t('dialog.Ok'),
  219. onClick: onDismiss
  220. }
  221. ];
  222. default:
  223. if (customActionNameKey?.length && customActionHandler?.length) {
  224. return customActionNameKey.map((customAction: string, customActionIndex: number) => {
  225. return {
  226. content: t(customAction),
  227. onClick: () => {
  228. if (customActionHandler?.[customActionIndex]()) {
  229. onDismiss();
  230. }
  231. },
  232. type: customActionType?.[customActionIndex],
  233. testId: customAction
  234. };
  235. });
  236. }
  237. return [];
  238. }
  239. }, [ appearance, onDismiss, customActionHandler, customActionNameKey, hideErrorSupportLink ]);
  240. const getIcon = useCallback(() => {
  241. let iconToDisplay;
  242. switch (icon || appearance) {
  243. case NOTIFICATION_ICON.ERROR:
  244. case NOTIFICATION_ICON.WARNING:
  245. iconToDisplay = IconWarningCircle;
  246. break;
  247. case NOTIFICATION_ICON.SUCCESS:
  248. iconToDisplay = IconCheck;
  249. break;
  250. case NOTIFICATION_ICON.MESSAGE:
  251. iconToDisplay = IconMessage;
  252. break;
  253. case NOTIFICATION_ICON.PARTICIPANT:
  254. iconToDisplay = IconUser;
  255. break;
  256. case NOTIFICATION_ICON.PARTICIPANTS:
  257. iconToDisplay = IconUsers;
  258. break;
  259. default:
  260. iconToDisplay = IconInfo;
  261. break;
  262. }
  263. return iconToDisplay;
  264. }, [ icon, appearance ]);
  265. return (
  266. <div
  267. className = { cx(classes.container, unmounting.get(uid ?? '') && 'unmount') }
  268. data-testid = { titleKey || descriptionKey }
  269. id = { uid }>
  270. <div className = { cx(classes.ribbon, appearance) } />
  271. <div className = { classes.content }>
  272. <div className = { icon }>
  273. <Icon
  274. color = { ICON_COLOR[appearance as keyof typeof ICON_COLOR] }
  275. size = { 20 }
  276. src = { getIcon() } />
  277. </div>
  278. <div className = { classes.textContainer }>
  279. <span className = { classes.title }>{title || t(titleKey ?? '', titleArguments)}</span>
  280. {renderDescription()}
  281. <div className = { classes.actionsContainer }>
  282. {mapAppearanceToButtons().map(({ content, onClick, type, testId }) => (
  283. <button
  284. className = { cx(classes.action, type) }
  285. data-testid = { testId }
  286. key = { content }
  287. onClick = { onClick }>
  288. {content}
  289. </button>
  290. ))}
  291. </div>
  292. </div>
  293. <Icon
  294. className = { classes.closeIcon }
  295. color = { theme.palette.icon04 }
  296. id = 'close-notification'
  297. onClick = { onDismiss }
  298. size = { 20 }
  299. src = { IconCloseLarge }
  300. testId = { `${titleKey || descriptionKey}-dismiss` } />
  301. </div>
  302. </div>
  303. );
  304. };
  305. export default Notification;