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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. drawer: {
  31. backgroundColor: theme.palette.ui01,
  32. maxHeight: `calc(${DRAWER_MAX_HEIGHT})`,
  33. borderRadius: '24px 24px 0 0'
  34. }
  35. };
  36. });
  37. /**
  38. * Component that displays the mobile friendly drawer on web.
  39. *
  40. * @returns {ReactElement}
  41. */
  42. function Drawer({
  43. children,
  44. className = '',
  45. headingId,
  46. isOpen,
  47. onClose
  48. }: IProps) {
  49. const { classes: styles } = useStyles();
  50. /**
  51. * Handles clicks within the menu, preventing the propagation of the click event.
  52. *
  53. * @param {Object} event - The click event.
  54. * @returns {void}
  55. */
  56. const handleInsideClick = useCallback(event => {
  57. event.stopPropagation();
  58. }, []);
  59. /**
  60. * Handles clicks outside of the menu, closing it, and also stopping further propagation.
  61. *
  62. * @param {Object} event - The click event.
  63. * @returns {void}
  64. */
  65. const handleOutsideClick = useCallback(event => {
  66. event.stopPropagation();
  67. onClose?.();
  68. }, [ onClose ]);
  69. /**
  70. * Handles pressing the escape key, closing the drawer.
  71. *
  72. * @param {KeyboardEvent<HTMLDivElement>} event - The keydown event.
  73. * @returns {void}
  74. */
  75. const handleEscKey = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
  76. if (event.key === 'Escape') {
  77. event.preventDefault();
  78. event.stopPropagation();
  79. onClose?.();
  80. }
  81. }, [ onClose ]);
  82. return (
  83. isOpen ? (
  84. <div
  85. className = 'drawer-menu-container'
  86. onClick = { handleOutsideClick }
  87. onKeyDown = { handleEscKey }>
  88. <div
  89. className = { `drawer-menu ${styles.drawer} ${className}` }
  90. onClick = { handleInsideClick }>
  91. <FocusOn
  92. returnFocus = {
  93. // If we return the focus to an element outside the viewport the page will scroll to
  94. // this element which in our case is undesirable and the element is outside of the
  95. // viewport on purpose (to be hidden). For example if we return the focus to the toolbox
  96. // when it is hidden the whole page will move up in order to show the toolbox. This is
  97. // usually followed up with displaying the toolbox (because now it is on focus) but
  98. // because of the animation the whole scenario looks like jumping large video.
  99. isElementInTheViewport
  100. }>
  101. <div
  102. aria-labelledby = { headingId ? `#${headingId}` : undefined }
  103. aria-modal = { true }
  104. data-autofocus = { true }
  105. role = 'dialog'
  106. tabIndex = { -1 }>
  107. {children}
  108. </div>
  109. </FocusOn>
  110. </div>
  111. </div>
  112. ) : null
  113. );
  114. }
  115. export default Drawer;