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.6KB

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