mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
Create Tooltip component Fix Popover positioning calculations Add margins to popover Remove @atlaskit/tooltip Update all components to use the new Tooltip component Added tooltip actions and reducers for the following functionality: when a user hovers over an element is sees the tooltip for that element and then hovers another element that has a tooltip, instead of using the delay and animations we just unmount the current tooltip and mount the next one immediately
456 lines
12 KiB
TypeScript
456 lines
12 KiB
TypeScript
import React, { Component, ReactNode } from 'react';
|
|
import ReactFocusLock from 'react-focus-lock';
|
|
|
|
import { IReduxState } from '../../../app/types';
|
|
import DialogPortal from '../../../toolbox/components/web/DialogPortal';
|
|
import Drawer from '../../../toolbox/components/web/Drawer';
|
|
import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
|
|
import { connect } from '../../redux/functions';
|
|
import { getContextMenuStyle } from '../functions.web';
|
|
|
|
/**
|
|
* The type of the React {@code Component} props of {@link Popover}.
|
|
*/
|
|
interface IProps {
|
|
|
|
/**
|
|
* Whether the child element can be clicked on.
|
|
*/
|
|
allowClick?: boolean;
|
|
|
|
/**
|
|
* A child React Element to use as the trigger for showing the dialog.
|
|
*/
|
|
children: ReactNode;
|
|
|
|
/**
|
|
* Additional CSS classnames to apply to the root of the {@code Popover}
|
|
* component.
|
|
*/
|
|
className?: string;
|
|
|
|
/**
|
|
* The ReactElement to display within the dialog.
|
|
*/
|
|
content: ReactNode;
|
|
|
|
/**
|
|
* Whether displaying of the popover should be prevented.
|
|
*/
|
|
disablePopover?: boolean;
|
|
|
|
/**
|
|
* The id of the dom element acting as the Popover label (matches aria-labelledby).
|
|
*/
|
|
headingId?: string;
|
|
|
|
/**
|
|
* String acting as the Popover label (matches aria-label).
|
|
*
|
|
* If headingId is set, this will not be used.
|
|
*/
|
|
headingLabel?: string;
|
|
|
|
/**
|
|
* An id attribute to apply to the root of the {@code Popover}
|
|
* component.
|
|
*/
|
|
id?: string;
|
|
|
|
/**
|
|
* Callback to invoke when the popover has closed.
|
|
*/
|
|
onPopoverClose: Function;
|
|
|
|
/**
|
|
* Callback to invoke when the popover has opened.
|
|
*/
|
|
onPopoverOpen?: Function;
|
|
|
|
/**
|
|
* Whether to display the Popover as a drawer.
|
|
*/
|
|
overflowDrawer?: boolean;
|
|
|
|
/**
|
|
* Where should the popover content be placed.
|
|
*/
|
|
position: string;
|
|
|
|
/**
|
|
* Whether the trigger for open/ close should be click or hover.
|
|
*/
|
|
trigger?: 'hover' | 'click';
|
|
|
|
/**
|
|
* Whether the popover is visible or not.
|
|
*/
|
|
visible: boolean;
|
|
}
|
|
|
|
/**
|
|
* The type of the React {@code Component} state of {@link Popover}.
|
|
*/
|
|
interface IState {
|
|
|
|
/**
|
|
* The style to apply to the context menu in order to position it correctly.
|
|
*/
|
|
contextMenuStyle?: {
|
|
bottom?: string;
|
|
left?: string;
|
|
position: string;
|
|
top?: string;
|
|
} | null;
|
|
}
|
|
|
|
/**
|
|
* Implements a React {@code Component} for showing an {@code Popover} on
|
|
* mouseenter of the trigger and contents, and hiding the dialog on mouseleave.
|
|
*
|
|
* @augments Component
|
|
*/
|
|
class Popover extends Component<IProps, IState> {
|
|
/**
|
|
* Default values for {@code Popover} component's properties.
|
|
*
|
|
* @static
|
|
*/
|
|
static defaultProps = {
|
|
className: '',
|
|
id: '',
|
|
trigger: 'hover'
|
|
};
|
|
|
|
/**
|
|
* Reference to the dialog container.
|
|
*/
|
|
_containerRef: React.RefObject<HTMLDivElement>;
|
|
|
|
_contextMenuRef: HTMLElement;
|
|
|
|
/**
|
|
* Initializes a new {@code Popover} instance.
|
|
*
|
|
* @param {Object} props - The read-only properties with which the new
|
|
* instance is to be initialized.
|
|
*/
|
|
constructor(props: IProps) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
contextMenuStyle: null
|
|
};
|
|
|
|
// Bind event handlers so they are only bound once for every instance.
|
|
this._onHideDialog = this._onHideDialog.bind(this);
|
|
this._onShowDialog = this._onShowDialog.bind(this);
|
|
this._onKeyPress = this._onKeyPress.bind(this);
|
|
this._containerRef = React.createRef();
|
|
this._onEscKey = this._onEscKey.bind(this);
|
|
this._onClick = this._onClick.bind(this);
|
|
this._onTouchStart = this._onTouchStart.bind(this);
|
|
this._setContextMenuRef = this._setContextMenuRef.bind(this);
|
|
this._setContextMenuStyle = this._setContextMenuStyle.bind(this);
|
|
this._getCustomDialogStyle = this._getCustomDialogStyle.bind(this);
|
|
this._onOutsideClick = this._onOutsideClick.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Sets up a touch event listener to attach.
|
|
*
|
|
* @inheritdoc
|
|
* @returns {void}
|
|
*/
|
|
componentDidMount() {
|
|
window.addEventListener('touchstart', this._onTouchStart);
|
|
if (this.props.trigger === 'click') {
|
|
// @ts-ignore
|
|
window.addEventListener('click', this._onOutsideClick);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the listener set up in the {@code componentDidMount} method.
|
|
*
|
|
* @inheritdoc
|
|
* @returns {void}
|
|
*/
|
|
componentWillUnmount() {
|
|
window.removeEventListener('touchstart', this._onTouchStart);
|
|
if (this.props.trigger === 'click') {
|
|
// @ts-ignore
|
|
window.removeEventListener('click', this._onOutsideClick);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles click outside the popover.
|
|
*
|
|
* @param {MouseEvent} e - The click event.
|
|
* @returns {void}
|
|
*/
|
|
_onOutsideClick(e: React.MouseEvent) { // @ts-ignore
|
|
if (!this._containerRef?.current?.contains(e.target) && this.props.visible) {
|
|
this._onHideDialog();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#render()}.
|
|
*
|
|
* @inheritdoc
|
|
* @returns {ReactElement}
|
|
*/
|
|
render() {
|
|
const { children,
|
|
className,
|
|
content,
|
|
headingId,
|
|
headingLabel,
|
|
id,
|
|
overflowDrawer,
|
|
visible,
|
|
trigger
|
|
} = this.props;
|
|
|
|
if (overflowDrawer) {
|
|
return (
|
|
<div
|
|
className = { className }
|
|
id = { id }
|
|
onClick = { this._onShowDialog }>
|
|
{ children }
|
|
<JitsiPortal>
|
|
<Drawer
|
|
headingId = { headingId }
|
|
isOpen = { visible }
|
|
onClose = { this._onHideDialog }>
|
|
{ content }
|
|
</Drawer>
|
|
</JitsiPortal>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className = { className }
|
|
id = { id }
|
|
onClick = { this._onClick }
|
|
onKeyPress = { this._onKeyPress }
|
|
{ ...(trigger === 'hover' ? {
|
|
onMouseEnter: this._onShowDialog,
|
|
onMouseLeave: this._onHideDialog,
|
|
tabIndex: 0
|
|
} : {}) }
|
|
ref = { this._containerRef }>
|
|
{ visible && (
|
|
<DialogPortal
|
|
getRef = { this._setContextMenuRef }
|
|
setSize = { this._setContextMenuStyle }
|
|
style = { this.state.contextMenuStyle }
|
|
targetSelector = '.popover-content'>
|
|
<ReactFocusLock
|
|
lockProps = {{
|
|
role: 'dialog',
|
|
'aria-modal': true,
|
|
'aria-labelledby': headingId,
|
|
'aria-label': !headingId && headingLabel ? headingLabel : undefined
|
|
}}
|
|
returnFocus = { true }>
|
|
{this._renderContent()}
|
|
</ReactFocusLock>
|
|
</DialogPortal>
|
|
)}
|
|
{ children }
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sets the context menu dialog style for positioning it on screen.
|
|
*
|
|
* @param {DOMRectReadOnly} size -The size info of the current context menu.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_setContextMenuStyle(size: DOMRectReadOnly) {
|
|
const style = this._getCustomDialogStyle(size);
|
|
|
|
this.setState({ contextMenuStyle: style });
|
|
}
|
|
|
|
/**
|
|
* Sets the context menu's ref.
|
|
*
|
|
* @param {HTMLElement} elem -The html element of the context menu.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_setContextMenuRef(elem: HTMLElement) {
|
|
this._contextMenuRef = elem;
|
|
}
|
|
|
|
/**
|
|
* Hide dialog on touch outside of the context menu.
|
|
*
|
|
* @param {TouchEvent} event - The touch event.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onTouchStart(event: TouchEvent) {
|
|
if (this.props.visible
|
|
&& !this.props.overflowDrawer
|
|
&& this._contextMenuRef
|
|
&& this._contextMenuRef.contains // @ts-ignore
|
|
&& !this._contextMenuRef.contains(event.target)) {
|
|
this._onHideDialog();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops displaying the {@code Popover}.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onHideDialog() {
|
|
this.setState({
|
|
contextMenuStyle: null
|
|
});
|
|
|
|
if (this.props.onPopoverClose) {
|
|
this.props.onPopoverClose();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays the {@code Popover} and calls any registered onPopoverOpen
|
|
* callbacks.
|
|
*
|
|
* @param {Object} event - The mouse event or the keypress event to intercept.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onShowDialog(event?: React.MouseEvent | React.KeyboardEvent) {
|
|
event?.stopPropagation();
|
|
|
|
if (!this.props.disablePopover) {
|
|
this.props.onPopoverOpen?.();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prevents switching from tile view to stage view on accidentally clicking
|
|
* the popover thumbs.
|
|
*
|
|
* @param {Object} event - The mouse event or the keypress event to intercept.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onClick(event: React.MouseEvent) {
|
|
const { allowClick, trigger, visible } = this.props;
|
|
|
|
if (!allowClick) {
|
|
event.stopPropagation();
|
|
}
|
|
if (trigger === 'click') {
|
|
if (visible) {
|
|
this._onHideDialog();
|
|
} else {
|
|
this._onShowDialog();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* KeyPress handler for accessibility.
|
|
*
|
|
* @param {Object} e - The key event to handle.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onKeyPress(e: React.KeyboardEvent) {
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (this.props.visible) {
|
|
this._onHideDialog();
|
|
} else {
|
|
this._onShowDialog(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* KeyPress handler for accessibility.
|
|
*
|
|
* @param {Object} e - The key event to handle.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onEscKey(e: React.KeyboardEvent) {
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (this.props.visible) {
|
|
this._onHideDialog();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets style for positioning the context menu on screen in regards to the trigger's
|
|
* position.
|
|
*
|
|
* @param {DOMRectReadOnly} size -The current context menu's size info.
|
|
*
|
|
* @returns {Object} - The new style of the context menu.
|
|
*/
|
|
_getCustomDialogStyle(size: DOMRectReadOnly) {
|
|
if (this._containerRef?.current) {
|
|
const bounds = this._containerRef.current.getBoundingClientRect();
|
|
|
|
return getContextMenuStyle(bounds, size, this.props.position);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the React Element to be displayed in the {@code Popover}.
|
|
* Also adds padding to support moving the mouse from the trigger to the
|
|
* dialog to prevent mouseleave events.
|
|
*
|
|
* @private
|
|
* @returns {ReactElement}
|
|
*/
|
|
_renderContent() {
|
|
const { content, position } = this.props;
|
|
|
|
return (
|
|
<div
|
|
className = 'popover'
|
|
onKeyDown = { this._onEscKey }>
|
|
<div className = { `popover-content ${position.split('-')[0]}` }>
|
|
{ content }
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps (parts of) the Redux state to the associated {@code Popover}'s props.
|
|
*
|
|
* @param {Object} state - The Redux state.
|
|
* @param {Object} ownProps - The own props of the component.
|
|
* @private
|
|
* @returns {IProps}
|
|
*/
|
|
function _mapStateToProps(state: IReduxState) {
|
|
return {
|
|
overflowDrawer: state['features/toolbox'].overflowDrawer
|
|
};
|
|
}
|
|
|
|
export default connect(_mapStateToProps)(Popover);
|