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

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