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.

Drawer.tsx 5.1KB

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