fix(toolbar): Move buttons to overflow menu when the space isn't enough

This commit is contained in:
Hristo Terezov 2019-03-05 14:26:45 +00:00
parent 27e1f5a1bc
commit a9d82a79ea
14 changed files with 378 additions and 30 deletions

View File

@ -40,3 +40,11 @@
.videocontainer .tOoji {
background: none;
}
/**
* Override @atlaskit/InlineDialog styling for the overflowmenu so it displays
* with the correct height.
*/
.toolbox-button-wth-dialog .eYJELv {
max-height: initial;
}

View File

@ -190,6 +190,14 @@
cursor: initial;
color: #3b475c;
}
i.toggled {
background: inherit;
}
i.toggled:hover {
background: inherit;
}
}
.beta-tag {

View File

@ -66,7 +66,7 @@
}
.info-dialog-dial-in {
white-space: nowrap;
word-break: break-all;
.conference-id,
.phone-number {
@ -77,6 +77,7 @@
.info-dialog-icon {
color: #6453C0;
font-size: 16px;
min-width: 30px;
}
.info-dialog-url-text,

View File

@ -362,7 +362,8 @@
"numbers": "Dial-in Numbers",
"password": "Password:",
"title": "Share",
"tooltip": "Share link and dial-in info for this meeting"
"tooltip": "Share link and dial-in info for this meeting",
"label": "Meeting info"
},
"inviteDialog": {
"alertOk": "Ok",
@ -631,11 +632,14 @@
"callQuality": "Manage call quality",
"cameraDisabled": "Camera is not available",
"chat": "Open / Close chat",
"closeChat": "Close chat",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
"etherpad": "Open / Close shared document",
"exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view",
"feedback": "Leave feedback",
"filmstrip": "Show / Hide videos",
"fullscreen": "View / Exit full screen",
@ -644,13 +648,16 @@
"lock": "Lock / Unlock room",
"login": "Login",
"logout": "Logout",
"lowerYourHand": "Lower your hand",
"micDisabled": "Microphone is not available",
"micMutedPopup": "Your microphone has been muted so that you would fully enjoy your shared video.",
"moreActions": "More actions",
"mute": "Mute / Unmute",
"openChat": "Open chat",
"pip": "Enter Picture-in-Picture mode",
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand",
"raiseYourHand": "Raise your hand",
"Settings": "Settings",
"sharedvideo": "Share a YouTube video",
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other members.",
@ -658,6 +665,10 @@
"shortcuts": "View shortcuts",
"sip": "Call SIP number",
"speakerStats": "Speaker stats",
"startScreenSharing": "Start screen sharing",
"startSubtitles": "Start subtitles",
"stopScreenSharing": "Stop screen sharing",
"stopSubtitles": "Stop subtitles",
"stopSharedVideo": "Stop YouTube video",
"talkWhileMutedPopup": "Trying to speak? You are muted.",
"tileViewToggle": "Toggle tile view",
@ -666,7 +677,7 @@
"videomute": "Start / Stop camera"
},
"transcribing": {
"ccButtonTooltip": "Start / Stop showing subtitles",
"ccButtonTooltip": "Start / Stop subtitles",
"error": "Transcribing failed. Please try again.",
"expandedLabel": "Transcribing is currently on",
"failedToStart": "Transcribing failed to start",

View File

@ -160,8 +160,9 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
* @returns {string}
*/
_getIconName() {
return (this._isToggled() ? this.toggledIconName : this.iconName)
|| this.iconName;
return (
this._isToggled() ? this.toggledIconName : this.iconName
) || this.iconName;
}
/**

View File

@ -5,3 +5,4 @@ export { default as AbstractButton } from './AbstractButton';
export type { Props as AbstractButtonProps } from './AbstractButton';
export { default as AbstractHangupButton } from './AbstractHangupButton';
export { default as AbstractVideoMuteButton } from './AbstractVideoMuteButton';
export { default as OverflowMenuItem } from './OverflowMenuItem';

View File

@ -5,12 +5,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import { getParticipantCount } from '../../base/participants';
import { OverflowMenuItem } from '../../base/toolbox';
import { getActiveSession } from '../../recording';
import { ToolbarButton } from '../../toolbox';
import { updateDialInNumbers } from '../actions';
import { InfoDialog } from './info-dialog';
@ -59,6 +60,11 @@ type Props = {
*/
dispatch: Dispatch<*>,
/**
* Whether to show the label or not.
*/
showLabel: boolean,
/**
* Invoked to obtain translated strings.
*/
@ -122,6 +128,8 @@ class InfoDialogButton extends Component<Props, State> {
// Bind event handlers so they are only bound once for every instance.
this._onDialogClose = this._onDialogClose.bind(this);
this._onDialogToggle = this._onDialogToggle.bind(this);
this._onClickOverflowMenuButton
= this._onClickOverflowMenuButton.bind(this);
}
/**
@ -142,16 +150,28 @@ class InfoDialogButton extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { _dialIn, _liveStreamViewURL, t } = this.props;
const { _dialIn, _liveStreamViewURL, showLabel, t } = this.props;
const { showDialog } = this.state;
const iconClass = `icon-info ${showDialog ? 'toggled' : ''}`;
if (showLabel) {
return (
<OverflowMenuItem
accessibilityLabel = { t('info.accessibilityLabel') }
icon = 'icon-info'
key = 'info-button'
onClick = { this._onClickOverflowMenuButton }
text = { t('info.label') } />
);
}
return (
<div className = 'toolbox-button-wth-dialog'>
<InlineDialog
content = {
<InfoDialog
dialIn = { _dialIn }
isInlineDialog = { true }
liveStreamViewURL = { _liveStreamViewURL }
onClose = { this._onDialogClose } /> }
isOpen = { showDialog }
@ -179,6 +199,23 @@ class InfoDialogButton extends Component<Props, State> {
this.setState({ showDialog: false });
}
_onClickOverflowMenuButton: () => void;
/**
* Opens the Info dialog.
*
* @returns {void}
*/
_onClickOverflowMenuButton() {
const { _dialIn, _liveStreamViewURL } = this.props;
this.props.dispatch(openDialog(InfoDialog, {
dialIn: _dialIn,
liveStreamViewURL: _liveStreamViewURL,
isInlineDialog: false
}));
}
_onDialogToggle: () => void;
/**

View File

@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { setPassword } from '../../../base/conference';
import { getInviteURL } from '../../../base/connection';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
@ -66,6 +67,11 @@ type Props = {
*/
dispatch: Dispatch<*>,
/**
* Whether is Atlaskit InlineDialog or a normal dialog.
*/
isInlineDialog: boolean,
/**
* The current known URL for a live stream in progress.
*/
@ -187,9 +193,14 @@ class InfoDialog extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { liveStreamViewURL, onMouseOver, t } = this.props;
const {
isInlineDialog,
liveStreamViewURL,
onMouseOver,
t
} = this.props;
return (
const inlineDialog = (
<div
className = 'info-dialog'
onMouseOver = { onMouseOver } >
@ -246,6 +257,20 @@ class InfoDialog extends Component<Props, State> {
value = { this._getTextToCopy() } />
</div>
);
if (isInlineDialog) {
return inlineDialog;
}
return (
<Dialog
cancelTitleKey = 'dialog.close'
submitDisabled = { true }
titleKey = 'info.label'
width = 'small'>
{ inlineDialog }
</Dialog>
);
}
/**

View File

@ -83,6 +83,7 @@ class PasswordForm extends Component<Props, State> {
this._onEnteredPasswordChange
= this._onEnteredPasswordChange.bind(this);
this._onPasswordSubmit = this._onPasswordSubmit.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
}
/**
@ -119,6 +120,7 @@ class PasswordForm extends Component<Props, State> {
return (
<form
className = 'info-password-form'
onKeyDown = { this._onKeyDown }
onSubmit = { this._onPasswordSubmit }>
<input
autoFocus = { true }
@ -175,9 +177,26 @@ class PasswordForm extends Component<Props, State> {
*/
_onPasswordSubmit(event) {
event.preventDefault();
event.stopPropagation();
this.props.onSubmit(this.state.enteredPassword);
}
_onKeyDown: (Object) => void;
/**
* Stops the the EnterKey for propagation in order to prevent the dialog
* to close.
*
* @param {Object} event - The key event.
* @private
* @returns {void}
*/
_onKeyDown(event) {
if (event.key === 'Enter') {
event.stopPropagation();
}
}
}
export default translate(PasswordForm);

View File

@ -19,6 +19,8 @@ class ClosedCaptionButton
iconName = 'icon-closed_caption';
toggledIconName = 'icon-closed_caption toggled';
tooltip = 'transcribing.ccButtonTooltip';
label = 'toolbar.startSubtitles';
toggledLabel = 'toolbar.stopSubtitles';
}
export default translate(connect(_abstractMapStateToProps)(

View File

@ -16,6 +16,7 @@ import {
getParticipants,
participantUpdated
} from '../../../base/participants';
import { OverflowMenuItem } from '../../../base/toolbox';
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
import { ChatCounter, toggleChat } from '../../../chat';
import { toggleDocument } from '../../../etherpad';
@ -56,7 +57,6 @@ import {
import AudioMuteButton from '../AudioMuteButton';
import HangupButton from '../HangupButton';
import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuItem from './OverflowMenuItem';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import ToolbarButton from './ToolbarButton';
import VideoMuteButton from '../VideoMuteButton';
@ -177,6 +177,17 @@ type Props = {
t: Function
};
/**
* The type of the React {@code Component} state of {@link Toolbox}.
*/
type State = {
/**
* The width of the browser's window.
*/
windowWidth: number
};
declare var APP: Object;
declare var interfaceConfig: Object;
@ -185,7 +196,7 @@ declare var interfaceConfig: Object;
*
* @extends Component
*/
class Toolbox extends Component<Props> {
class Toolbox extends Component<Props, State> {
/**
* Initializes a new {@code Toolbox} instance.
*
@ -198,6 +209,7 @@ class Toolbox extends Component<Props> {
// Bind event handlers so they are only bound once per instance.
this._onMouseOut = this._onMouseOut.bind(this);
this._onMouseOver = this._onMouseOver.bind(this);
this._onResize = this._onResize.bind(this);
this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this);
this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this);
@ -232,6 +244,10 @@ class Toolbox extends Component<Props> {
= this._onToolbarToggleSharedVideo.bind(this);
this._onToolbarOpenLocalRecordingInfoDialog
= this._onToolbarOpenLocalRecordingInfoDialog.bind(this);
this.state = {
windowWidth: window.innerWidth
};
}
/**
@ -273,6 +289,8 @@ class Toolbox extends Component<Props> {
shortcut.helpDescription);
}
});
window.addEventListener('resize', this._onResize);
}
/**
@ -303,6 +321,8 @@ class Toolbox extends Component<Props> {
componentWillUnmount() {
[ 'C', 'D', 'R', 'S' ].forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter));
window.removeEventListener('resize', this._onResize);
}
/**
@ -482,6 +502,24 @@ class Toolbox extends Component<Props> {
this.props.dispatch(setToolbarHovered(true));
}
_onResize: () => void;
/**
* A window resize handler used to calculate the number of buttons we can
* fit in the toolbar.
*
* @private
* @returns {void}
*/
_onResize() {
const width = window.innerWidth;
if (this.state.windowWidth !== width) {
this.setState({ windowWidth: width });
}
}
_onSetOverflowVisible: (boolean) => void;
/**
@ -788,13 +826,30 @@ class Toolbox extends Component<Props> {
this.props.dispatch(openDialog(LocalRecordingInfoDialog));
}
/**
* Returns true if the the desktop sharing button should be visible and
* false otherwise.
*
* @returns {boolean}
*/
_isDesktopSharingButtonVisible() {
const {
_desktopSharingEnabled,
_desktopSharingDisabledTooltipKey
} = this.props;
return _desktopSharingEnabled || _desktopSharingDisabledTooltipKey;
}
/**
* Renders a button for toggleing screen sharing.
*
* @private
* @param {boolean} isInOverflowMenu - True if the button is moved to the
* overflow menu.
* @returns {ReactElement|null}
*/
_renderDesktopSharingButton() {
_renderDesktopSharingButton(isInOverflowMenu = false) {
const {
_desktopSharingEnabled,
_desktopSharingDisabledTooltipKey,
@ -802,13 +857,28 @@ class Toolbox extends Component<Props> {
t
} = this.props;
const visible
= _desktopSharingEnabled || _desktopSharingDisabledTooltipKey;
if (!visible) {
if (!this._isDesktopSharingButtonVisible()) {
return null;
}
if (isInOverflowMenu) {
return (
<OverflowMenuItem
accessibilityLabel
= { t('toolbar.accessibilityLabel.shareYourScreen') }
disabled = { _desktopSharingEnabled }
icon = { 'icon-share-desktop' }
key = 'desktop'
onClick = { this._onToolbarToggleScreenshare }
text = {
t(`toolbar.${
_screensharing
? 'stopScreenSharing' : 'startScreenSharing'}`
)
} />
);
}
const classNames = `icon-share-desktop ${
_screensharing ? 'toggled' : ''} ${
_desktopSharingEnabled ? '' : 'disabled'}`;
@ -826,6 +896,15 @@ class Toolbox extends Component<Props> {
);
}
/**
* Returns true if the profile button is visible and false otherwise.
*
* @returns {boolean}
*/
_isProfileVisible() {
return this.props._isGuest && this._shouldShowButton('profile');
}
/**
* Renders the list elements of the overflow menu.
*
@ -838,14 +917,12 @@ class Toolbox extends Component<Props> {
_etherpadInitialized,
_feedbackConfigured,
_fullScreen,
_isGuest,
_sharingVideo,
t
} = this.props;
return [
_isGuest
&& this._shouldShowButton('profile')
this._isProfileVisible()
&& <OverflowMenuProfileItem
key = 'profile'
onClick = { this._onToolbarToggleProfile } />,
@ -924,6 +1001,88 @@ class Toolbox extends Component<Props> {
];
}
/**
* Renders a list of buttons that are moved to the overflow menu.
*
* @private
* @param {Array<string>} movedButtons - The names of the buttons to be
* moved.
* @returns {Array<ReactElement>}
*/
_renderMovedButtons(movedButtons) {
const {
_chatOpen,
_raisedHand,
t
} = this.props;
return movedButtons.map(buttonName => {
switch (buttonName) {
case 'desktop':
return this._renderDesktopSharingButton(true);
case 'raisehand':
return (
<OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.raiseHand') }
icon = { 'icon-raised-hand' }
key = 'raisedHand'
onClick = { this._onToolbarToggleRaiseHand }
text = {
t(`toolbar.${
_raisedHand
? 'lowerYourHand' : 'raiseYourHand'}`
)
} />
);
case 'chat':
return (
<OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.chat') }
icon = { 'icon-chat' }
key = 'chat'
onClick = { this._onToolbarToggleChat }
text = {
t(`toolbar.${
_chatOpen ? 'closeChat' : 'openChat'}`
)
} />
);
case 'closedcaptions':
return <ClosedCaptionButton showLabel = { true } />;
case 'info':
return <InfoDialogButton showLabel = { true } />;
case 'invite':
return (
<OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.invite') }
icon = 'icon-invite'
key = 'invite'
onClick = { this._onToolbarOpenInvite }
text = { t('toolbar.invite') } />
);
case 'tileview':
return <TileViewButton showLabel = { true } />;
case 'localrecording':
return (
<OverflowMenuItem
accessibilityLabel
= { t('toolbar.accessibilityLabel.localRecording') }
icon = { 'icon-thumb-menu icon-rec' }
key = 'localrecording'
onClick = {
this._onToolbarOpenLocalRecordingInfoDialog
}
text = { t('localRecording.dialogTitle') } />
);
default:
return null;
}
});
}
/**
* Renders the toolbox content.
*
@ -941,13 +1100,86 @@ class Toolbox extends Component<Props> {
const overflowHasItems = Boolean(overflowMenuContent.filter(
child => child).length);
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const buttonsLeft = [];
const buttonsRight = [];
const maxNumberOfButtonsPerGroup = Math.floor(
(
this.state.windowWidth
- 168 // the width of the central group by design
- 48 // the minimum space between the button groups
)
/ 56 // the width + padding of a button
/ 2 // divide by the number of groups(left and right group)
);
if (this._shouldShowButton('desktop')
&& this._isDesktopSharingButtonVisible()) {
buttonsLeft.push('desktop');
}
if (this._shouldShowButton('raisehand')) {
buttonsLeft.push('raisehand');
}
if (this._shouldShowButton('chat')) {
buttonsLeft.push('chat');
}
if (this._shouldShowButton('closedcaptions')) {
buttonsLeft.push('closedcaptions');
}
if (overflowHasItems) {
buttonsRight.push('overflowmenu');
}
if (this._shouldShowButton('info')) {
buttonsRight.push('info');
}
if (this._shouldShowButton('invite') && !_hideInviteButton) {
buttonsRight.push('invite');
}
if (this._shouldShowButton('tileview')) {
buttonsRight.push('tileview');
}
if (this._shouldShowButton('localrecording')) {
buttonsRight.push('localrecording');
}
const movedButtons = [];
if (buttonsLeft.length > maxNumberOfButtonsPerGroup) {
movedButtons.push(...buttonsLeft.splice(
maxNumberOfButtonsPerGroup,
buttonsLeft.length - maxNumberOfButtonsPerGroup));
if (buttonsRight.indexOf('overflowmenu') === -1) {
buttonsRight.unshift('overflowmenu');
}
}
if (buttonsRight.length > maxNumberOfButtonsPerGroup) {
if (buttonsRight.indexOf('overflowmenu') === -1) {
buttonsRight.unshift('overflowmenu');
}
let numberOfButtons = maxNumberOfButtonsPerGroup;
// make sure the more button will be displayed when we move buttons.
if (numberOfButtons === 0) {
numberOfButtons++;
}
movedButtons.push(...buttonsRight.splice(
numberOfButtons,
buttonsRight.length - numberOfButtons));
}
overflowMenuContent.splice(
1, 0, ...this._renderMovedButtons(movedButtons));
return (
<div className = 'toolbox-content'>
<div className = 'button-group-left'>
{ this._shouldShowButton('desktop')
{ buttonsLeft.indexOf('desktop') !== -1
&& this._renderDesktopSharingButton() }
{ this._shouldShowButton('raisehand')
{ buttonsLeft.indexOf('raisehand') !== -1
&& <ToolbarButton
accessibilityLabel =
{
@ -958,7 +1190,7 @@ class Toolbox extends Component<Props> {
: 'icon-raised-hand' }
onClick = { this._onToolbarToggleRaiseHand }
tooltip = { t('toolbar.raiseHand') } /> }
{ this._shouldShowButton('chat')
{ buttonsLeft.indexOf('chat') !== -1
&& <div className = 'toolbar-button-with-badge'>
<ToolbarButton
accessibilityLabel =
@ -971,7 +1203,7 @@ class Toolbox extends Component<Props> {
<ChatCounter />
</div> }
{
this._shouldShowButton('closedcaptions')
buttonsLeft.indexOf('closedcaptions') !== -1
&& <ClosedCaptionButton />
}
</div>
@ -984,24 +1216,26 @@ class Toolbox extends Component<Props> {
visible = { this._shouldShowButton('camera') } />
</div>
<div className = 'button-group-right'>
{ this._shouldShowButton('localrecording')
{ buttonsRight.indexOf('localrecording') !== -1
&& <LocalRecordingButton
onClick = {
this._onToolbarOpenLocalRecordingInfoDialog
} />
}
{ this._shouldShowButton('tileview')
{ buttonsRight.indexOf('tileview') !== -1
&& <TileViewButton /> }
{ this._shouldShowButton('invite')
&& !_hideInviteButton
{ buttonsRight.indexOf('invite') !== -1
&& <ToolbarButton
accessibilityLabel =
{ t('toolbar.accessibilityLabel.invite') }
iconName = 'icon-invite'
onClick = { this._onToolbarOpenInvite }
tooltip = { t('toolbar.invite') } /> }
{ this._shouldShowButton('info') && <InfoDialogButton /> }
{ overflowHasItems
{
buttonsRight.indexOf('info') !== -1
&& <InfoDialogButton />
}
{ buttonsRight.indexOf('overflowmenu') !== -1
&& <OverflowMenuButton
isOpen = { _overflowMenuVisible }
onVisibilityChange = { this._onSetOverflowVisible }>

View File

@ -38,7 +38,8 @@ type Props = AbstractButtonProps & {
class TileViewButton<P: Props> extends AbstractButton<P, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.tileView';
iconName = 'icon-tiles-many';
label = 'toolbar.tileViewToggle';
label = 'toolbar.enterTileView';
toggledLabel = 'toolbar.exitTileView';
toggledIconName = 'icon-tiles-many toggled';
tooltip = 'toolbar.tileViewToggle';