diff --git a/css/_popover.scss b/css/_popover.scss new file mode 100644 index 0000000000..2af5e8ed98 --- /dev/null +++ b/css/_popover.scss @@ -0,0 +1,33 @@ +/** + * Mousemove padding styles are used to add invisible elements to the popover + * to allow mouse movement from the popover trigger to the popover itself + * without triggering a mouseleave event. + */ +.popover-mousemove-padding-bottom { + bottom: -15px; + height: 20px; + position: absolute; + right: 0; + width: 100%; +} +.popover-mousemove-padding-right { + height: 100%; + position: absolute; + right: -20; + top: 0; + width: 40px; +} + +/** + * An invisible element is added to the top of the popover to ensure the mouse + * stays over the popover when the popover's height is shrunk, which would then + * normally leave the mouse outside of the popover itself and cause a mouseleave + * event. + */ +.popover-mouse-padding-top { + height: 30px; + position: absolute; + right: 0; + top: -25px; + width: 100%; +} diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index a6d5800328..21a1fedbcc 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -378,28 +378,6 @@ .remote-video-menu-trigger { margin-top: 7px; } -.popover-mousemove-padding-bottom { - bottom: -15px; - height: 20px; - position: absolute; - right: 0; - width: 100%; -} -.popover-mousemove-padding-right { - height: 100%; - position: absolute; - right: -20; - top: 0; - width: 40px; -} - -.popover-mouse-top-padding { - height: 30px; - position: absolute; - right: 0; - top: -25px; - width: 100%; -} /** * Audio indicator on video thumbnails. diff --git a/css/main.scss b/css/main.scss index 53db76ebe4..a0681c33e6 100644 --- a/css/main.scss +++ b/css/main.scss @@ -69,6 +69,7 @@ @import 'aui-components/dropdown'; @import '404'; @import 'policy'; +@import 'popover'; @import 'filmstrip'; @import 'unsupported-browser/main'; @import 'modals/invite/add-people'; diff --git a/react/features/base/popover/components/Popover.native.js b/react/features/base/popover/components/Popover.native.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/react/features/base/popover/components/Popover.web.js b/react/features/base/popover/components/Popover.web.js new file mode 100644 index 0000000000..64bcc60510 --- /dev/null +++ b/react/features/base/popover/components/Popover.web.js @@ -0,0 +1,179 @@ +import InlineDialog from '@atlaskit/inline-dialog'; +import React, { Component } from 'react'; + +/** + * A map of dialog positions, relative to trigger, to css classes used to + * manipulate elements for handling mouse events. + * + * @private + * @type {object} + */ +const DIALOG_TO_PADDING_POSITION = { + 'left': 'popover-mousemove-padding-right', + 'top': 'popover-mousemove-padding-bottom' +}; + +/** + * Takes the position expected by {@code InlineDialog} and maps it to a CSS + * class that can be used styling the elements used for preventing mouseleave + * events when moving from the trigger to the dialog. + * + * @param {string} position - From which position the dialog will display. + * @private + * @returns {string} + */ +function _mapPositionToPaddingClass(position = 'left') { + return DIALOG_TO_PADDING_POSITION[position.split(' ')[0]]; +} + +/** + * Implements a React {@code Component} for showing an {@code InlineDialog} on + * mouseenter of the trigger and contents, and hiding the dialog on mouseleave. + * + * @extends Component + */ +class Popover extends Component { + /** + * Default values for {@code Popover} component's properties. + * + * @static + */ + static defaultProps = { + className: '', + id: '' + }; + + /** + * {@code Popover} component's property types. + * + * @static + */ + static propTypes = { + /** + * A child React Element to use as the trigger for showing the dialog. + */ + children: React.PropTypes.object, + + /** + * Additional CSS classnames to apply to the root of the {@code Popover} + * component. + */ + className: React.PropTypes.string, + + /** + * The ReactElement to display within the dialog. + */ + content: React.PropTypes.object, + + /** + * An id attribute to apply to the root of the {@code Popover} + * component. + */ + id: React.PropTypes.string, + + /** + * Callback to invoke when the popover has opened. + */ + onPopoverOpen: React.PropTypes.func, + + /** + * From which side of the dialog trigger the dialog should display. The + * value will be passed to {@code InlineDialog}. + */ + position: React.PropTypes.string + }; + + /** + * Initializes a new {@code Popover} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + this.state = { + /** + * Whether or not the {@code InlineDialog} should be displayed. + * + * @type {boolean} + */ + showDialog: false + }; + + // Bind event handlers so they are only bound once for every instance. + this._onHideDialog = this._onHideDialog.bind(this); + this._onShowDialog = this._onShowDialog.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( +
+ + { this.props.children } + +
+ ); + } + + /** + * Stops displaying the {@code InlineDialog}. + * + * @private + * @returns {void} + */ + _onHideDialog() { + this.setState({ showDialog: false }); + } + + /** + * Displays the {@code InlineDialog} and calls any registered onPopoverOpen + * callbacks. + * + * @private + * @returns {void} + */ + _onShowDialog() { + this.setState({ showDialog: true }); + + if (this.props.onPopoverOpen) { + this.props.onPopoverOpen(); + } + + } + + /** + * Renders the React Element to be displayed in the {@code InlineDialog}. + * 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 ( +
+ { content } +
+
+
+ ); + } +} + +export default Popover; diff --git a/react/features/base/popover/components/index.js b/react/features/base/popover/components/index.js new file mode 100644 index 0000000000..02df6c38e8 --- /dev/null +++ b/react/features/base/popover/components/index.js @@ -0,0 +1 @@ +export { default as Popover } from './Popover'; diff --git a/react/features/base/popover/index.js b/react/features/base/popover/index.js new file mode 100644 index 0000000000..07635cbbc8 --- /dev/null +++ b/react/features/base/popover/index.js @@ -0,0 +1 @@ +export * from './components'; diff --git a/react/features/connection-indicator/components/ConnectionIndicator.js b/react/features/connection-indicator/components/ConnectionIndicator.js index ee9ec52d60..e0c02c513c 100644 --- a/react/features/connection-indicator/components/ConnectionIndicator.js +++ b/react/features/connection-indicator/components/ConnectionIndicator.js @@ -1,7 +1,7 @@ -import { default as Popover } from '@atlaskit/inline-dialog'; import React, { Component } from 'react'; import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet'; +import { Popover } from '../../base/popover'; import { ConnectionStatsTable } from '../../connection-stats'; import statsEmitter from '../statsEmitter'; @@ -123,8 +123,6 @@ class ConnectionIndicator extends Component { }; // Bind event handlers so they are only bound once for every instance. - this._onHideStats = this._onHideStats.bind(this); - this._onShowStats = this._onShowStats.bind(this); this._onStatsUpdated = this._onStatsUpdated.bind(this); this._onToggleShowMore = this._onToggleShowMore.bind(this); } @@ -174,48 +172,21 @@ class ConnectionIndicator extends Component { */ render() { return ( -
- -
-
-
- { this._renderIcon() } -
+ content = { this._renderStatisticsTable() } + position = { this.props.statsPopoverPosition }> +
+
+
+ { this._renderIcon() }
- -
+
+ ); } - /** - * Sets the state not to show the Statistics Table popover. - * - * @private - * @returns {void} - */ - _onHideStats() { - this.setState({ showStats: false }); - } - - /** - * Sets the state to show the Statistics Table popover. - * - * @private - * @returns {void} - */ - _onShowStats() { - if (this.props.enableStatsDisplay) { - this.setState({ showStats: true }); - } - } - /** * Callback invoked when new connection stats associated with the passed in * user ID are available. Will update the component's display of current @@ -295,9 +266,7 @@ class ConnectionIndicator extends Component { } /** - * Creates a {@code ConnectionStatisticsTable} instance and an empty div - * for preventing mouseleave events when moving from the icon to the - * popover. + * Creates a {@code ConnectionStatisticsTable} instance. * * @returns {ReactElement} */ @@ -312,23 +281,16 @@ class ConnectionIndicator extends Component { } = this.state.stats; return ( -
- -
-
-
+ ); } } diff --git a/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js b/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js index 41b6f8f54d..feac5c7c03 100644 --- a/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js +++ b/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js @@ -1,4 +1,3 @@ -import { default as Popover } from '@atlaskit/inline-dialog'; import React, { Component } from 'react'; import { @@ -9,6 +8,8 @@ import { VolumeSlider } from './'; +import { Popover } from '../../base/popover'; + declare var $: Object; declare var interfaceConfig: Object; @@ -73,10 +74,6 @@ class RemoteVideoMenuTriggerButton extends Component { constructor(props) { super(props); - this.state = { - showRemoteMenu: false - }; - /** * The internal reference to topmost DOM/HTML element backing the React * {@code Component}. Accessed directly for associating an element as @@ -87,8 +84,7 @@ class RemoteVideoMenuTriggerButton extends Component { */ this._rootElement = null; - // Bind event handlers so they are only bound once for every instance. - this._onHideRemoteMenu = this._onHideRemoteMenu.bind(this); + // Bind event handler so it is only bound once for every instance. this._onShowRemoteMenu = this._onShowRemoteMenu.bind(this); } @@ -106,35 +102,21 @@ class RemoteVideoMenuTriggerButton extends Component { } return ( -
- - - - - -
+ + + + + ); } - /** - * Closes the {@code RemoteVideoMenu}. - * - * @private - * @returns {void} - */ - _onHideRemoteMenu() { - this.setState({ showRemoteMenu: false }); - } - /** * Opens the {@code RemoteVideoMenu}. * @@ -143,8 +125,6 @@ class RemoteVideoMenuTriggerButton extends Component { */ _onShowRemoteMenu() { this.props.onMenuDisplay(); - - this.setState({ showRemoteMenu: true }); } /** @@ -172,13 +152,11 @@ class RemoteVideoMenuTriggerButton extends Component { ); buttons.push( ); } @@ -204,15 +182,9 @@ class RemoteVideoMenuTriggerButton extends Component { if (buttons.length > 0) { return ( -
- - { buttons } - -
-
+ + { buttons } + ); } diff --git a/react/features/video-quality/components/VideoQualityLabel.web.js b/react/features/video-quality/components/VideoQualityLabel.web.js index 67e3563f8e..784b4b0961 100644 --- a/react/features/video-quality/components/VideoQualityLabel.web.js +++ b/react/features/video-quality/components/VideoQualityLabel.web.js @@ -1,9 +1,8 @@ -import { default as Popover } from '@atlaskit/inline-dialog'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { translate } from '../../base/i18n'; - +import { Popover } from '../../base/popover'; import { VideoQualityDialog } from './'; import { @@ -89,13 +88,6 @@ export class VideoQualityLabel extends Component { super(props); this.state = { - /** - * Whether or not the {@code VideoQualityDialog} is displayed. - * - * @type {boolean} - */ - showVideoQualityDialog: false, - /** * Whether or not the filmstrip is transitioning from not visible * to visible. Used to set a transition class for animation. @@ -104,10 +96,6 @@ export class VideoQualityLabel extends Component { */ togglingToVisible: false }; - - // Bind event handlers so they are only bound once for every instance. - this._onHideQualityDialog = this._onHideQualityDialog.bind(this); - this._onShowQualityDialog = this._onShowQualityDialog.bind(this); } /** @@ -161,23 +149,18 @@ export class VideoQualityLabel extends Component { = `${baseClasses} ${filmstrip} ${remoteVideosVisible} ${opening}`; return ( -
} id = 'videoResolutionLabel' - onMouseEnter = { this._onShowQualityDialog } - onMouseLeave = { this._onHideQualityDialog }> - -
- { _audioOnly - ? - : this._mapResolutionToTranslation(_resolution) } -
-
-
+ position = { 'left top' }> +
+ { _audioOnly + ? + : this._mapResolutionToTranslation(_resolution) } +
+ ); } @@ -209,41 +192,6 @@ export class VideoQualityLabel extends Component { return this.props.t( RESOLUTION_TO_TRANSLATION_KEY[highestMatchingResolution]); } - - /** - * Shows the {@code VideoQualityDialog}. - * - * @private - * @returns {void} - */ - _onShowQualityDialog() { - this.setState({ showVideoQualityDialog: true }); - } - - /** - * Hides the {@code VideoQualityDialog}. - * - * @private - * @returns {void} - */ - _onHideQualityDialog() { - this.setState({ showVideoQualityDialog: false }); - } - - /** - * Returns a React Element for choosing a maximum receive video quality. - * - * @private - * @returns {ReactElement} - */ - _renderQualityDialog() { - return ( -
- -
-
- ); - } } /**