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 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import { Theme } from '@mui/material';
  2. import React, { ReactElement, useCallback, useContext, useEffect } from 'react';
  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 { IconClose } 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: Theme) => {
  14. return {
  15. container: {
  16. width: '100%',
  17. height: '100%',
  18. position: 'fixed',
  19. top: 0,
  20. left: 0,
  21. display: 'flex',
  22. justifyContent: 'center',
  23. alignItems: 'flex-start',
  24. animation: `${keyframes`
  25. 0% {
  26. opacity: 0.4;
  27. }
  28. 100% {
  29. opacity: 1;
  30. }
  31. `} 0.2s forwards ease-out`,
  32. '&.unmount': {
  33. animation: `${keyframes`
  34. 0% {
  35. opacity: 1;
  36. }
  37. 100% {
  38. opacity: 0.5;
  39. }
  40. `} 0.15s forwards ease-in`
  41. }
  42. },
  43. backdrop: {
  44. position: 'absolute',
  45. width: '100%',
  46. height: '100%',
  47. top: 0,
  48. left: 0,
  49. backgroundColor: theme.palette.ui02,
  50. opacity: 0.75
  51. },
  52. modal: {
  53. zIndex: 1,
  54. backgroundColor: theme.palette.ui01,
  55. border: `1px solid ${theme.palette.ui03}`,
  56. boxShadow: '0px 4px 25px 4px rgba(20, 20, 20, 0.6)',
  57. borderRadius: `${theme.shape.borderRadius}px`,
  58. display: 'flex',
  59. flexDirection: 'column',
  60. height: 'auto',
  61. minHeight: '200px',
  62. maxHeight: '560px',
  63. marginTop: '64px',
  64. animation: `${keyframes`
  65. 0% {
  66. margin-top: 85px
  67. }
  68. 100% {
  69. margin-top: 64px
  70. }
  71. `} 0.2s forwards ease-out`,
  72. '&.medium': {
  73. width: '400px'
  74. },
  75. '&.large': {
  76. width: '664px'
  77. },
  78. '&.unmount': {
  79. animation: `${keyframes`
  80. 0% {
  81. margin-top: 64px
  82. }
  83. 100% {
  84. margin-top: 40px
  85. }
  86. `} 0.15s forwards ease-in`
  87. },
  88. '@media (max-width: 448px)': {
  89. width: '100% !important',
  90. maxHeight: 'initial',
  91. height: '100%',
  92. margin: 0,
  93. position: 'absolute',
  94. top: 0,
  95. left: 0,
  96. bottom: 0,
  97. animation: `${keyframes`
  98. 0% {
  99. margin-top: 15px
  100. }
  101. 100% {
  102. margin-top: 0
  103. }
  104. `} 0.2s forwards ease-out`,
  105. '&.unmount': {
  106. animation: `${keyframes`
  107. 0% {
  108. margin-top: 0
  109. }
  110. 100% {
  111. margin-top: 15px
  112. }
  113. `} 0.15s forwards ease-in`
  114. }
  115. }
  116. },
  117. header: {
  118. width: '100%',
  119. padding: '24px',
  120. boxSizing: 'border-box',
  121. display: 'flex',
  122. alignItems: 'flex-start',
  123. justifyContent: 'space-between'
  124. },
  125. title: {
  126. color: theme.palette.text01,
  127. ...withPixelLineHeight(theme.typography.heading5),
  128. margin: 0,
  129. padding: 0
  130. },
  131. content: {
  132. height: 'auto',
  133. overflowY: 'auto',
  134. width: '100%',
  135. boxSizing: 'border-box',
  136. padding: '0 24px',
  137. overflowX: 'hidden',
  138. '@media (max-width: 448px)': {
  139. height: '100%'
  140. }
  141. },
  142. footer: {
  143. width: '100%',
  144. boxSizing: 'border-box',
  145. display: 'flex',
  146. alignItems: 'center',
  147. justifyContent: 'flex-end',
  148. padding: '24px',
  149. '& button:last-child': {
  150. marginLeft: '16px'
  151. }
  152. }
  153. };
  154. });
  155. interface DialogProps {
  156. cancel?: {
  157. hidden?: boolean;
  158. translationKey?: string;
  159. };
  160. children?: ReactElement | ReactElement[];
  161. description?: string;
  162. ok?: {
  163. disabled?: boolean;
  164. hidden?: boolean;
  165. translationKey?: string;
  166. };
  167. onCancel?: () => void;
  168. onSubmit?: () => void;
  169. size?: 'large' | 'medium';
  170. title?: string;
  171. titleKey?: string;
  172. }
  173. const Dialog = ({
  174. cancel = { translationKey: 'dialog.Cancel' },
  175. children,
  176. description,
  177. ok = { translationKey: 'dialog.Ok' },
  178. onCancel,
  179. onSubmit,
  180. size = 'medium',
  181. title,
  182. titleKey
  183. }: DialogProps) => {
  184. const { classes, cx } = useStyles();
  185. const { t } = useTranslation();
  186. const { isUnmounting } = useContext(DialogTransitionContext);
  187. const dispatch = useDispatch();
  188. const onClose = useCallback(() => {
  189. onCancel?.();
  190. dispatch(hideDialog());
  191. }, [ onCancel ]);
  192. const handleKeyDown = useCallback((e: KeyboardEvent) => {
  193. if (e.key === 'Escape') {
  194. onClose();
  195. }
  196. }, []);
  197. const submit = useCallback(() => {
  198. onSubmit?.();
  199. dispatch(hideDialog());
  200. }, [ onSubmit ]);
  201. useEffect(() => {
  202. window.addEventListener('keydown', handleKeyDown);
  203. return () => window.removeEventListener('keydown', handleKeyDown);
  204. }, []);
  205. return (
  206. <div className = { cx(classes.container, isUnmounting && 'unmount') }>
  207. <div
  208. className = { classes.backdrop }
  209. onClick = { onClose } />
  210. <div
  211. aria-describedby = { description }
  212. aria-labelledby = { title ?? t(titleKey ?? '') }
  213. aria-modal = { true }
  214. className = { cx(classes.modal, isUnmounting && 'unmount', size) }
  215. role = 'dialog'>
  216. <div className = { classes.header }>
  217. <p className = { classes.title }>{title ?? t(titleKey ?? '')}</p>
  218. <ClickableIcon
  219. accessibilityLabel = { t('dialog.close') }
  220. icon = { IconClose }
  221. onClick = { onClose } />
  222. </div>
  223. <div className = { classes.content }>{children}</div>
  224. <div className = { classes.footer }>
  225. {!cancel.hidden && <Button
  226. accessibilityLabel = { t(cancel.translationKey ?? '') }
  227. labelKey = { cancel.translationKey }
  228. onClick = { onClose }
  229. type = 'tertiary' />}
  230. {!ok.hidden && <Button
  231. accessibilityLabel = { t(ok.translationKey ?? '') }
  232. disabled = { ok.disabled }
  233. labelKey = { ok.translationKey }
  234. onClick = { submit } />}
  235. </div>
  236. </div>
  237. </div>
  238. );
  239. };
  240. export default Dialog;