Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

Notification.tsx 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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. userSelect: 'all',
  114. '&:not(:empty)': {
  115. marginTop: theme.spacing(1)
  116. }
  117. },
  118. actionsContainer: {
  119. display: 'flex',
  120. width: '100%',
  121. '&:not(:empty)': {
  122. marginTop: theme.spacing(2)
  123. }
  124. },
  125. action: {
  126. border: 0,
  127. outline: 0,
  128. backgroundColor: 'transparent',
  129. color: theme.palette.action01,
  130. ...withPixelLineHeight(theme.typography.bodyShortBold),
  131. marginRight: theme.spacing(3),
  132. padding: 0,
  133. cursor: 'pointer',
  134. '&:last-of-type': {
  135. marginRight: 0
  136. },
  137. '&.destructive': {
  138. color: theme.palette.textError
  139. }
  140. },
  141. closeIcon: {
  142. cursor: 'pointer'
  143. }
  144. };
  145. });
  146. const Notification = ({
  147. appearance = NOTIFICATION_TYPE.NORMAL,
  148. customActionHandler,
  149. customActionNameKey,
  150. customActionType,
  151. description,
  152. descriptionArguments,
  153. descriptionKey,
  154. disableClosing,
  155. hideErrorSupportLink,
  156. icon,
  157. onDismissed,
  158. title,
  159. titleArguments,
  160. titleKey,
  161. uid
  162. }: IProps) => {
  163. const { classes, cx, theme } = useStyles();
  164. const { t } = useTranslation();
  165. const { unmounting } = useContext(NotificationsTransitionContext);
  166. const ICON_COLOR = {
  167. error: theme.palette.iconError,
  168. normal: theme.palette.action01,
  169. success: theme.palette.success01,
  170. warning: theme.palette.warning01
  171. };
  172. const onDismiss = useCallback(() => {
  173. onDismissed(uid);
  174. }, [ uid ]);
  175. // eslint-disable-next-line react/no-multi-comp
  176. const renderDescription = useCallback(() => {
  177. const descriptionArray = [];
  178. descriptionKey
  179. && descriptionArray.push(t(descriptionKey, descriptionArguments));
  180. description && typeof description === 'string' && descriptionArray.push(description);
  181. // Keeping in mind that:
  182. // - Notifications that use the `translateToHtml` function get an element-based description array with one entry
  183. // - Message notifications receive string-based description arrays that might need additional parsing
  184. // We look for ready-to-render elements, and if present, we roll with them
  185. // Otherwise, we use the Message component that accepts a string `text` prop
  186. const shouldRenderHtml = descriptionArray.length === 1 && isValidElement(descriptionArray[0]);
  187. // the id is used for testing the UI
  188. return (
  189. <div
  190. className = { classes.description }
  191. data-testid = { descriptionKey } >
  192. {shouldRenderHtml ? descriptionArray : <Message text = { descriptionArray.join(' ') } />}
  193. {typeof description === 'object' && description}
  194. </div>
  195. );
  196. }, [ description, descriptionArguments, descriptionKey, classes ]);
  197. const _onOpenSupportLink = () => {
  198. window.open(interfaceConfig.SUPPORT_URL, '_blank', 'noopener');
  199. };
  200. const mapAppearanceToButtons = useCallback((): {
  201. content: string; onClick: () => void; testId?: string; type?: string; }[] => {
  202. switch (appearance) {
  203. case NOTIFICATION_TYPE.ERROR: {
  204. const buttons = [
  205. {
  206. content: t('dialog.dismiss'),
  207. onClick: onDismiss
  208. }
  209. ];
  210. if (!hideErrorSupportLink && interfaceConfig.SUPPORT_URL) {
  211. buttons.push({
  212. content: t('dialog.contactSupport'),
  213. onClick: _onOpenSupportLink
  214. });
  215. }
  216. return buttons;
  217. }
  218. case NOTIFICATION_TYPE.WARNING:
  219. return [
  220. {
  221. content: t('dialog.Ok'),
  222. onClick: onDismiss
  223. }
  224. ];
  225. default:
  226. if (customActionNameKey?.length && customActionHandler?.length) {
  227. return customActionNameKey.map((customAction: string, customActionIndex: number) => {
  228. return {
  229. content: t(customAction),
  230. onClick: () => {
  231. if (customActionHandler?.[customActionIndex]()) {
  232. onDismiss();
  233. }
  234. },
  235. type: customActionType?.[customActionIndex],
  236. testId: customAction
  237. };
  238. });
  239. }
  240. return [];
  241. }
  242. }, [ appearance, onDismiss, customActionHandler, customActionNameKey, hideErrorSupportLink ]);
  243. const getIcon = useCallback(() => {
  244. let iconToDisplay;
  245. switch (icon || appearance) {
  246. case NOTIFICATION_ICON.ERROR:
  247. case NOTIFICATION_ICON.WARNING:
  248. iconToDisplay = IconWarningCircle;
  249. break;
  250. case NOTIFICATION_ICON.SUCCESS:
  251. iconToDisplay = IconCheck;
  252. break;
  253. case NOTIFICATION_ICON.MESSAGE:
  254. iconToDisplay = IconMessage;
  255. break;
  256. case NOTIFICATION_ICON.PARTICIPANT:
  257. iconToDisplay = IconUser;
  258. break;
  259. case NOTIFICATION_ICON.PARTICIPANTS:
  260. iconToDisplay = IconUsers;
  261. break;
  262. default:
  263. iconToDisplay = IconInfo;
  264. break;
  265. }
  266. return iconToDisplay;
  267. }, [ icon, appearance ]);
  268. return (
  269. <div
  270. aria-atomic = 'false'
  271. aria-live = 'polite'
  272. className = { cx(classes.container, unmounting.get(uid ?? '') && 'unmount') }
  273. data-testid = { titleKey || descriptionKey }
  274. id = { uid }>
  275. <div className = { cx(classes.ribbon, appearance) } />
  276. <div className = { classes.content }>
  277. <div className = { icon }>
  278. <Icon
  279. color = { ICON_COLOR[appearance as keyof typeof ICON_COLOR] }
  280. size = { 20 }
  281. src = { getIcon() } />
  282. </div>
  283. <div className = { classes.textContainer }>
  284. <span className = { classes.title }>{title || t(titleKey ?? '', titleArguments)}</span>
  285. {renderDescription()}
  286. <div className = { classes.actionsContainer }>
  287. {mapAppearanceToButtons().map(({ content, onClick, type, testId }) => (
  288. <button
  289. className = { cx(classes.action, type) }
  290. data-testid = { testId }
  291. key = { content }
  292. onClick = { onClick }>
  293. {content}
  294. </button>
  295. ))}
  296. </div>
  297. </div>
  298. { !disableClosing && (
  299. <Icon
  300. className = { classes.closeIcon }
  301. color = { theme.palette.icon04 }
  302. id = 'close-notification'
  303. onClick = { onDismiss }
  304. size = { 20 }
  305. src = { IconCloseLarge }
  306. testId = { `${titleKey || descriptionKey}-dismiss` } />
  307. )}
  308. </div>
  309. </div>
  310. );
  311. };
  312. export default Notification;