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

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