You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Dialog.tsx 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import React, { useCallback, useContext, useEffect } from 'react';
  2. import FocusLock from 'react-focus-lock';
  3. import { useTranslation } from 'react-i18next';
  4. import { useDispatch } from 'react-redux';
  5. import { keyframes } from 'tss-react';
  6. import { makeStyles } from 'tss-react/mui';
  7. import { hideDialog } from '../../../dialog/actions';
  8. import { IconCloseLarge } from '../../../icons/svg';
  9. import { withPixelLineHeight } from '../../../styles/functions.web';
  10. import Button from './Button';
  11. import ClickableIcon from './ClickableIcon';
  12. import { DialogTransitionContext } from './DialogTransition';
  13. const useStyles = makeStyles()(theme => {
  14. return {
  15. container: {
  16. width: '100%',
  17. height: '100%',
  18. position: 'fixed',
  19. color: theme.palette.text01,
  20. ...withPixelLineHeight(theme.typography.bodyLongRegular),
  21. top: 0,
  22. left: 0,
  23. display: 'flex',
  24. justifyContent: 'center',
  25. alignItems: 'flex-start',
  26. animation: `${keyframes`
  27. 0% {
  28. opacity: 0.4;
  29. }
  30. 100% {
  31. opacity: 1;
  32. }
  33. `} 0.2s forwards ease-out`,
  34. '&.unmount': {
  35. animation: `${keyframes`
  36. 0% {
  37. opacity: 1;
  38. }
  39. 100% {
  40. opacity: 0.5;
  41. }
  42. `} 0.15s forwards ease-in`
  43. }
  44. },
  45. backdrop: {
  46. position: 'absolute',
  47. width: '100%',
  48. height: '100%',
  49. top: 0,
  50. left: 0,
  51. backgroundColor: theme.palette.ui02,
  52. opacity: 0.75
  53. },
  54. modal: {
  55. backgroundColor: theme.palette.ui01,
  56. border: `1px solid ${theme.palette.ui03}`,
  57. boxShadow: '0px 4px 25px 4px rgba(20, 20, 20, 0.6)',
  58. borderRadius: `${theme.shape.borderRadius}px`,
  59. display: 'flex',
  60. flexDirection: 'column',
  61. height: 'auto',
  62. minHeight: '200px',
  63. maxHeight: '760px',
  64. marginTop: '64px',
  65. animation: `${keyframes`
  66. 0% {
  67. margin-top: 85px
  68. }
  69. 100% {
  70. margin-top: 64px
  71. }
  72. `} 0.2s forwards ease-out`,
  73. '&.medium': {
  74. width: '400px'
  75. },
  76. '&.large': {
  77. width: '664px'
  78. },
  79. '&.unmount': {
  80. animation: `${keyframes`
  81. 0% {
  82. margin-top: 64px
  83. }
  84. 100% {
  85. margin-top: 40px
  86. }
  87. `} 0.15s forwards ease-in`
  88. },
  89. '@media (max-width: 448px)': {
  90. width: '100% !important',
  91. maxHeight: 'initial',
  92. height: '100%',
  93. margin: 0,
  94. position: 'absolute',
  95. top: 0,
  96. left: 0,
  97. bottom: 0,
  98. animation: `${keyframes`
  99. 0% {
  100. margin-top: 15px
  101. }
  102. 100% {
  103. margin-top: 0
  104. }
  105. `} 0.2s forwards ease-out`,
  106. '&.unmount': {
  107. animation: `${keyframes`
  108. 0% {
  109. margin-top: 0
  110. }
  111. 100% {
  112. margin-top: 15px
  113. }
  114. `} 0.15s forwards ease-in`
  115. }
  116. }
  117. },
  118. header: {
  119. width: '100%',
  120. padding: '24px',
  121. boxSizing: 'border-box',
  122. display: 'flex',
  123. alignItems: 'flex-start',
  124. justifyContent: 'space-between'
  125. },
  126. closeIcon: {
  127. '&:focus': {
  128. boxShadow: 'none'
  129. }
  130. },
  131. title: {
  132. color: theme.palette.text01,
  133. ...withPixelLineHeight(theme.typography.heading5),
  134. margin: 0,
  135. padding: 0
  136. },
  137. content: {
  138. height: 'auto',
  139. overflowY: 'auto',
  140. width: '100%',
  141. boxSizing: 'border-box',
  142. padding: '0 24px',
  143. overflowX: 'hidden',
  144. minHeight: '40px',
  145. '@media (max-width: 448px)': {
  146. height: '100%'
  147. }
  148. },
  149. footer: {
  150. width: '100%',
  151. boxSizing: 'border-box',
  152. display: 'flex',
  153. alignItems: 'center',
  154. justifyContent: 'flex-end',
  155. padding: '24px',
  156. '& button:last-child': {
  157. marginLeft: '16px'
  158. }
  159. },
  160. focusLock: {
  161. zIndex: 1
  162. }
  163. };
  164. });
  165. interface IDialogProps {
  166. back?: {
  167. hidden?: boolean;
  168. onClick?: () => void;
  169. translationKey?: string;
  170. };
  171. cancel?: {
  172. hidden?: boolean;
  173. translationKey?: string;
  174. };
  175. children?: React.ReactNode;
  176. className?: string;
  177. description?: string;
  178. disableAutoHideOnSubmit?: boolean;
  179. disableBackdropClose?: boolean;
  180. disableEnter?: boolean;
  181. hideCloseButton?: boolean;
  182. ok?: {
  183. disabled?: boolean;
  184. hidden?: boolean;
  185. translationKey?: string;
  186. };
  187. onCancel?: () => void;
  188. onSubmit?: () => void;
  189. size?: 'large' | 'medium';
  190. title?: string;
  191. titleKey?: string;
  192. }
  193. const Dialog = ({
  194. back = { hidden: true },
  195. cancel = { translationKey: 'dialog.Cancel' },
  196. children,
  197. className,
  198. description,
  199. disableAutoHideOnSubmit = false,
  200. disableBackdropClose,
  201. hideCloseButton,
  202. disableEnter,
  203. ok = { translationKey: 'dialog.Ok' },
  204. onCancel,
  205. onSubmit,
  206. size = 'medium',
  207. title,
  208. titleKey
  209. }: IDialogProps) => {
  210. const { classes, cx } = useStyles();
  211. const { t } = useTranslation();
  212. const { isUnmounting } = useContext(DialogTransitionContext);
  213. const dispatch = useDispatch();
  214. const onClose = useCallback(() => {
  215. dispatch(hideDialog());
  216. onCancel?.();
  217. }, [ onCancel ]);
  218. const submit = useCallback(() => {
  219. !disableAutoHideOnSubmit && dispatch(hideDialog());
  220. onSubmit?.();
  221. }, [ onSubmit ]);
  222. const handleKeyDown = useCallback((e: KeyboardEvent) => {
  223. if (e.key === 'Escape') {
  224. onClose();
  225. }
  226. if (e.key === 'Enter' && !disableEnter) {
  227. submit();
  228. }
  229. }, []);
  230. const onBackdropClick = useCallback(() => {
  231. !disableBackdropClose && onClose();
  232. }, [ disableBackdropClose, onClose ]);
  233. useEffect(() => {
  234. window.addEventListener('keydown', handleKeyDown);
  235. return () => window.removeEventListener('keydown', handleKeyDown);
  236. }, []);
  237. return (
  238. <div className = { cx(classes.container, isUnmounting && 'unmount') }>
  239. <div
  240. className = { classes.backdrop }
  241. onClick = { onBackdropClick } />
  242. <FocusLock className = { classes.focusLock }>
  243. <div
  244. aria-describedby = { description }
  245. aria-labelledby = { title ?? t(titleKey ?? '') }
  246. aria-modal = { true }
  247. className = { cx(classes.modal, isUnmounting && 'unmount', size, className) }
  248. role = 'dialog'>
  249. <div className = { classes.header }>
  250. <p
  251. className = { classes.title }
  252. id = 'dialog-title'>
  253. {title ?? t(titleKey ?? '')}
  254. </p>
  255. {!hideCloseButton && (
  256. <ClickableIcon
  257. accessibilityLabel = { t('dialog.close') }
  258. className = { classes.closeIcon }
  259. icon = { IconCloseLarge }
  260. id = 'modal-header-close-button'
  261. onClick = { onClose } />
  262. )}
  263. </div>
  264. <div
  265. className = { classes.content }
  266. data-autofocus-inside = 'true'>
  267. {children}
  268. </div>
  269. <div
  270. className = { classes.footer }
  271. data-autofocus-inside = 'true'>
  272. {!back.hidden && <Button
  273. accessibilityLabel = { t(back.translationKey ?? '') }
  274. labelKey = { back.translationKey }
  275. // eslint-disable-next-line react/jsx-handler-names
  276. onClick = { back.onClick }
  277. type = 'secondary' />}
  278. {!cancel.hidden && <Button
  279. accessibilityLabel = { t(cancel.translationKey ?? '') }
  280. labelKey = { cancel.translationKey }
  281. onClick = { onClose }
  282. type = 'tertiary' />}
  283. {!ok.hidden && <Button
  284. accessibilityLabel = { t(ok.translationKey ?? '') }
  285. disabled = { ok.disabled }
  286. id = 'modal-dialog-ok-button'
  287. labelKey = { ok.translationKey }
  288. onClick = { submit } />}
  289. </div>
  290. </div>
  291. </FocusLock>
  292. </div>
  293. );
  294. };
  295. export default Dialog;