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:
Mihai-Andrei Uscat 2021-01-04 15:30:23 +02:00 committed by Saúl Ibarra Corretgé
parent 5ef60c3a7d
commit c752ea13f1
18 changed files with 523 additions and 48 deletions

View File

@ -48,3 +48,19 @@
.toolbox-button-wth-dialog .eYJELv {
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;
}

124
css/_drawer.scss Normal file
View File

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

View File

@ -121,6 +121,7 @@ $poweredByZ: 100;
$ringingZ: 300;
$sideToolbarContainerZ: 300;
$toolbarZ: 350;
$drawerZ: 351;
$tooltipsZ: 401;
$dropdownMaskZ: 900;
$dropdownZ: 901;

View File

@ -103,5 +103,6 @@ $flagsImagePath: "../images/";
@import 'e2ee';
@import 'responsive';
@import 'connection-status';
@import 'drawer';
/* Modules END */

View File

@ -50,6 +50,12 @@
}
}
.dial-in-number {
display: flex;
justify-content: space-between;
padding-right: 8px;
}
.dial-in-numbers-list {
margin-top: 20px;
font-size: 12px;

View File

@ -135,7 +135,6 @@
.dial-in-copy {
display: inline-block;
vertical-align: middle;
margin-left: 21px;
cursor: pointer;
}
}

View File

