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.

ContextMenu.tsx 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import { Theme } from '@mui/material';
  2. import React, { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
  3. import { useSelector } from 'react-redux';
  4. import { makeStyles } from 'tss-react/mui';
  5. // eslint-disable-next-line lines-around-comment
  6. // @ts-ignore
  7. import { Drawer, JitsiPortal } from '../../../../toolbox/components/web';
  8. import { showOverflowDrawer } from '../../../../toolbox/functions.web';
  9. import participantsPaneTheme from '../../../components/themes/participantsPaneTheme.json';
  10. import { withPixelLineHeight } from '../../../styles/functions.web';
  11. /**
  12. * Get a style property from a style declaration as a float.
  13. *
  14. * @param {CSSStyleDeclaration} styles - Style declaration.
  15. * @param {string} name - Property name.
  16. * @returns {number} Float value.
  17. */
  18. const getFloatStyleProperty = (styles: CSSStyleDeclaration, name: string) =>
  19. parseFloat(styles.getPropertyValue(name));
  20. /**
  21. * Gets the outer height of an element, including margins.
  22. *
  23. * @param {Element} element - Target element.
  24. * @returns {number} Computed height.
  25. */
  26. const getComputedOuterHeight = (element: HTMLElement) => {
  27. const computedStyle = getComputedStyle(element);
  28. return element.offsetHeight
  29. + getFloatStyleProperty(computedStyle, 'margin-top')
  30. + getFloatStyleProperty(computedStyle, 'margin-bottom');
  31. };
  32. interface IProps {
  33. /**
  34. * Accessibility label for menu container.
  35. */
  36. accessibilityLabel?: string;
  37. /**
  38. * Children of the context menu.
  39. */
  40. children: ReactNode;
  41. /**
  42. * Class name for context menu. Used to overwrite default styles.
  43. */
  44. className?: string;
  45. /**
  46. * The entity for which the context menu is displayed.
  47. */
  48. entity?: Object;
  49. /**
  50. * Whether or not the menu is hidden. Used to overwrite the internal isHidden.
  51. */
  52. hidden?: boolean;
  53. /**
  54. * Whether or not the menu is already in a drawer.
  55. */
  56. inDrawer?: boolean;
  57. /**
  58. * Whether or not drawer should be open.
  59. */
  60. isDrawerOpen?: boolean;
  61. /**
  62. * Target elements against which positioning calculations are made.
  63. */
  64. offsetTarget?: HTMLElement;
  65. /**
  66. * Callback for click on an item in the menu.
  67. */
  68. onClick?: (e?: React.MouseEvent) => void;
  69. /**
  70. * Callback for drawer close.
  71. */
  72. onDrawerClose?: (e?: React.MouseEvent) => void;
  73. /**
  74. * Keydown handler.
  75. */
  76. onKeyDown?: (e?: React.KeyboardEvent) => void;
  77. /**
  78. * Callback for the mouse entering the component.
  79. */
  80. onMouseEnter?: (e?: React.MouseEvent) => void;
  81. /**
  82. * Callback for the mouse leaving the component.
  83. */
  84. onMouseLeave?: (e?: React.MouseEvent) => void;
  85. }
  86. const MAX_HEIGHT = 400;
  87. const useStyles = makeStyles()((theme: Theme) => {
  88. return {
  89. contextMenu: {
  90. backgroundColor: theme.palette.ui01,
  91. border: `1px solid ${theme.palette.ui04}`,
  92. borderRadius: `${Number(theme.shape.borderRadius)}px`,
  93. boxShadow: '0px 4px 25px 4px rgba(20, 20, 20, 0.6)',
  94. color: theme.palette.text01,
  95. ...withPixelLineHeight(theme.typography.bodyShortRegular),
  96. marginTop: `${(participantsPaneTheme.panePadding * 2) + theme.typography.bodyShortRegular.fontSize}px`,
  97. position: 'absolute',
  98. right: `${participantsPaneTheme.panePadding}px`,
  99. top: 0,
  100. zIndex: 2,
  101. maxHeight: `${MAX_HEIGHT}px`,
  102. overflowY: 'auto',
  103. padding: `${theme.spacing(2)} 0`
  104. },
  105. contextMenuHidden: {
  106. pointerEvents: 'none',
  107. visibility: 'hidden'
  108. },
  109. drawer: {
  110. paddingTop: '16px',
  111. '& > div': {
  112. ...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
  113. '& svg': {
  114. fill: theme.palette.icon01
  115. }
  116. }
  117. }
  118. };
  119. });
  120. const ContextMenu = ({
  121. accessibilityLabel,
  122. children,
  123. className,
  124. entity,
  125. hidden,
  126. inDrawer,
  127. isDrawerOpen,
  128. offsetTarget,
  129. onClick,
  130. onKeyDown,
  131. onDrawerClose,
  132. onMouseEnter,
  133. onMouseLeave
  134. }: IProps) => {
  135. const [ isHidden, setIsHidden ] = useState(true);
  136. const containerRef = useRef<HTMLDivElement | null>(null);
  137. const { classes: styles, cx } = useStyles();
  138. const _overflowDrawer = useSelector(showOverflowDrawer);
  139. useLayoutEffect(() => {
  140. if (_overflowDrawer) {
  141. return;
  142. }
  143. if (entity && offsetTarget
  144. && containerRef.current
  145. && offsetTarget?.offsetParent
  146. && offsetTarget.offsetParent instanceof HTMLElement
  147. ) {
  148. const { current: container } = containerRef;
  149. const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
  150. const outerHeight = getComputedOuterHeight(container);
  151. const height = Math.min(MAX_HEIGHT, outerHeight);
  152. container.style.top = offsetTop + height > offsetHeight + scrollTop
  153. ? `${offsetTop - outerHeight}`
  154. : `${offsetTop}`;
  155. setIsHidden(false);
  156. } else {
  157. hidden === undefined && setIsHidden(true);
  158. }
  159. }, [ entity, offsetTarget, _overflowDrawer ]);
  160. useEffect(() => {
  161. if (hidden !== undefined) {
  162. setIsHidden(hidden);
  163. }
  164. }, [ hidden ]);
  165. if (_overflowDrawer && inDrawer) {
  166. return (<div
  167. className = { styles.drawer }
  168. onClick = { onDrawerClose }>
  169. {children}
  170. </div>);
  171. }
  172. return _overflowDrawer
  173. ? <JitsiPortal>
  174. <Drawer
  175. isOpen = { isDrawerOpen && _overflowDrawer }
  176. onClose = { onDrawerClose }>
  177. <div
  178. className = { styles.drawer }
  179. onClick = { onDrawerClose }>
  180. {children}
  181. </div>
  182. </Drawer>
  183. </JitsiPortal>
  184. : <div
  185. aria-label = { accessibilityLabel }
  186. className = { cx(participantsPaneTheme.ignoredChildClassName,
  187. styles.contextMenu,
  188. isHidden && styles.contextMenuHidden,
  189. className
  190. ) }
  191. onClick = { onClick }
  192. onKeyDown = { onKeyDown }
  193. onMouseEnter = { onMouseEnter }
  194. onMouseLeave = { onMouseLeave }
  195. ref = { containerRef }>
  196. {children}
  197. </div>;
  198. };
  199. export default ContextMenu;