您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

ContextMenu.tsx 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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. import { spacing } from '../../Tokens';
  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. * ARIA attributes.
  34. */
  35. [key: `aria-${string}`]: string;
  36. /**
  37. * Accessibility label for menu container.
  38. */
  39. accessibilityLabel?: string;
  40. /**
  41. * Children of the context menu.
  42. */
  43. children: ReactNode;
  44. /**
  45. * Class name for context menu. Used to overwrite default styles.
  46. */
  47. className?: string;
  48. /**
  49. * The entity for which the context menu is displayed.
  50. */
  51. entity?: Object;
  52. /**
  53. * Whether or not the menu is hidden. Used to overwrite the internal isHidden.
  54. */
  55. hidden?: boolean;
  56. /**
  57. * Optional id.
  58. */
  59. id?: string;
  60. /**
  61. * Whether or not the menu is already in a drawer.
  62. */
  63. inDrawer?: boolean;
  64. /**
  65. * Whether or not drawer should be open.
  66. */
  67. isDrawerOpen?: boolean;
  68. /**
  69. * Target elements against which positioning calculations are made.
  70. */
  71. offsetTarget?: HTMLElement;
  72. /**
  73. * Callback for click on an item in the menu.
  74. */
  75. onClick?: (e?: React.MouseEvent) => void;
  76. /**
  77. * Callback for drawer close.
  78. */
  79. onDrawerClose?: (e?: React.MouseEvent) => void;
  80. /**
  81. * Keydown handler.
  82. */
  83. onKeyDown?: (e?: React.KeyboardEvent) => void;
  84. /**
  85. * Callback for the mouse entering the component.
  86. */
  87. onMouseEnter?: (e?: React.MouseEvent) => void;
  88. /**
  89. * Callback for the mouse leaving the component.
  90. */
  91. onMouseLeave?: (e?: React.MouseEvent) => void;
  92. /**
  93. * Container role.
  94. */
  95. role?: string;
  96. /**
  97. * Tab index for the menu.
  98. */
  99. tabIndex?: number;
  100. }
  101. const MAX_HEIGHT = 400;
  102. const useStyles = makeStyles()(theme => {
  103. return {
  104. contextMenu: {
  105. backgroundColor: theme.palette.ui01,
  106. border: `1px solid ${theme.palette.ui04}`,
  107. borderRadius: `${Number(theme.shape.borderRadius)}px`,
  108. boxShadow: '0px 1px 2px rgba(41, 41, 41, 0.25)',
  109. color: theme.palette.text01,
  110. ...withPixelLineHeight(theme.typography.bodyShortRegular),
  111. marginTop: `${(participantsPaneTheme.panePadding * 2) + theme.typography.bodyShortRegular.fontSize}px`,
  112. position: 'absolute',
  113. right: `${participantsPaneTheme.panePadding}px`,
  114. top: 0,
  115. zIndex: 2,
  116. maxHeight: `${MAX_HEIGHT}px`,
  117. overflowY: 'auto',
  118. padding: `${theme.spacing(2)} 0`
  119. },
  120. contextMenuHidden: {
  121. pointerEvents: 'none',
  122. visibility: 'hidden'
  123. },
  124. drawer: {
  125. paddingTop: '16px',
  126. '& > div': {
  127. ...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
  128. '& svg': {
  129. fill: theme.palette.icon01
  130. }
  131. }
  132. }
  133. };
  134. });
  135. const ContextMenu = ({
  136. accessibilityLabel,
  137. children,
  138. className,
  139. entity,
  140. hidden,
  141. id,
  142. inDrawer,
  143. isDrawerOpen,
  144. offsetTarget,
  145. onClick,
  146. onKeyDown,
  147. onDrawerClose,
  148. onMouseEnter,
  149. onMouseLeave,
  150. role,
  151. tabIndex,
  152. ...aria
  153. }: IProps) => {
  154. const [ isHidden, setIsHidden ] = useState(true);
  155. const containerRef = useRef<HTMLDivElement | null>(null);
  156. const { classes: styles, cx } = useStyles();
  157. const _overflowDrawer = useSelector(showOverflowDrawer);
  158. useLayoutEffect(() => {
  159. if (_overflowDrawer) {
  160. return;
  161. }
  162. if (entity && offsetTarget
  163. && containerRef.current
  164. && offsetTarget?.offsetParent
  165. && offsetTarget.offsetParent instanceof HTMLElement
  166. ) {
  167. const { current: container } = containerRef;
  168. // make sure the max height is not set
  169. // @ts-ignore
  170. container.style.maxHeight = null;
  171. const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
  172. let outerHeight = getComputedOuterHeight(container);
  173. let height = Math.min(MAX_HEIGHT, outerHeight);
  174. if (offsetTop + height > offsetHeight + scrollTop && height > offsetTop) {
  175. // top offset and + padding + border
  176. container.style.maxHeight = `${offsetTop - ((spacing[2] * 2) + 2)}px`;
  177. }
  178. // get the height after style changes
  179. outerHeight = getComputedOuterHeight(container);
  180. height = Math.min(MAX_HEIGHT, outerHeight);
  181. container.style.top = offsetTop + height > offsetHeight + scrollTop
  182. ? `${offsetTop - outerHeight}`
  183. : `${offsetTop}`;
  184. setIsHidden(false);
  185. } else {
  186. hidden === undefined && setIsHidden(true);
  187. }
  188. }, [ entity, offsetTarget, _overflowDrawer ]);
  189. useEffect(() => {
  190. if (hidden !== undefined) {
  191. setIsHidden(hidden);
  192. }
  193. }, [ hidden ]);
  194. if (_overflowDrawer && inDrawer) {
  195. return (<div
  196. className = { styles.drawer }
  197. onClick = { onDrawerClose }>
  198. {children}
  199. </div>);
  200. }
  201. return _overflowDrawer
  202. ? <JitsiPortal>
  203. <Drawer
  204. isOpen = { Boolean(isDrawerOpen && _overflowDrawer) }
  205. onClose = { onDrawerClose }>
  206. <div
  207. className = { styles.drawer }
  208. onClick = { onDrawerClose }>
  209. {children}
  210. </div>
  211. </Drawer>
  212. </JitsiPortal>
  213. : <div
  214. { ...aria }
  215. aria-label = { accessibilityLabel }
  216. className = { cx(participantsPaneTheme.ignoredChildClassName,
  217. styles.contextMenu,
  218. isHidden && styles.contextMenuHidden,
  219. className
  220. ) }
  221. id = { id }
  222. onClick = { onClick }
  223. onKeyDown = { onKeyDown }
  224. onMouseEnter = { onMouseEnter }
  225. onMouseLeave = { onMouseLeave }
  226. ref = { containerRef }
  227. role = { role }
  228. tabIndex = { tabIndex }>
  229. {children}
  230. </div>;
  231. };
  232. export default ContextMenu;