@ -3,6 +3,8 @@
import InlineDialog from '@atlaskit/inline-dialog';
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
* manipulate elements for handling mouse events.
@ -66,6 +68,11 @@ type Props = {
*/
onPopoverOpen: Function,
/**
* Whether to display the Popover as a drawer.
*/
overflowDrawer: boolean,
/**
* From which side of the dialog trigger the dialog should display. The
* value will be passed to {@code InlineDialog}.
@ -101,6 +108,11 @@ class Popover extends Component<Props, State> {
id: ''
};
/**
* Reference to the Popover that is meant to open as a drawer.
*/
_drawerContainerRef: Object;
/**
* 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.
this._onHideDialog = this._onHideDialog.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}
*/
render() {
const { children, className, content, id, overflowDrawer, position } = this.props;
if (overflowDrawer) {
return (
<div
className = { this.props.className }
id = { this.props.id }
className = { className }
id = { id }
ref = { this._drawerContainerRef }>
{ children }
<DrawerPortal>
<Drawer
isOpen = { this.state.showDialog }
onClose = { this._onHideDialog }>
{ content }
</Drawer>
</DrawerPortal>
</div>
);
}
return (
<div
className = { className }
id = { id }
onMouseEnter = { this._onShowDialog }
onMouseLeave = { this._onHideDialog }>
<InlineDialog
content = { this._renderContent() }
isOpen = { this.state.showDialog }
position = { this.props.position }>
{ this.props.children }
position = { position }>
{ children }
</InlineDialog>
</div>
);
@ -160,10 +237,12 @@ class Popover extends Component<Props, State> {
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen
* callbacks.
*
* @param {MouseEvent} event - The mouse event to intercept.
* @private
* @returns {void}
*/
_onShowDialog() {
_onShowDialog(event) {
event.stopPropagation();
if (!this.props.disablePopover) {
this.setState({ showDialog: true });

View File

@ -9,3 +9,8 @@ export const FILMSTRIP_SIZE = 90;
* The aspect ratio of a tile in tile view.
*/
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;

View File

@ -3,9 +3,11 @@
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { StateListenerRegistry, equals } from '../base/redux';
import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
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.
@ -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));
});

View File

@ -79,6 +79,7 @@ class DialInNumber extends Component<Props> {
return (
<div className = 'dial-in-number'>
<div>
<span className = 'phone-number'>
<span className = 'info-label'>
{ t('info.dialInNumber') }
@ -98,6 +99,7 @@ class DialInNumber extends Component<Props> {
{ `${_formatConferenceIDPin(conferenceID)}#` }
</span>
</span>
</div>
<a
className = 'dial-in-copy'
onClick = { this._onCopyText }>

View File

@ -60,6 +60,11 @@ type Props = {
*/
_menuPosition: string,
/**
* Whether to display the Popover as a drawer.
*/
_overflowDrawer: boolean,
/**
* The current state of the participant's remote control session.
*/
@ -122,6 +127,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
return (
<Popover
content = { content }
overflowDrawer = { this.props._overflowDrawer }
position = { this.props._menuPosition }>
<span
className = 'popover-trigger remote-video-menu-trigger'>
@ -237,14 +243,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {{
* _isAudioMuted: boolean,
* _isModerator: boolean,
* _disableKick: boolean,
* _disableRemoteMute: boolean,
* _menuPosition: string,
* _remoteControlState: number
* }}
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
@ -259,6 +258,7 @@ function _mapStateToProps(state, ownProps) {
const { active, controller } = state['features/remote-control'];
const { requestedParticipant, controlled } = controller;
const activeParticipant = requestedParticipant || controlled;
const { overflowDrawer } = state['features/toolbox'];
if (_supportsRemoteControl
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
@ -291,7 +291,8 @@ function _mapStateToProps(state, ownProps) {
_disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute),
_remoteControlState,
_menuPosition
_menuPosition,
_overflowDrawer: overflowDrawer
};
}

View File

@ -29,6 +29,11 @@ export const FULL_SCREEN_CHANGED = 'FULL_SCREEN_CHANGED';
*/
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.
*

View File

@ -4,7 +4,8 @@ import type { Dispatch } from 'redux';
import {
FULL_SCREEN_CHANGED,
SET_FULL_SCREEN
SET_FULL_SCREEN,
SET_OVERFLOW_DRAWER
} from './actionTypes';
import {
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
};
}

View File

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

View File

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

View File

@ -6,7 +6,10 @@ import React, { Component } from 'react';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { translate } from '../../../base/i18n';
import { IconMenuThumb } from '../../../base/icons';
import { connect } from '../../../base/redux';
import Drawer from './Drawer';
import DrawerPortal from './DrawerPortal';
import ToolbarButton from './ToolbarButton';
/**
@ -29,6 +32,11 @@ type Props = {
*/
onVisibilityChange: Function,
/**
* Whether to display the OverflowMenu as a drawer.
*/
overflowDrawer: boolean,
/**
* Invoked to obtain translated strings.
*/
@ -63,15 +71,48 @@ class OverflowMenuButton extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { children, isOpen, t } = this.props;
const { children, isOpen, overflowDrawer } = this.props;
return (
<div className = 'toolbox-button-wth-dialog'>
{
overflowDrawer ? (
<>
{this._renderToolbarButton()}
<DrawerPortal>
<Drawer
canExpand = { true }
isOpen = { isOpen }
onClose = { this._onCloseDialog }>
{children}
</Drawer>
</DrawerPortal>
</>
) : (
<InlineDialog
content = { children }
isOpen = { isOpen }
onClose = { this._onCloseDialog }
position = { 'top right' }>
{this._renderToolbarButton()}
</InlineDialog>
)
}
</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') }
@ -79,8 +120,6 @@ class OverflowMenuButton extends Component<Props> {
onClick = { this._onToggleDialogVisibility }
toggled = { isOpen }
tooltip = { t('toolbar.moreActions') } />
</InlineDialog>
</div>
);
}
@ -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));

View File

@ -2,3 +2,5 @@ export { default as AudioSettingsButton } from './AudioSettingsButton';
export { default as VideoSettingsButton } from './VideoSettingsButton';
export { default as ToolbarButton } from './ToolbarButton';
export { default as Toolbox } from './Toolbox';
export { default as Drawer } from './Drawer';
export { default as DrawerPortal } from './DrawerPortal';

View File

@ -5,6 +5,7 @@ import { ReducerRegistry, set } from '../base/redux';
import {
CLEAR_TOOLBOX_TIMEOUT,
FULL_SCREEN_CHANGED,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_ALWAYS_VISIBLE,
@ -25,6 +26,7 @@ declare var interfaceConfig: Object;
* alwaysVisible: boolean,
* enabled: boolean,
* hovered: boolean,
* overflowDrawer: boolean,
* overflowMenuVisible: boolean,
* timeoutID: number,
* timeoutMS: number,
@ -79,6 +81,13 @@ function _getInitialState() {
*/
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.
*
@ -103,7 +112,7 @@ function _getInitialState() {
timeoutMS,
/**
* The indicator which determines whether the Toolbox is visible.
* The indicator that determines whether the Toolbox is visible.
*
* @type {boolean}
*/
@ -127,6 +136,12 @@ ReducerRegistry.register(
fullScreen: action.fullScreen
};
case SET_OVERFLOW_DRAWER:
return {
...state,
overflowDrawer: action.displayAsDrawer
};
case SET_OVERFLOW_MENU_VISIBLE:
return {
...state,