Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

Notification.tsx 11KB

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