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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
  2. import { FocusOn } from 'react-focus-on';
  3. import { makeStyles } from 'tss-react/mui';
  4. import { isElementInTheViewport } from '../../../base/ui/functions.web';
  5. import { DRAWER_MAX_HEIGHT } from '../../constants';
  6. interface IProps {
  7. /**
  8. * The component(s) to be displayed within the drawer menu.
  9. */
  10. children: ReactNode;
  11. /**
  12. * Class name for custom styles.
  13. */
  14. className?: string;
  15. /**
  16. * The id of the dom element acting as the Drawer label.
  17. */
  18. headingId?: string;
  19. /**
  20. * Whether the drawer should be shown or not.
  21. */
  22. isOpen: boolean;
  23. /**
  24. * Function that hides the drawer.
  25. */
  26. onClose?: Function;
  27. }
  28. const useStyles = makeStyles()(theme => {
  29. return {
  30. drawerMenuContainer: {
  31. height: '100dvh',
  32. display: 'flex',
  33. alignItems: 'flex-end'
  34. },
  35. drawer: {
  36. backgroundColor: theme.palette.ui01,
  37. maxHeight: `calc(${DRAWER_MAX_HEIGHT})`,
  38. borderRadius: '24px 24px 0 0',
  39. overflowY: 'auto',
  40. marginBottom: 'env(safe-area-inset-bottom, 0)',
  41. width: '100%',
  42. '& .overflow-menu': {
  43. margin: 'auto',
  44. fontSize: '1.2em',
  45. listStyleType: 'none',
  46. padding: 0,
  47. height: 'calc(80vh - 144px - 64px)',
  48. overflowY: 'auto',
  49. '& .overflow-menu-item': {
  50. boxSizing: 'border-box',
  51. height: '48px',
  52. padding: '12px 16px',
  53. alignItems: 'center',
  54. color: theme.palette.text01,
  55. cursor: 'pointer',
  56. display: 'flex',
  57. fontSize: '16px',
  58. '& div': {
  59. display: 'flex',
  60. flexDirection: 'row',
  61. alignItems: 'center'
  62. },
  63. '&.disabled': {
  64. cursor: 'initial',
  65. color: '#3b475c'
  66. }
  67. }
  68. }
  69. }
  70. };
  71. });
  72. /**
  73. * Component that displays the mobile friendly drawer on web.
  74. *
  75. * @returns {ReactElement}
  76. */
  77. function Drawer({
  78. children,
  79. className = '',
  80. headingId,
  81. isOpen,
  82. onClose
  83. }: IProps) {
  84. const { classes, cx } = useStyles();
  85. /**
  86. * Handles clicks within the menu, preventing the propagation of the click event.
  87. *
  88. * @param {Object} event - The click event.
  89. * @returns {void}
  90. */
  91. const handleInsideClick = useCallback(event => {
  92. event.stopPropagation();
  93. }, []);
  94. /**
  95. * Handles clicks outside of the menu, closing it, and also stopping further propagation.
  96. *
  97. * @param {Object} event - The click event.
  98. * @returns {void}
  99. */
  100. const handleOutsideClick = useCallback(event => {
  101. event.stopPropagation();
  102. onClose?.();
  103. }, [ onClose ]);
  104. /**
  105. * Handles pressing the escape key, closing the drawer.
  106. *
  107. * @param {KeyboardEvent<HTMLDivElement>} event - The keydown event.
  108. * @returns {void}
  109. */
  110. const handleEscKey = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
  111. if (event.key === 'Escape') {
  112. event.preventDefault();
  113. event.stopPropagation();
  114. onClose?.();
  115. }
  116. }, [ onClose ]);
  117. return (
  118. isOpen ? (
  119. <div
  120. className = { classes.drawerMenuContainer }
  121. onClick = { handleOutsideClick }
  122. onKeyDown = { handleEscKey }>
  123. <div
  124. className = { cx(classes.drawer, className) }
  125. onClick = { handleInsideClick }>
  126. <FocusOn
  127. returnFocus = {
  128. // If we return the focus to an element outside the viewport the page will scroll to
  129. // this element which in our case is undesirable and the element is outside of the
  130. // viewport on purpose (to be hidden). For example if we return the focus to the toolbox
  131. // when it is hidden the whole page will move up in order to show the toolbox. This is
  132. // usually followed up with displaying the toolbox (because now it is on focus) but
  133. // because of the animation the whole scenario looks like jumping large video.
  134. isElementInTheViewport
  135. }>
  136. <div
  137. aria-labelledby = { headingId ? `#${headingId}` : undefined }
  138. aria-modal = { true }
  139. data-autofocus = { true }
  140. role = 'dialog'
  141. tabIndex = { -1 }>
  142. {children}
  143. </div>
  144. </FocusOn>
  145. </div>
  146. </div>
  147. ) : null
  148. );
  149. }
  150. export default Drawer;