feat(popover): create a wrapper around InlineDialog

This commit is contained in:
Leonard Kim 2017-08-22 12:22:06 -07:00 committed by yanas
parent c54879d605
commit 85f0ad2791
10 changed files with 265 additions and 190 deletions

33
css/_popover.scss Normal file
View File

@ -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%;
}

View File

@ -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.

View File

@ -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';

View File

@ -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 (
<div
className = { this.props.className }
id = { this.props.id }
onMouseEnter = { this._onShowDialog }
onMouseLeave = { this._onHideDialog }>
<InlineDialog
content = { this._renderContent() }
isOpen = { this.state.showDialog }
position = { this.props.position }>
{ this.props.children }
</InlineDialog>
</div>
);
}
/**
* 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 (
<div className = 'popover'>
{ content }
<div className = 'popover-mouse-padding-top' />
<div className = { _mapPositionToPaddingClass(position) } />
</div>
);
}
}
export default Popover;

View File

@ -0,0 +1 @@
export { default as Popover } from './Popover';

View File

@ -0,0 +1 @@
export * from './components';

View File

@ -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 (
<div
<Popover
className = 'indicator-container'
onMouseEnter = { this._onShowStats }
onMouseLeave = { this._onHideStats }>
<Popover
content = { this._renderStatisticsTable() }
isOpen = { this.state.showStats }
position = { this.props.statsPopoverPosition }>
<div className = 'popover-trigger'>
<div className = 'connection-indicator indicator'>
<div className = 'connection indicatoricon'>
{ this._renderIcon() }
</div>
content = { this._renderStatisticsTable() }
position = { this.props.statsPopoverPosition }>
<div className = 'popover-trigger'>
<div className = 'connection-indicator indicator'>
<div className = 'connection indicatoricon'>
{ this._renderIcon() }
</div>
</div>
</Popover>
</div>
</div>
</Popover>
);
}
/**
* 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 (
<div>
<ConnectionStatsTable
bandwidth = { bandwidth }
bitrate = { bitrate }
framerate = { framerate }
isLocalVideo = { this.props.isLocalVideo }
onShowMore = { this._onToggleShowMore }
packetLoss = { packetLoss }
resolution = { resolution }
shouldShowMore = { this.state.showMoreStats }
transport = { transport } />
<div className = 'popover-mouse-top-padding' />
<div
className = { interfaceConfig.VERTICAL_FILMSTRIP
? 'popover-mousemove-padding-right'
: 'popover-mousemove-padding-bottom' } />
</div>
<ConnectionStatsTable
bandwidth = { bandwidth }
bitrate = { bitrate }
framerate = { framerate }
isLocalVideo = { this.props.isLocalVideo }
onShowMore = { this._onToggleShowMore }
packetLoss = { packetLoss }
resolution = { resolution }
shouldShowMore = { this.state.showMoreStats }
transport = { transport } />
);
}
}

View File

@ -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 (
<div
onMouseEnter = { this._onShowRemoteMenu }
onMouseLeave = { this._onHideRemoteMenu }>
<Popover
content = { content }
isOpen = { this.state.showRemoteMenu }
position = { interfaceConfig.VERTICAL_FILMSTRIP
? 'left middle' : 'top center' }>
<span
className = 'popover-trigger remote-video-menu-trigger'>
<i
className = 'icon-thumb-menu'
title = 'Remote user controls' />
</span>
</Popover>
</div>
<Popover
content = { content }
onPopoverOpen = { this._onShowRemoteMenu }
position = { interfaceConfig.VERTICAL_FILMSTRIP
? 'left middle' : 'top center' }>
<span
className = 'popover-trigger remote-video-menu-trigger'>
<i
className = 'icon-thumb-menu'
title = 'Remote user controls' />
</span>
</Popover>
);
}
/**
* 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 {
<MuteButton
isAudioMuted = { isAudioMuted }
key = 'mute'
onClick = { this._onHideRemoteMenu }
participantID = { participantID } />
);
buttons.push(
<KickButton
key = 'kick'
onClick = { this._onHideRemoteMenu }
participantID = { participantID } />
);
}
@ -204,15 +182,9 @@ class RemoteVideoMenuTriggerButton extends Component {
if (buttons.length > 0) {
return (
<div>
<RemoteVideoMenu id = { participantID }>
{ buttons }
</RemoteVideoMenu>
<div
className = { interfaceConfig.VERTICAL_FILMSTRIP
? 'popover-mousemove-padding-right'
: 'popover-mousemove-padding-bottom' } />
</div>
<RemoteVideoMenu id = { participantID }>
{ buttons }
</RemoteVideoMenu>
);
}

View File

@ -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 (
<div
<Popover
className = { classNames }
content = { <VideoQualityDialog /> }
id = 'videoResolutionLabel'
onMouseEnter = { this._onShowQualityDialog }
onMouseLeave = { this._onHideQualityDialog }>
<Popover
content = { this._renderQualityDialog() }
isOpen = { this.state.showVideoQualityDialog }
position = { 'left top' }>
<div
className = 'video-quality-label-status'>
{ _audioOnly
? <i className = 'icon-visibility-off' />
: this._mapResolutionToTranslation(_resolution) }
</div>
</Popover>
</div>
position = { 'left top' }>
<div
className = 'video-quality-label-status'>
{ _audioOnly
? <i className = 'icon-visibility-off' />
: this._mapResolutionToTranslation(_resolution) }
</div>
</Popover>
);
}
@ -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 (
<div>
<VideoQualityDialog />
<div className = 'popover-mousemove-padding-right' />
</div>
);
}
}
/**