feat(overflow): Add responsive drawer at small screen width.
* Implement opening toolbar and participant overflows as drawers when below certain width. * Fix dial-in copy button displaying incorrectly.
This commit is contained in:
parent
5ef60c3a7d
commit
c752ea13f1
|
@ -48,3 +48,19 @@
|
||||||
.toolbox-button-wth-dialog .eYJELv {
|
.toolbox-button-wth-dialog .eYJELv {
|
||||||
max-height: initial;
|
max-height: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override @atlaskit/InlineDialog styling for the overflowmenu so it displays
|
||||||
|
* a scrollable list of elements at small screen widths.
|
||||||
|
*/
|
||||||
|
.sc-eNQAEJ {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep overflow menu within screen vertical bounds and make it scrollable.
|
||||||
|
*/
|
||||||
|
.toolbox-button-wth-dialog .sc-ckVGcZ.fdAqDG > :first-child {
|
||||||
|
max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 16px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
.drawer-portal {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: $drawerZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-menu {
|
||||||
|
padding: 12px 16px;
|
||||||
|
max-height: 50vh;
|
||||||
|
background: #242528;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
max-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-toggle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 44px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $overflowMenuItemHoverBG;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg, path {
|
||||||
|
fill: #b8c7e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupmenu {
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupmenu__item {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&#{&} .overflow-menu {
|
||||||
|
margin: auto;
|
||||||
|
font-size: 1.2em;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.overflow-menu-item {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 48px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
color: $overflowMenuItemColor;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $overflowMenuItemHoverBG;
|
||||||
|
color: $overflowMenuItemHoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unclickable {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
&.unclickable:hover {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
cursor: initial;
|
||||||
|
color: #3b475c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-tag {
|
||||||
|
background: $overflowMenuItemColor;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: $overflowMenuBG;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-menu-item-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: inline;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i:hover {
|
||||||
|
background-color: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: #B8C7E0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-text {
|
||||||
|
max-width: 150px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -121,6 +121,7 @@ $poweredByZ: 100;
|
||||||
$ringingZ: 300;
|
$ringingZ: 300;
|
||||||
$sideToolbarContainerZ: 300;
|
$sideToolbarContainerZ: 300;
|
||||||
$toolbarZ: 350;
|
$toolbarZ: 350;
|
||||||
|
$drawerZ: 351;
|
||||||
$tooltipsZ: 401;
|
$tooltipsZ: 401;
|
||||||
$dropdownMaskZ: 900;
|
$dropdownMaskZ: 900;
|
||||||
$dropdownZ: 901;
|
$dropdownZ: 901;
|
||||||
|
|
|
@ -103,5 +103,6 @@ $flagsImagePath: "../images/";
|
||||||
@import 'e2ee';
|
@import 'e2ee';
|
||||||
@import 'responsive';
|
@import 'responsive';
|
||||||
@import 'connection-status';
|
@import 'connection-status';
|
||||||
|
@import 'drawer';
|
||||||
|
|
||||||
/* Modules END */
|
/* Modules END */
|
||||||
|
|
|
@ -50,6 +50,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dial-in-number {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.dial-in-numbers-list {
|
.dial-in-numbers-list {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
@ -135,7 +135,6 @@
|
||||||
.dial-in-copy {
|
.dial-in-copy {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: 21px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import InlineDialog from '@atlaskit/inline-dialog';
|
import InlineDialog from '@atlaskit/inline-dialog';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A map of dialog positions, relative to trigger, to css classes used to
|
* A map of dialog positions, relative to trigger, to css classes used to
|
||||||
* manipulate elements for handling mouse events.
|
* manipulate elements for handling mouse events.
|
||||||
|
@ -66,6 +68,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
onPopoverOpen: Function,
|
onPopoverOpen: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to display the Popover as a drawer.
|
||||||
|
*/
|
||||||
|
overflowDrawer: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From which side of the dialog trigger the dialog should display. The
|
* From which side of the dialog trigger the dialog should display. The
|
||||||
* value will be passed to {@code InlineDialog}.
|
* value will be passed to {@code InlineDialog}.
|
||||||
|
@ -101,6 +108,11 @@ class Popover extends Component<Props, State> {
|
||||||
id: ''
|
id: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the Popover that is meant to open as a drawer.
|
||||||
|
*/
|
||||||
|
_drawerContainerRef: Object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new {@code Popover} instance.
|
* Initializes a new {@code Popover} instance.
|
||||||
*
|
*
|
||||||
|
@ -117,6 +129,51 @@ class Popover extends Component<Props, State> {
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this._onHideDialog = this._onHideDialog.bind(this);
|
this._onHideDialog = this._onHideDialog.bind(this);
|
||||||
this._onShowDialog = this._onShowDialog.bind(this);
|
this._onShowDialog = this._onShowDialog.bind(this);
|
||||||
|
this._drawerContainerRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up an event listener to open a drawer when clicking, rather than entering the
|
||||||
|
* overflow area.
|
||||||
|
*
|
||||||
|
* TODO: This should be done by setting an {@code onClick} handler on the div, but for some
|
||||||
|
* reason that doesn't seem to work whatsoever.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
if (this._drawerContainerRef && this._drawerContainerRef.current) {
|
||||||
|
this._drawerContainerRef.current.addEventListener('click', this._onShowDialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the listener set up in the {@code componentDidMount} method.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._drawerContainerRef && this._drawerContainerRef.current) {
|
||||||
|
this._drawerContainerRef.current.removeEventListener('click', this._onShowDialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React Component's componentDidUpdate.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (prevProps.overflowDrawer !== this.props.overflowDrawer) {
|
||||||
|
// Make sure the listeners are set up when resizing the screen past the drawer threshold.
|
||||||
|
if (this.props.overflowDrawer) {
|
||||||
|
this.componentDidMount();
|
||||||
|
} else {
|
||||||
|
this.componentWillUnmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,17 +183,37 @@ class Popover extends Component<Props, State> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
|
const { children, className, content, id, overflowDrawer, position } = this.props;
|
||||||
|
|
||||||
|
if (overflowDrawer) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className = { className }
|
||||||
|
id = { id }
|
||||||
|
ref = { this._drawerContainerRef }>
|
||||||
|
{ children }
|
||||||
|
<DrawerPortal>
|
||||||
|
<Drawer
|
||||||
|
isOpen = { this.state.showDialog }
|
||||||
|
onClose = { this._onHideDialog }>
|
||||||
|
{ content }
|
||||||
|
</Drawer>
|
||||||
|
</DrawerPortal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className = { this.props.className }
|
className = { className }
|
||||||
id = { this.props.id }
|
id = { id }
|
||||||
onMouseEnter = { this._onShowDialog }
|
onMouseEnter = { this._onShowDialog }
|
||||||
onMouseLeave = { this._onHideDialog }>
|
onMouseLeave = { this._onHideDialog }>
|
||||||
<InlineDialog
|
<InlineDialog
|
||||||
content = { this._renderContent() }
|
content = { this._renderContent() }
|
||||||
isOpen = { this.state.showDialog }
|
isOpen = { this.state.showDialog }
|
||||||
position = { this.props.position }>
|
position = { position }>
|
||||||
{ this.props.children }
|
{ children }
|
||||||
</InlineDialog>
|
</InlineDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -160,10 +237,12 @@ class Popover extends Component<Props, State> {
|
||||||
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen
|
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen
|
||||||
* callbacks.
|
* callbacks.
|
||||||
*
|
*
|
||||||
|
* @param {MouseEvent} event - The mouse event to intercept.
|
||||||
* @private
|
* @private
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onShowDialog() {
|
_onShowDialog(event) {
|
||||||
|
event.stopPropagation();
|
||||||
if (!this.props.disablePopover) {
|
if (!this.props.disablePopover) {
|
||||||
this.setState({ showDialog: true });
|
this.setState({ showDialog: true });
|
||||||
|
|
||||||
|
|
|
@ -9,3 +9,8 @@ export const FILMSTRIP_SIZE = 90;
|
||||||
* The aspect ratio of a tile in tile view.
|
* The aspect ratio of a tile in tile view.
|
||||||
*/
|
*/
|
||||||
export const TILE_ASPECT_RATIO = 16 / 9;
|
export const TILE_ASPECT_RATIO = 16 / 9;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Width below which the overflow menu(s) will be displayed as drawer(s).
|
||||||
|
*/
|
||||||
|
export const DISPLAY_DRAWER_THRESHOLD = 512;
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
||||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||||
import { StateListenerRegistry, equals } from '../base/redux';
|
import { StateListenerRegistry, equals } from '../base/redux';
|
||||||
|
import { setOverflowDrawer } from '../toolbox/actions.web';
|
||||||
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
||||||
|
|
||||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
||||||
|
import { DISPLAY_DRAWER_THRESHOLD } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||||
|
@ -123,3 +125,12 @@ StateListenerRegistry.register(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for changes in the client width to determine whether the overflow menu(s) should be displayed as drawers.
|
||||||
|
*/
|
||||||
|
StateListenerRegistry.register(
|
||||||
|
/* selector */ state => state['features/base/responsive-ui'].clientWidth < DISPLAY_DRAWER_THRESHOLD,
|
||||||
|
/* listener */ (widthBelowThreshold, store) => {
|
||||||
|
store.dispatch(setOverflowDrawer(widthBelowThreshold));
|
||||||
|
});
|
||||||
|
|
|
@ -79,25 +79,27 @@ class DialInNumber extends Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'dial-in-number'>
|
<div className = 'dial-in-number'>
|
||||||
<span className = 'phone-number'>
|
<div>
|
||||||
<span className = 'info-label'>
|
<span className = 'phone-number'>
|
||||||
{ t('info.dialInNumber') }
|
<span className = 'info-label'>
|
||||||
|
{ t('info.dialInNumber') }
|
||||||
|
</span>
|
||||||
|
<span className = 'spacer'> </span>
|
||||||
|
<span className = 'info-value'>
|
||||||
|
{ phoneNumber }
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className = 'spacer'> </span>
|
<span className = 'spacer'> </span>
|
||||||
<span className = 'info-value'>
|
<span className = 'conference-id'>
|
||||||
{ phoneNumber }
|
<span className = 'info-label'>
|
||||||
|
{ t('info.dialInConferenceID') }
|
||||||
|
</span>
|
||||||
|
<span className = 'spacer'> </span>
|
||||||
|
<span className = 'info-value'>
|
||||||
|
{ `${_formatConferenceIDPin(conferenceID)}#` }
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
<span className = 'spacer'> </span>
|
|
||||||
<span className = 'conference-id'>
|
|
||||||
<span className = 'info-label'>
|
|
||||||
{ t('info.dialInConferenceID') }
|
|
||||||
</span>
|
|
||||||
<span className = 'spacer'> </span>
|
|
||||||
<span className = 'info-value'>
|
|
||||||
{ `${_formatConferenceIDPin(conferenceID)}#` }
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<a
|
<a
|
||||||
className = 'dial-in-copy'
|
className = 'dial-in-copy'
|
||||||
onClick = { this._onCopyText }>
|
onClick = { this._onCopyText }>
|
||||||
|
|
|
@ -60,6 +60,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_menuPosition: string,
|
_menuPosition: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to display the Popover as a drawer.
|
||||||
|
*/
|
||||||
|
_overflowDrawer: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current state of the participant's remote control session.
|
* The current state of the participant's remote control session.
|
||||||
*/
|
*/
|
||||||
|
@ -122,6 +127,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content = { content }
|
content = { content }
|
||||||
|
overflowDrawer = { this.props._overflowDrawer }
|
||||||
position = { this.props._menuPosition }>
|
position = { this.props._menuPosition }>
|
||||||
<span
|
<span
|
||||||
className = 'popover-trigger remote-video-menu-trigger'>
|
className = 'popover-trigger remote-video-menu-trigger'>
|
||||||
|
@ -237,14 +243,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||||
* @param {Object} state - The Redux state.
|
* @param {Object} state - The Redux state.
|
||||||
* @param {Object} ownProps - The own props of the component.
|
* @param {Object} ownProps - The own props of the component.
|
||||||
* @private
|
* @private
|
||||||
* @returns {{
|
* @returns {Props}
|
||||||
* _isAudioMuted: boolean,
|
|
||||||
* _isModerator: boolean,
|
|
||||||
* _disableKick: boolean,
|
|
||||||
* _disableRemoteMute: boolean,
|
|
||||||
* _menuPosition: string,
|
|
||||||
* _remoteControlState: number
|
|
||||||
* }}
|
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state, ownProps) {
|
function _mapStateToProps(state, ownProps) {
|
||||||
const { participantID } = ownProps;
|
const { participantID } = ownProps;
|
||||||
|
@ -259,6 +258,7 @@ function _mapStateToProps(state, ownProps) {
|
||||||
const { active, controller } = state['features/remote-control'];
|
const { active, controller } = state['features/remote-control'];
|
||||||
const { requestedParticipant, controlled } = controller;
|
const { requestedParticipant, controlled } = controller;
|
||||||
const activeParticipant = requestedParticipant || controlled;
|
const activeParticipant = requestedParticipant || controlled;
|
||||||
|
const { overflowDrawer } = state['features/toolbox'];
|
||||||
|
|
||||||
if (_supportsRemoteControl
|
if (_supportsRemoteControl
|
||||||
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
|
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
|
||||||
|
@ -291,7 +291,8 @@ function _mapStateToProps(state, ownProps) {
|
||||||
_disableKick: Boolean(disableKick),
|
_disableKick: Boolean(disableKick),
|
||||||
_disableRemoteMute: Boolean(disableRemoteMute),
|
_disableRemoteMute: Boolean(disableRemoteMute),
|
||||||
_remoteControlState,
|
_remoteControlState,
|
||||||
_menuPosition
|
_menuPosition,
|
||||||
|
_overflowDrawer: overflowDrawer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,11 @@ export const FULL_SCREEN_CHANGED = 'FULL_SCREEN_CHANGED';
|
||||||
*/
|
*/
|
||||||
export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
|
export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers.
|
||||||
|
*/
|
||||||
|
export const SET_OVERFLOW_DRAWER = 'SET_OVERFLOW_DRAWER';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the (redux) action which shows/hides the OverflowMenu.
|
* The type of the (redux) action which shows/hides the OverflowMenu.
|
||||||
*
|
*
|
||||||
|
|
|
@ -4,7 +4,8 @@ import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FULL_SCREEN_CHANGED,
|
FULL_SCREEN_CHANGED,
|
||||||
SET_FULL_SCREEN
|
SET_FULL_SCREEN,
|
||||||
|
SET_OVERFLOW_DRAWER
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import {
|
import {
|
||||||
clearToolboxTimeout,
|
clearToolboxTimeout,
|
||||||
|
@ -143,3 +144,19 @@ export function showToolbox(timeout: number = 0): Object {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals a request to display overflow as drawer.
|
||||||
|
*
|
||||||
|
* @param {boolean} displayAsDrawer - True to display overflow as drawer, false to preserve original behaviour.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_OVERFLOW_DRAWER,
|
||||||
|
* displayAsDrawer: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setOverflowDrawer(displayAsDrawer: boolean) {
|
||||||
|
return {
|
||||||
|
type: SET_OVERFLOW_DRAWER,
|
||||||
|
displayAsDrawer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { Icon, IconArrowUp, IconArrowDown } from '../../../base/icons';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the drawer should have a button that expands its size or not.
|
||||||
|
*/
|
||||||
|
canExpand: ?boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component(s) to be displayed within the drawer menu.
|
||||||
|
*/
|
||||||
|
children: React$Node,
|
||||||
|
|
||||||
|
/**
|
||||||
|
Whether the drawer should be shown or not.
|
||||||
|
*/
|
||||||
|
isOpen: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
Function that hides the drawer.
|
||||||
|
*/
|
||||||
|
onClose: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays the mobile friendly drawer on web.
|
||||||
|
*
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
function Drawer({
|
||||||
|
canExpand,
|
||||||
|
children,
|
||||||
|
isOpen,
|
||||||
|
onClose }: Props) {
|
||||||
|
const [ expanded, setExpanded ] = useState(false);
|
||||||
|
const drawerRef: Object = useRef(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the drawer when clicking outside of it.
|
||||||
|
*
|
||||||
|
* @param {Event} event - Mouse down event object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function handleOutsideClick(event: MouseEvent) {
|
||||||
|
if (drawerRef.current && !drawerRef.current.contains(event.target)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('mousedown', handleOutsideClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousedown', handleOutsideClick);
|
||||||
|
};
|
||||||
|
}, [ drawerRef ]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the menu state between expanded/collapsed.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function toggleExpanded() {
|
||||||
|
setExpanded(!expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
isOpen ? (
|
||||||
|
<div
|
||||||
|
className = { `drawer-menu${expanded ? ' expanded' : ''}` }
|
||||||
|
ref = { drawerRef }>
|
||||||
|
{canExpand && (
|
||||||
|
<div
|
||||||
|
className = 'drawer-toggle'
|
||||||
|
onClick = { toggleExpanded }>
|
||||||
|
<Icon src = { expanded ? IconArrowDown : IconArrowUp } />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Drawer;
|
|
@ -0,0 +1,47 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component(s) to be displayed within the drawer portal.
|
||||||
|
*/
|
||||||
|
children: React$Node
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component meant to render a drawer at the bottom of the screen,
|
||||||
|
* by creating a portal containing the component's children.
|
||||||
|
*
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
function DrawerPortal({ children }: Props) {
|
||||||
|
const [ portalTarget ] = useState(() => {
|
||||||
|
const portalDiv = document.createElement('div');
|
||||||
|
|
||||||
|
portalDiv.className = 'drawer-portal';
|
||||||
|
|
||||||
|
return portalDiv;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (document.body) {
|
||||||
|
document.body.appendChild(portalTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (document.body) {
|
||||||
|
document.body.removeChild(portalTarget);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
children,
|
||||||
|
portalTarget
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DrawerPortal;
|
|
@ -6,7 +6,10 @@ import React, { Component } from 'react';
|
||||||
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
|
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { IconMenuThumb } from '../../../base/icons';
|
import { IconMenuThumb } from '../../../base/icons';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
|
||||||
|
import Drawer from './Drawer';
|
||||||
|
import DrawerPortal from './DrawerPortal';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +32,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
onVisibilityChange: Function,
|
onVisibilityChange: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to display the OverflowMenu as a drawer.
|
||||||
|
*/
|
||||||
|
overflowDrawer: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked to obtain translated strings.
|
* Invoked to obtain translated strings.
|
||||||
*/
|
*/
|
||||||
|
@ -63,27 +71,58 @@ class OverflowMenuButton extends Component<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { children, isOpen, t } = this.props;
|
const { children, isOpen, overflowDrawer } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'toolbox-button-wth-dialog'>
|
<div className = 'toolbox-button-wth-dialog'>
|
||||||
<InlineDialog
|
{
|
||||||
content = { children }
|
overflowDrawer ? (
|
||||||
isOpen = { isOpen }
|
<>
|
||||||
onClose = { this._onCloseDialog }
|
{this._renderToolbarButton()}
|
||||||
position = { 'top right' }>
|
<DrawerPortal>
|
||||||
<ToolbarButton
|
<Drawer
|
||||||
accessibilityLabel =
|
canExpand = { true }
|
||||||
{ t('toolbar.accessibilityLabel.moreActions') }
|
isOpen = { isOpen }
|
||||||
icon = { IconMenuThumb }
|
onClose = { this._onCloseDialog }>
|
||||||
onClick = { this._onToggleDialogVisibility }
|
{children}
|
||||||
toggled = { isOpen }
|
</Drawer>
|
||||||
tooltip = { t('toolbar.moreActions') } />
|
</DrawerPortal>
|
||||||
</InlineDialog>
|
</>
|
||||||
|
) : (
|
||||||
|
<InlineDialog
|
||||||
|
content = { children }
|
||||||
|
isOpen = { isOpen }
|
||||||
|
onClose = { this._onCloseDialog }
|
||||||
|
position = { 'top right' }>
|
||||||
|
{this._renderToolbarButton()}
|
||||||
|
</InlineDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderToolbarButton: () => React$Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the actual toolbar overflow menu button.
|
||||||
|
*
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
_renderToolbarButton() {
|
||||||
|
const { isOpen, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
accessibilityLabel =
|
||||||
|
{ t('toolbar.accessibilityLabel.moreActions') }
|
||||||
|
icon = { IconMenuThumb }
|
||||||
|
onClick = { this._onToggleDialogVisibility }
|
||||||
|
toggled = { isOpen }
|
||||||
|
tooltip = { t('toolbar.moreActions') } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_onCloseDialog: () => void;
|
_onCloseDialog: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,4 +152,19 @@ class OverflowMenuButton extends Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(OverflowMenuButton);
|
/**
|
||||||
|
* Maps (parts of) the Redux state to the associated props for the
|
||||||
|
* {@code OverflowMenuButton} component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const { overflowDrawer } = state['features/toolbox'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
overflowDrawer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(mapStateToProps)(OverflowMenuButton));
|
||||||
|
|
|
@ -2,3 +2,5 @@ export { default as AudioSettingsButton } from './AudioSettingsButton';
|
||||||
export { default as VideoSettingsButton } from './VideoSettingsButton';
|
export { default as VideoSettingsButton } from './VideoSettingsButton';
|
||||||
export { default as ToolbarButton } from './ToolbarButton';
|
export { default as ToolbarButton } from './ToolbarButton';
|
||||||
export { default as Toolbox } from './Toolbox';
|
export { default as Toolbox } from './Toolbox';
|
||||||
|
export { default as Drawer } from './Drawer';
|
||||||
|
export { default as DrawerPortal } from './DrawerPortal';
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ReducerRegistry, set } from '../base/redux';
|
||||||
import {
|
import {
|
||||||
CLEAR_TOOLBOX_TIMEOUT,
|
CLEAR_TOOLBOX_TIMEOUT,
|
||||||
FULL_SCREEN_CHANGED,
|
FULL_SCREEN_CHANGED,
|
||||||
|
SET_OVERFLOW_DRAWER,
|
||||||
SET_OVERFLOW_MENU_VISIBLE,
|
SET_OVERFLOW_MENU_VISIBLE,
|
||||||
SET_TOOLBAR_HOVERED,
|
SET_TOOLBAR_HOVERED,
|
||||||
SET_TOOLBOX_ALWAYS_VISIBLE,
|
SET_TOOLBOX_ALWAYS_VISIBLE,
|
||||||
|
@ -25,6 +26,7 @@ declare var interfaceConfig: Object;
|
||||||
* alwaysVisible: boolean,
|
* alwaysVisible: boolean,
|
||||||
* enabled: boolean,
|
* enabled: boolean,
|
||||||
* hovered: boolean,
|
* hovered: boolean,
|
||||||
|
* overflowDrawer: boolean,
|
||||||
* overflowMenuVisible: boolean,
|
* overflowMenuVisible: boolean,
|
||||||
* timeoutID: number,
|
* timeoutID: number,
|
||||||
* timeoutMS: number,
|
* timeoutMS: number,
|
||||||
|
@ -79,6 +81,13 @@ function _getInitialState() {
|
||||||
*/
|
*/
|
||||||
hovered: false,
|
hovered: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The indicator which determines whether the overflow menu(s) are to be displayed as drawers.
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
overflowDrawer: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The indicator which determines whether the OverflowMenu is visible.
|
* The indicator which determines whether the OverflowMenu is visible.
|
||||||
*
|
*
|
||||||
|
@ -103,7 +112,7 @@ function _getInitialState() {
|
||||||
timeoutMS,
|
timeoutMS,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The indicator which determines whether the Toolbox is visible.
|
* The indicator that determines whether the Toolbox is visible.
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
|
@ -127,6 +136,12 @@ ReducerRegistry.register(
|
||||||
fullScreen: action.fullScreen
|
fullScreen: action.fullScreen
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case SET_OVERFLOW_DRAWER:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
overflowDrawer: action.displayAsDrawer
|
||||||
|
};
|
||||||
|
|
||||||
case SET_OVERFLOW_MENU_VISIBLE:
|
case SET_OVERFLOW_MENU_VISIBLE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
Loading…
Reference in New Issue