From c752ea13f16839340da59812a4263cb65cd15545 Mon Sep 17 00:00:00 2001 From: Mihai-Andrei Uscat Date: Mon, 4 Jan 2021 15:30:23 +0200 Subject: [PATCH] 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. --- css/_atlaskit_overrides.scss | 16 +++ css/_drawer.scss | 124 ++++++++++++++++++ css/_variables.scss | 1 + css/main.scss | 1 + css/modals/invite/_info.scss | 6 + css/modals/invite/_invite_more.scss | 1 - .../base/popover/components/Popover.web.js | 89 ++++++++++++- react/features/filmstrip/constants.js | 5 + react/features/filmstrip/subscriber.web.js | 11 ++ .../add-people-dialog/web/DialInNumber.js | 34 ++--- .../web/RemoteVideoMenuTriggerButton.js | 19 +-- react/features/toolbox/actionTypes.js | 5 + react/features/toolbox/actions.web.js | 19 ++- .../features/toolbox/components/web/Drawer.js | 90 +++++++++++++ .../toolbox/components/web/DrawerPortal.js | 47 +++++++ .../components/web/OverflowMenuButton.js | 84 +++++++++--- .../features/toolbox/components/web/index.js | 2 + react/features/toolbox/reducer.js | 17 ++- 18 files changed, 523 insertions(+), 48 deletions(-) create mode 100644 css/_drawer.scss create mode 100644 react/features/toolbox/components/web/Drawer.js create mode 100644 react/features/toolbox/components/web/DrawerPortal.js diff --git a/css/_atlaskit_overrides.scss b/css/_atlaskit_overrides.scss index b06b8586ef..8805eb2fe0 100644 --- a/css/_atlaskit_overrides.scss +++ b/css/_atlaskit_overrides.scss @@ -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; +} diff --git a/css/_drawer.scss b/css/_drawer.scss new file mode 100644 index 0000000000..d1004c1fda --- /dev/null +++ b/css/_drawer.scss @@ -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; + } + } +} diff --git a/css/_variables.scss b/css/_variables.scss index f6b1b0ea94..b1673da181 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -121,6 +121,7 @@ $poweredByZ: 100; $ringingZ: 300; $sideToolbarContainerZ: 300; $toolbarZ: 350; +$drawerZ: 351; $tooltipsZ: 401; $dropdownMaskZ: 900; $dropdownZ: 901; diff --git a/css/main.scss b/css/main.scss index cf23468860..5dc10d112a 100644 --- a/css/main.scss +++ b/css/main.scss @@ -103,5 +103,6 @@ $flagsImagePath: "../images/"; @import 'e2ee'; @import 'responsive'; @import 'connection-status'; +@import 'drawer'; /* Modules END */ diff --git a/css/modals/invite/_info.scss b/css/modals/invite/_info.scss index 6c240a01ae..a021c5d30c 100644 --- a/css/modals/invite/_info.scss +++ b/css/modals/invite/_info.scss @@ -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; diff --git a/css/modals/invite/_invite_more.scss b/css/modals/invite/_invite_more.scss index 742f4e8981..710c078066 100644 --- a/css/modals/invite/_invite_more.scss +++ b/css/modals/invite/_invite_more.scss @@ -135,7 +135,6 @@ .dial-in-copy { display: inline-block; vertical-align: middle; - margin-left: 21px; cursor: pointer; } } diff --git a/react/features/base/popover/components/Popover.web.js b/react/features/base/popover/components/Popover.web.js index 92f075c3f6..b0158eed79 100644 --- a/react/features/base/popover/components/Popover.web.js +++ b/react/features/base/popover/components/Popover.web.js @@ -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 { 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 { // 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 { * @returns {ReactElement} */ render() { + const { children, className, content, id, overflowDrawer, position } = this.props; + + if (overflowDrawer) { + return ( +
+ { children } + + + { content } + + +
+ ); + } + return (
- { this.props.children } + position = { position }> + { children }
); @@ -160,10 +237,12 @@ class Popover extends Component { * 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 }); diff --git a/react/features/filmstrip/constants.js b/react/features/filmstrip/constants.js index 147ff410ad..d0a5b1f066 100644 --- a/react/features/filmstrip/constants.js +++ b/react/features/filmstrip/constants.js @@ -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; diff --git a/react/features/filmstrip/subscriber.web.js b/react/features/filmstrip/subscriber.web.js index eaebbd3f1e..c9dac75666 100644 --- a/react/features/filmstrip/subscriber.web.js +++ b/react/features/filmstrip/subscriber.web.js @@ -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)); + }); diff --git a/react/features/invite/components/add-people-dialog/web/DialInNumber.js b/react/features/invite/components/add-people-dialog/web/DialInNumber.js index fc470e97d4..e26e98f624 100644 --- a/react/features/invite/components/add-people-dialog/web/DialInNumber.js +++ b/react/features/invite/components/add-people-dialog/web/DialInNumber.js @@ -79,25 +79,27 @@ class DialInNumber extends Component { return (
- - - { t('info.dialInNumber') } +
+ + + { t('info.dialInNumber') } + +   + + { phoneNumber } +   - - { phoneNumber } + + + { t('info.dialInConferenceID') } + +   + + { `${_formatConferenceIDPin(conferenceID)}#` } + - -   - - - { t('info.dialInConferenceID') } - -   - - { `${_formatConferenceIDPin(conferenceID)}#` } - - +
diff --git a/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js b/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js index 53b64baec4..3d06b13e96 100644 --- a/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js +++ b/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js @@ -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 { return ( @@ -237,14 +243,7 @@ class RemoteVideoMenuTriggerButton extends Component { * @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 }; } diff --git a/react/features/toolbox/actionTypes.js b/react/features/toolbox/actionTypes.js index 36b5fec71c..e01f0ff1d5 100644 --- a/react/features/toolbox/actionTypes.js +++ b/react/features/toolbox/actionTypes.js @@ -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. * diff --git a/react/features/toolbox/actions.web.js b/react/features/toolbox/actions.web.js index 7c37b14617..d1a2b8119d 100644 --- a/react/features/toolbox/actions.web.js +++ b/react/features/toolbox/actions.web.js @@ -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 + }; +} diff --git a/react/features/toolbox/components/web/Drawer.js b/react/features/toolbox/components/web/Drawer.js new file mode 100644 index 0000000000..1cb0a5dc38 --- /dev/null +++ b/react/features/toolbox/components/web/Drawer.js @@ -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 ? ( +
+ {canExpand && ( +
+ +
+ )} + {children} +
+ ) : null + ); +} + +export default Drawer; diff --git a/react/features/toolbox/components/web/DrawerPortal.js b/react/features/toolbox/components/web/DrawerPortal.js new file mode 100644 index 0000000000..7cab2722f9 --- /dev/null +++ b/react/features/toolbox/components/web/DrawerPortal.js @@ -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; diff --git a/react/features/toolbox/components/web/OverflowMenuButton.js b/react/features/toolbox/components/web/OverflowMenuButton.js index 80fc5b5c04..a10c289944 100644 --- a/react/features/toolbox/components/web/OverflowMenuButton.js +++ b/react/features/toolbox/components/web/OverflowMenuButton.js @@ -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,27 +71,58 @@ class OverflowMenuButton extends Component { * @returns {ReactElement} */ render() { - const { children, isOpen, t } = this.props; + const { children, isOpen, overflowDrawer } = this.props; return (
- - - + { + overflowDrawer ? ( + <> + {this._renderToolbarButton()} + + + {children} + + + + ) : ( + + {this._renderToolbarButton()} + + ) + }
); } + _renderToolbarButton: () => React$Node; + + /** + * Renders the actual toolbar overflow menu button. + * + * @returns {ReactElement} + */ + _renderToolbarButton() { + const { isOpen, t } = this.props; + + return ( + + ); + } + _onCloseDialog: () => void; /** @@ -113,4 +152,19 @@ class OverflowMenuButton extends Component { } } -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)); diff --git a/react/features/toolbox/components/web/index.js b/react/features/toolbox/components/web/index.js index 2bec9f22b5..a5fa7be74d 100644 --- a/react/features/toolbox/components/web/index.js +++ b/react/features/toolbox/components/web/index.js @@ -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'; diff --git a/react/features/toolbox/reducer.js b/react/features/toolbox/reducer.js index e84f9406ad..a7740554df 100644 --- a/react/features/toolbox/reducer.js +++ b/react/features/toolbox/reducer.js @@ -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,