您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

BaseDialog.tsx 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import React, { ReactNode, useCallback, useContext, useEffect } from 'react';
  2. import { FocusOn } from 'react-focus-on';
  3. import { useTranslation } from 'react-i18next';
  4. import { keyframes } from 'tss-react';
  5. import { makeStyles } from 'tss-react/mui';
  6. import { withPixelLineHeight } from '../../../styles/functions.web';
  7. import { isElementInTheViewport } from '../../functions.web';
  8. import { DialogTransitionContext } from './DialogTransition';
  9. const useStyles = makeStyles()(theme => {
  10. return {
  11. container: {
  12. width: '100%',
  13. height: '100%',
  14. position: 'fixed',
  15. color: theme.palette.text01,
  16. ...withPixelLineHeight(theme.typography.bodyLongRegular),
  17. top: 0,
  18. left: 0,
  19. display: 'flex',
  20. justifyContent: 'center',
  21. alignItems: 'flex-start',
  22. zIndex: 301,
  23. animation: `${keyframes`
  24. 0% {
  25. opacity: 0.4;
  26. }
  27. 100% {
  28. opacity: 1;
  29. }
  30. `} 0.2s forwards ease-out`,
  31. '&.unmount': {
  32. animation: `${keyframes`
  33. 0% {
  34. opacity: 1;
  35. }
  36. 100% {
  37. opacity: 0.5;
  38. }
  39. `} 0.15s forwards ease-in`
  40. }
  41. },
  42. backdrop: {
  43. position: 'absolute',
  44. width: '100%',
  45. height: '100%',
  46. top: 0,
  47. left: 0,
  48. backgroundColor: theme.palette.ui02,
  49. opacity: 0.75
  50. },
  51. modal: {
  52. backgroundColor: theme.palette.ui01,
  53. border: `1px solid ${theme.palette.ui03}`,
  54. boxShadow: '0px 4px 25px 4px rgba(20, 20, 20, 0.6)',
  55. borderRadius: `${theme.shape.borderRadius}px`,
  56. display: 'flex',
  57. flexDirection: 'column',
  58. height: 'auto',
  59. minHeight: '200px',
  60. maxHeight: '80vh',
  61. marginTop: '64px',
  62. animation: `${keyframes`
  63. 0% {
  64. margin-top: 85px
  65. }
  66. 100% {
  67. margin-top: 64px
  68. }
  69. `} 0.2s forwards ease-out`,
  70. '&.medium': {
  71. width: '400px'
  72. },
  73. '&.large': {
  74. width: '664px'
  75. },
  76. '&.unmount': {
  77. animation: `${keyframes`
  78. 0% {
  79. margin-top: 64px
  80. }
  81. 100% {
  82. margin-top: 40px
  83. }
  84. `} 0.15s forwards ease-in`
  85. },
  86. '@media (max-width: 448px)': {
  87. width: '100% !important',
  88. maxHeight: 'initial',
  89. height: '100%',
  90. margin: 0,
  91. position: 'absolute',
  92. top: 0,
  93. left: 0,
  94. bottom: 0,
  95. animation: `${keyframes`
  96. 0% {
  97. margin-top: 15px
  98. }
  99. 100% {
  100. margin-top: 0
  101. }
  102. `} 0.2s forwards ease-out`,
  103. '&.unmount': {
  104. animation: `${keyframes`
  105. 0% {
  106. margin-top: 0
  107. }
  108. 100% {
  109. margin-top: 15px
  110. }
  111. `} 0.15s forwards ease-in`
  112. }
  113. }
  114. },
  115. focusLock: {
  116. zIndex: 1
  117. }
  118. };
  119. });
  120. export interface IProps {
  121. children?: ReactNode;
  122. className?: string;
  123. description?: string;
  124. disableBackdropClose?: boolean;
  125. disableEnter?: boolean;
  126. onClose?: () => void;
  127. size?: 'large' | 'medium';
  128. submit?: () => void;
  129. title?: string;
  130. titleKey?: string;
  131. }
  132. const BaseDialog = ({
  133. children,
  134. className,
  135. description,
  136. disableBackdropClose,
  137. disableEnter,
  138. onClose,
  139. size = 'medium',
  140. submit,
  141. title,
  142. titleKey
  143. }: IProps) => {
  144. const { classes, cx } = useStyles();
  145. const { isUnmounting } = useContext(DialogTransitionContext);
  146. const { t } = useTranslation();
  147. const onBackdropClick = useCallback(() => {
  148. !disableBackdropClose && onClose?.();
  149. }, [ disableBackdropClose, onClose ]);
  150. const handleKeyDown = useCallback((e: KeyboardEvent) => {
  151. if (e.key === 'Escape') {
  152. onClose?.();
  153. }
  154. if (e.key === 'Enter' && !disableEnter) {
  155. submit?.();
  156. }
  157. }, []);
  158. useEffect(() => {
  159. window.addEventListener('keydown', handleKeyDown);
  160. return () => window.removeEventListener('keydown', handleKeyDown);
  161. }, []);
  162. return (
  163. <div className = { cx(classes.container, isUnmounting && 'unmount') }>
  164. <div className = { classes.backdrop } />
  165. <FocusOn
  166. className = { classes.focusLock }
  167. onClickOutside = { onBackdropClick }
  168. returnFocus = {
  169. // If we return the focus to an element outside the viewport the page will scroll to
  170. // this element which in our case is undesirable and the element is outside of the
  171. // viewport on purpose (to be hidden). For example if we return the focus to the toolbox
  172. // when it is hidden the whole page will move up in order to show the toolbox. This is
  173. // usually followed up with displaying the toolbox (because now it is on focus) but
  174. // because of the animation the whole scenario looks like jumping large video.
  175. isElementInTheViewport
  176. }>
  177. <div
  178. aria-description = { description }
  179. aria-label = { title ?? t(titleKey ?? '') }
  180. aria-modal = { true }
  181. className = { cx(classes.modal, isUnmounting && 'unmount', size, className) }
  182. data-autofocus = { true }
  183. role = 'dialog'
  184. tabIndex = { -1 }>
  185. {children}
  186. </div>
  187. </FocusOn>
  188. </div>
  189. );
  190. };
  191. export default BaseDialog;