Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

Popover.web.tsx 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import React, { Component, ReactNode } from 'react';
  2. import ReactFocusLock from 'react-focus-lock';
  3. import { IReduxState } from '../../../app/types';
  4. import DialogPortal from '../../../toolbox/components/web/DialogPortal';
  5. import Drawer from '../../../toolbox/components/web/Drawer';
  6. import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
  7. import { isMobileBrowser } from '../../environment/utils';
  8. import { connect } from '../../redux/functions';
  9. import { getContextMenuStyle } from '../functions.web';
  10. /**
  11. * The type of the React {@code Component} props of {@link Popover}.
  12. */
  13. interface IProps {
  14. /**
  15. * A child React Element to use as the trigger for showing the dialog.
  16. */
  17. children: ReactNode;
  18. /**
  19. * Additional CSS classnames to apply to the root of the {@code Popover}
  20. * component.
  21. */
  22. className?: string;
  23. /**
  24. * The ReactElement to display within the dialog.
  25. */
  26. content: ReactNode;
  27. /**
  28. * Whether displaying of the popover should be prevented.
  29. */
  30. disablePopover?: boolean;
  31. /**
  32. * The id of the dom element acting as the Popover label (matches aria-labelledby).
  33. */
  34. headingId?: string;
  35. /**
  36. * String acting as the Popover label (matches aria-label).
  37. *
  38. * If headingId is set, this will not be used.
  39. */
  40. headingLabel?: string;
  41. /**
  42. * An id attribute to apply to the root of the {@code Popover}
  43. * component.
  44. */
  45. id?: string;
  46. /**
  47. * Callback to invoke when the popover has closed.
  48. */
  49. onPopoverClose: Function;
  50. /**
  51. * Callback to invoke when the popover has opened.
  52. */
  53. onPopoverOpen?: Function;
  54. /**
  55. * Whether to display the Popover as a drawer.
  56. */
  57. overflowDrawer?: boolean;
  58. /**
  59. * Where should the popover content be placed.
  60. */
  61. position: string;
  62. /**
  63. * Whether the trigger for open/ close should be click or hover.
  64. */
  65. trigger?: 'hover' | 'click';
  66. /**
  67. * Whether the popover is visible or not.
  68. */
  69. visible: boolean;
  70. }
  71. /**
  72. * The type of the React {@code Component} state of {@link Popover}.
  73. */
  74. interface IState {
  75. /**
  76. * The style to apply to the context menu in order to position it correctly.
  77. */
  78. contextMenuStyle?: {
  79. bottom?: string;
  80. left?: string;
  81. position: string;
  82. top?: string;
  83. } | null;
  84. }
  85. /**
  86. * Implements a React {@code Component} for showing an {@code Popover} on
  87. * mouseenter of the trigger and contents, and hiding the dialog on mouseleave.
  88. *
  89. * @augments Component
  90. */
  91. class Popover extends Component<IProps, IState> {
  92. /**
  93. * Default values for {@code Popover} component's properties.
  94. *
  95. * @static
  96. */
  97. static defaultProps = {
  98. className: '',
  99. id: '',
  100. trigger: 'hover'
  101. };
  102. /**
  103. * Reference to the dialog container.
  104. */
  105. _containerRef: React.RefObject<HTMLDivElement>;
  106. _contextMenuRef: HTMLElement;
  107. /**
  108. * Initializes a new {@code Popover} instance.
  109. *
  110. * @param {Object} props - The read-only properties with which the new
  111. * instance is to be initialized.
  112. */
  113. constructor(props: IProps) {
  114. super(props);
  115. this.state = {
  116. contextMenuStyle: null
  117. };
  118. // Bind event handlers so they are only bound once for every instance.
  119. this._onHideDialog = this._onHideDialog.bind(this);
  120. this._onShowDialog = this._onShowDialog.bind(this);
  121. this._onKeyPress = this._onKeyPress.bind(this);
  122. this._containerRef = React.createRef();
  123. this._onEscKey = this._onEscKey.bind(this);
  124. this._onClick = this._onClick.bind(this);
  125. this._onTouchStart = this._onTouchStart.bind(this);
  126. this._setContextMenuRef = this._setContextMenuRef.bind(this);
  127. this._setContextMenuStyle = this._setContextMenuStyle.bind(this);
  128. this._getCustomDialogStyle = this._getCustomDialogStyle.bind(this);
  129. this._onOutsideClick = this._onOutsideClick.bind(this);
  130. }
  131. /**
  132. * Sets up a touch event listener to attach.
  133. *
  134. * @inheritdoc
  135. * @returns {void}
  136. */
  137. componentDidMount() {
  138. window.addEventListener('touchstart', this._onTouchStart);
  139. if (this.props.trigger === 'click') {
  140. // @ts-ignore
  141. window.addEventListener('click', this._onOutsideClick);
  142. }
  143. }
  144. /**
  145. * Removes the listener set up in the {@code componentDidMount} method.
  146. *
  147. * @inheritdoc
  148. * @returns {void}
  149. */
  150. componentWillUnmount() {
  151. window.removeEventListener('touchstart', this._onTouchStart);
  152. if (this.props.trigger === 'click') {
  153. // @ts-ignore
  154. window.removeEventListener('click', this._onOutsideClick);
  155. }
  156. }
  157. /**
  158. * Handles click outside the popover.
  159. *
  160. * @param {MouseEvent} e - The click event.
  161. * @returns {void}
  162. */
  163. _onOutsideClick(e: React.MouseEvent) { // @ts-ignore
  164. if (!this._containerRef?.current?.contains(e.target) && this.props.visible) {
  165. this._onHideDialog();
  166. }
  167. }
  168. /**
  169. * Implements React's {@link Component#render()}.
  170. *
  171. * @inheritdoc
  172. * @returns {ReactElement}
  173. */
  174. render() {
  175. const { children,
  176. className,
  177. content,
  178. headingId,
  179. headingLabel,
  180. id,
  181. overflowDrawer,
  182. visible,
  183. trigger
  184. } = this.props;
  185. if (overflowDrawer) {
  186. return (
  187. <div
  188. className = { className }
  189. id = { id }
  190. onClick = { this._onShowDialog }>
  191. { children }
  192. <JitsiPortal>
  193. <Drawer
  194. headingId = { headingId }
  195. isOpen = { visible }
  196. onClose = { this._onHideDialog }>
  197. { content }
  198. </Drawer>
  199. </JitsiPortal>
  200. </div>
  201. );
  202. }
  203. return (
  204. <div
  205. className = { className }
  206. id = { id }
  207. onClick = { this._onClick }
  208. onKeyPress = { this._onKeyPress }
  209. { ...(trigger === 'hover' ? {
  210. onMouseEnter: this._onShowDialog,
  211. onMouseLeave: this._onHideDialog,
  212. tabIndex: 0
  213. } : {}) }
  214. ref = { this._containerRef }>
  215. { visible && (
  216. <DialogPortal
  217. getRef = { this._setContextMenuRef }
  218. setSize = { this._setContextMenuStyle }
  219. style = { this.state.contextMenuStyle }>
  220. <ReactFocusLock
  221. lockProps = {{
  222. role: 'dialog',
  223. 'aria-modal': true,
  224. 'aria-labelledby': headingId,
  225. 'aria-label': !headingId && headingLabel ? headingLabel : undefined
  226. }}
  227. returnFocus = { true }>
  228. {this._renderContent()}
  229. </ReactFocusLock>
  230. </DialogPortal>
  231. )}
  232. { children }
  233. </div>
  234. );
  235. }
  236. /**
  237. * Sets the context menu dialog style for positioning it on screen.
  238. *
  239. * @param {DOMRectReadOnly} size -The size info of the current context menu.
  240. *
  241. * @returns {void}
  242. */
  243. _setContextMenuStyle(size: DOMRectReadOnly) {
  244. const style = this._getCustomDialogStyle(size);
  245. this.setState({ contextMenuStyle: style });
  246. }
  247. /**
  248. * Sets the context menu's ref.
  249. *
  250. * @param {HTMLElement} elem -The html element of the context menu.
  251. *
  252. * @returns {void}
  253. */
  254. _setContextMenuRef(elem: HTMLElement) {
  255. this._contextMenuRef = elem;
  256. }
  257. /**
  258. * Hide dialog on touch outside of the context menu.
  259. *
  260. * @param {TouchEvent} event - The touch event.
  261. * @private
  262. * @returns {void}
  263. */
  264. _onTouchStart(event: TouchEvent) {
  265. if (this.props.visible
  266. && !this.props.overflowDrawer
  267. && this._contextMenuRef
  268. && this._contextMenuRef.contains // @ts-ignore
  269. && !this._contextMenuRef.contains(event.target)) {
  270. this._onHideDialog();
  271. }
  272. }
  273. /**
  274. * Stops displaying the {@code Popover}.
  275. *
  276. * @private
  277. * @returns {void}
  278. */
  279. _onHideDialog() {
  280. this.setState({
  281. contextMenuStyle: null
  282. });
  283. if (this.props.onPopoverClose) {
  284. this.props.onPopoverClose();
  285. }
  286. }
  287. /**
  288. * Displays the {@code Popover} and calls any registered onPopoverOpen
  289. * callbacks.
  290. *
  291. * @param {Object} event - The mouse event or the keypress event to intercept.
  292. * @private
  293. * @returns {void}
  294. */
  295. _onShowDialog(event?: React.MouseEvent | React.KeyboardEvent) {
  296. event?.stopPropagation();
  297. if (!this.props.disablePopover) {
  298. this.props.onPopoverOpen?.();
  299. }
  300. }
  301. /**
  302. * Prevents switching from tile view to stage view on accidentally clicking
  303. * the popover thumbs.
  304. *
  305. * @param {Object} event - The mouse event or the keypress event to intercept.
  306. * @private
  307. * @returns {void}
  308. */
  309. _onClick(event: React.MouseEvent) {
  310. const { trigger, visible } = this.props;
  311. event.stopPropagation();
  312. if (trigger === 'click') {
  313. if (visible) {
  314. this._onHideDialog();
  315. } else {
  316. this._onShowDialog();
  317. }
  318. }
  319. }
  320. /**
  321. * KeyPress handler for accessibility.
  322. *
  323. * @param {Object} e - The key event to handle.
  324. *
  325. * @returns {void}
  326. */
  327. _onKeyPress(e: React.KeyboardEvent) {
  328. if (e.key === ' ' || e.key === 'Enter') {
  329. e.preventDefault();
  330. if (this.props.visible) {
  331. this._onHideDialog();
  332. } else {
  333. this._onShowDialog(e);
  334. }
  335. }
  336. }
  337. /**
  338. * KeyPress handler for accessibility.
  339. *
  340. * @param {Object} e - The key event to handle.
  341. *
  342. * @returns {void}
  343. */
  344. _onEscKey(e: React.KeyboardEvent) {
  345. if (e.key === 'Escape') {
  346. e.preventDefault();
  347. e.stopPropagation();
  348. if (this.props.visible) {
  349. this._onHideDialog();
  350. }
  351. }
  352. }
  353. /**
  354. * Gets style for positioning the context menu on screen in regards to the trigger's
  355. * position.
  356. *
  357. * @param {DOMRectReadOnly} size -The current context menu's size info.
  358. *
  359. * @returns {Object} - The new style of the context menu.
  360. */
  361. _getCustomDialogStyle(size: DOMRectReadOnly) {
  362. if (this._containerRef?.current) {
  363. const bounds = this._containerRef.current.getBoundingClientRect();
  364. return getContextMenuStyle(bounds, size, this.props.position);
  365. }
  366. }
  367. /**
  368. * Renders the React Element to be displayed in the {@code Popover}.
  369. * Also adds padding to support moving the mouse from the trigger to the
  370. * dialog to prevent mouseleave events.
  371. *
  372. * @private
  373. * @returns {ReactElement}
  374. */
  375. _renderContent() {
  376. const { content } = this.props;
  377. return (
  378. <div
  379. className = 'popover'
  380. onKeyDown = { this._onEscKey }>
  381. { content }
  382. {!isMobileBrowser() && (
  383. <>
  384. <div className = 'popover-mousemove-padding-top' />
  385. <div className = 'popover-mousemove-padding-right' />
  386. <div className = 'popover-mousemove-padding-left' />
  387. <div className = 'popover-mousemove-padding-bottom' />
  388. </>)}
  389. </div>
  390. );
  391. }
  392. }
  393. /**
  394. * Maps (parts of) the Redux state to the associated {@code Popover}'s props.
  395. *
  396. * @param {Object} state - The Redux state.
  397. * @param {Object} ownProps - The own props of the component.
  398. * @private
  399. * @returns {IProps}
  400. */
  401. function _mapStateToProps(state: IReduxState) {
  402. return {
  403. overflowDrawer: state['features/toolbox'].overflowDrawer
  404. };
  405. }
  406. export default connect(_mapStateToProps)(Popover);