feat(dynamic-branding): Add custom theme branding (#10335)

- apply theming to various buttons
This commit is contained in:
Horatiu Muresan 2021-11-15 10:37:54 +02:00 committed by GitHub
parent 61025edb74
commit 8ebca64af1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 848 additions and 271 deletions

View File

@ -896,25 +896,55 @@ var config = {
If there is no url set or there are missing fields, the defaults are applied.
The config file should be in JSON.
None of the fields are mandatory and the response must have the shape:
{
// The domain url to apply (will replace the domain in the sharing conference link/embed section)
inviteDomain: 'example-company.org,
// The hex value for the colour used as background
backgroundColor: '#fff',
// The url for the image used as background
backgroundImageUrl: 'https://example.com/background-img.png',
// The anchor url used when clicking the logo image
logoClickUrl: 'https://example-company.org',
// The url used for the image used as logo
logoImageUrl: 'https://example.com/logo-img.png',
// Overwrite for pool of background images for avatars
avatarBackgrounds: ['url(https://example.com/avatar-background-1.png)', '#FFF'],
// The lobby/prejoin screen background
premeetingBackground: 'url(https://example.com/premeeting-background.png)',
// A list of images that can be used as video backgrounds.
// When this field is present, the default images will be replaced with those provided.
virtualBackgrounds: ['https://example.com/img.jpg']
}
{
// The domain url to apply (will replace the domain in the sharing conference link/embed section)
inviteDomain: 'example-company.org,
// The hex value for the colour used as background
backgroundColor: '#fff',
// The url for the image used as background
backgroundImageUrl: 'https://example.com/background-img.png',
// The anchor url used when clicking the logo image
logoClickUrl: 'https://example-company.org',
// The url used for the image used as logo
logoImageUrl: 'https://example.com/logo-img.png',
// Overwrite for pool of background images for avatars
avatarBackgrounds: ['url(https://example.com/avatar-background-1.png)', '#FFF'],
// The lobby/prejoin screen background
premeetingBackground: 'url(https://example.com/premeeting-background.png)',
// A list of images that can be used as video backgrounds.
// When this field is present, the default images will be replaced with those provided.
virtualBackgrounds: ['https://example.com/img.jpg'],
// Object containing a theme's properties. It also supports partial overwrites of the main theme.
// For a list of all possible theme tokens and their current defaults, please check:
// https://github.com/jitsi/jitsi-meet/tree/master/resources/custom-theme/custom-theme.json
// For a short explanations on each of the tokens, please check:
// https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/ui/Tokens.js
// IMPORTANT!: This is work in progress so many of the various tokens are not yet applied in code
// or they are partially applied.
customTheme: {
palette: {
ui01: "orange !important",
ui02: "maroon",
surface02: 'darkgreen',
ui03: "violet",
ui04: "magenta",
ui05: "blueviolet",
field02Hover: 'red',
action01: 'green',
action01Hover: 'lightgreen',
action02Disabled: 'beige',
success02: 'cadetblue',
action02Hover: 'aliceblue'
},
typography: {
labelRegular: {
fontSize: 25,
lineHeight: 30,
fontWeight: 500
}
}
}
}
*/
// dynamicBrandingUrl: '',

View File

@ -77,14 +77,6 @@
}
}
.toolbox-button {
color: $toolbarButtonColor;
cursor: pointer;
display: inline-block;
line-height: $newToolbarSize;
text-align: center;
}
.toolbar-button-with-badge {
display: inline-block;
position: relative;
@ -115,86 +107,6 @@
padding-bottom: env(safe-area-inset-bottom, 0);
}
.toolbox-content-items {
background: $newToolbarBackgroundColor;
border-radius: 6px;
margin: 0 auto;
padding: 6px;
text-align: center;
pointer-events: all;
box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
>div {
margin-left: 8px;
&:first-child {
margin-left: 0;
}
}
}
.overflow-menu {
font-size: 14px;
list-style-type: none;
padding: 8px 0;
background-color: $menuBG;
.profile-text {
max-width: 150px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.overflow-menu-item {
align-items: center;
color: $overflowMenuItemColor;
cursor: pointer;
display: flex;
font-size: 14px;
font-weight: 400;
height: 40px;
line-height: 24px;
padding: 8px 16px;
box-sizing: border-box;
@media (hover: hover) and (pointer: fine) {
&:hover {
background: $overflowMenuItemBackground;
}
}
div {
display: flex;
flex-direction: row;
align-items: center;
}
&.unclickable {
cursor: default;
}
&.disabled {
cursor: initial;
color: #929292;
&:hover {
background: none;
}
svg {
fill: #929292;
}
}
@media (hover: hover) and (pointer: fine) {
&.unclickable:hover {
background: inherit;
}
}
}
.beta-tag {
background: #36383C;
border-radius: 3px;
@ -205,73 +117,12 @@
text-transform: uppercase;
}
.overflow-menu-item-icon {
margin-right: 16px;
i {
display: inline;
font-size: 24px;
}
@media (hover: hover) and (pointer: fine) {
i:hover {
background-color: initial;
}
}
img {
max-width: 24px;
max-height: 24px;
}
svg {
fill: #fff;
height: 20px;
width: 20px;
}
}
.overflow-menu-hr {
border-top: 1px solid #4C4D50;
border-bottom: 0;
margin: 8px 0;
}
.toolbox-icon {
display: flex;
border-radius: 3px;
flex-direction: column;
font-size: 24px;
height: $newToolbarSize;
justify-content: center;
width: $newToolbarSize;
@media (hover: hover) and (pointer: fine) {
&:hover {
background: $newToolbarButtonHoverColor;
}
}
@media (max-width: 320px) {
height: 36px;
width: 36px;
}
&.toggled {
background: $newToolbarButtonToggleColor;
}
&.disabled {
cursor: initial !important;
background-color: #36383c !important;
svg {
fill: #929292 !important;
}
}
}
.hangup-button {
background-color: $hangupColor;

View File

@ -1,38 +0,0 @@
.copy-button {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 8px 8px 16px;
margin-top: 5px;
width: calc(100% - 24px);
height: 24px;
background: #0376DA;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #278ADF;
font-weight: 600;
}
&-content {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 292px;
margin-right: 16px;
&.selected {
font-weight: 600;
}
}
&.clicked {
background: #31B76A;
}
& > div > svg > path {
fill: #fff;
}
}

View File

@ -33,7 +33,6 @@ $flagsImagePath: "../images/";
@import 'inlay';
@import 'reload_overlay/reload_overlay';
@import 'mini_toolbox';
@import 'buttons/copy.scss';
@import 'modals/desktop-picker/desktop-picker';
@import 'modals/device-selection/device-selection';
@import 'modals/dialog';

View File

@ -138,7 +138,6 @@
.toolbox-content-items {
background: transparent;
border-radius: 0;
box-shadow: none;
display: flex;
justify-content: space-evenly;

View File

@ -2,14 +2,67 @@
/* eslint-disable react/jsx-no-bind */
import React, { useState } from 'react';
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { Icon, IconCheck, IconCopy } from '../../base/icons';
import { withPixelLineHeight } from '../styles/functions.web';
import { copyText } from '../util';
const styles = theme => {
return {
copyButton: {
...withPixelLineHeight(theme.typography.bodyLongRegular),
borderRadius: theme.shape.borderRadius,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 8px 8px 16px',
marginTop: 5,
width: 'calc(100% - 24px)',
height: 24,
background: theme.palette.action01,
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.action01Hover,
fontWeight: 600
},
'&.clicked': {
background: theme.palette.success02
},
'& > div > svg > path': {
fill: theme.palette.text01
}
},
content: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: 292,
marginRight: 16,
'&.selected': {
fontWeight: 600
}
}
};
};
let mounted;
type Props = {
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* Css class to apply on container.
*/
@ -46,10 +99,18 @@ type Props = {
*
* @returns {React$Element<any>}
*/
function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: Props) {
function CopyButton({ classes, className, displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: Props) {
const [ isClicked, setIsClicked ] = useState(false);
const [ isHovered, setIsHovered ] = useState(false);
useEffect(() => {
mounted = true;
return () => {
mounted = false;
};
}, []);
/**
* Click handler for the element.
*
@ -64,7 +125,10 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
setIsClicked(true);
setTimeout(() => {
setIsClicked(false);
// avoid: Can't perform a React state update on an unmounted component
if (mounted) {
setIsClicked(false);
}
}, 2500);
}
}
@ -112,7 +176,7 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
if (isClicked) {
return (
<>
<div className = 'copy-button-content selected'>
<div className = { clsx(classes.content, 'selected') }>
<span role = { 'alert' }>{ textOnCopySuccess }</span>
</div>
<Icon src = { IconCheck } />
@ -122,8 +186,8 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
return (
<>
<div className = 'copy-button-content'>
{isHovered ? textOnHover : displayedText}
<div className = { `${classes.copyButton}-content` }>
{ isHovered ? textOnHover : displayedText }
</div>
<Icon src = { IconCopy } />
</>
@ -133,7 +197,7 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
return (
<div
aria-label = { textOnHover }
className = { `${className} copy-button${isClicked ? ' clicked' : ''}` }
className = { clsx(className, classes.copyButton, isClicked ? ' clicked' : '') }
id = { id }
onBlur = { onHoverOut }
onClick = { onClick }
@ -152,4 +216,4 @@ CopyButton.defaultProps = {
className: ''
};
export default CopyButton;
export default withStyles(styles)(CopyButton);

View File

@ -1,8 +1,11 @@
// @flow
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { useCallback } from 'react';
import { Icon, IconArrowDown } from '../../../icons';
import { withPixelLineHeight } from '../../../styles/functions.web';
type Props = {
@ -11,6 +14,11 @@ type Props = {
*/
children: React$Node,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* Text css class of the button.
*/
@ -78,6 +86,91 @@ type Props = {
ariaDropDownLabel?: string
};
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current UI theme.
*
* @returns {Object}
*/
const styles = theme => {
return {
actionButton: {
...withPixelLineHeight(theme.typography.bodyLongBold),
borderRadius: theme.shape.borderRadius,
boxSizing: 'border-box',
color: theme.palette.text01,
cursor: 'pointer',
display: 'inline-block',
marginBottom: '16px',
padding: '7px 16px',
position: 'relative',
textAlign: 'center',
width: '100%',
'&.primary': {
background: theme.palette.action01,
border: '1px solid #0376DA',
'&:hover': {
backgroundColor: theme.palette.action01Hover
}
},
'&.secondary': {
background: theme.palette.action02,
border: '1px solid transparent'
},
'&.text': {
width: 'auto',
fontSize: '13px',
margin: '0',
padding: '0'
},
'&.disabled': {
background: theme.palette.action01Disabled,
border: '1px solid #5E6D7A',
color: '#AFB6BC',
cursor: 'initial',
'.icon': {
'& > svg': {
fill: '#AFB6BC'
}
}
},
[theme.breakpoints.down('400')]: {
fontSize: 16,
marginBottom: 8,
padding: '11px 16px'
}
},
options: {
borderRadius: theme.shape.borderRadius / 2,
alignItems: 'center',
display: 'flex',
height: '100%',
justifyContent: 'center',
position: 'absolute',
right: 0,
top: 0,
width: 36,
'&:hover': {
backgroundColor: '#0262B6'
},
'& svg': {
pointerEvents: 'none'
}
}
};
};
/**
* Button used for pre meeting actions.
*
@ -85,6 +178,7 @@ type Props = {
*/
function ActionButton({
children,
classes,
className = '',
disabled,
hasOptions,
@ -115,11 +209,18 @@ function ActionButton({
}
}, [ onOptionsClick, disabled ]);
const containerClasses = clsx(
classes.actionButton,
className && className,
type,
disabled && 'disabled'
);
return (
<div
aria-disabled = { disabled }
aria-label = { ariaLabel }
className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
className = { containerClasses }
data-testid = { testId ? testId : undefined }
onClick = { disabled ? undefined : onClick }
onKeyPress = { onKeyPressHandler }
@ -132,7 +233,7 @@ function ActionButton({
aria-haspopup = 'true'
aria-label = { ariaDropDownLabel }
aria-pressed = { ariaPressed }
className = 'options'
className = { classes.options }
data-testid = 'prejoin.joinOptions'
onClick = { disabled ? undefined : onOptionsClick }
onKeyPress = { onOptionsKeyPressHandler }
@ -148,4 +249,4 @@ function ActionButton({
);
}
export default ActionButton;
export default withStyles(styles)(ActionButton);

View File

@ -370,3 +370,12 @@ export const typography = {
letterSpacing: 0
}
};
export const breakpoints = {
values: {
'0': 0,
'320': 320,
'400': 400,
'480': 480
}
};

View File

@ -1,6 +1,6 @@
// @flow
import { font, colors, colorMap, spacing, shape, typography } from '../Tokens';
import { font, colors, colorMap, spacing, shape, typography, breakpoints } from '../Tokens';
import { createWebTheme } from '../functions';
export default createWebTheme({
@ -10,10 +10,5 @@ export default createWebTheme({
spacing,
shape,
typography,
breakpoints: {
values: {
'0': 0,
'480': 480
}
}
breakpoints
});

View File

@ -15,7 +15,7 @@ import { formatCommonClasses } from '../functions';
const useStyles = makeStyles(theme =>
createStyles({
'@global': {
...formatCommonClasses(commonStyles),
...formatCommonClasses(commonStyles(theme)),
...getGlobalStyles(theme)
}
})

View File

@ -3,10 +3,17 @@
import { ThemeProvider } from '@material-ui/core/styles';
import * as React from 'react';
import { connect } from '../../../base/redux';
import BaseTheme from './BaseTheme';
type Props = {
/**
* The default theme or theme set through advanced branding.
*/
_theme: Object,
/**
* The children of the component.
*/
@ -19,6 +26,22 @@ type Props = {
* @param {Object} props - The props of the component.
* @returns {React.ReactNode}
*/
export default function JitsiThemeProvider(props: Props) {
return <ThemeProvider theme = { BaseTheme }>{ props.children }</ThemeProvider>;
function JitsiThemeProvider(props: Props) {
return <ThemeProvider theme = { props._theme }>{ props.children }</ThemeProvider>;
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
const { muiBrandedTheme } = state['features/dynamic-branding'];
return {
_theme: muiBrandedTheme || BaseTheme
};
}
export default connect(_mapStateToProps)(JitsiThemeProvider);

View File

@ -6,19 +6,153 @@
*
*/
export const commonClassName = {
emptyList: 'empty-list'
emptyList: 'empty-list',
overflowMenuItem: 'overflow-menu-item',
overflowMenuItemIcon: 'overflow-menu-item-icon',
toolboxIcon: 'toolbox-icon',
toolboxButton: 'toolbox-button',
toolboxContentItems: 'toolbox-content-items'
};
/**
* An object containing the declaration of the common, reusable CSS classes.
* Returns an object containing the declaration of the common, reusable CSS classes.
*
* @param {Object} theme -The theme.
*
* @returns {Object} - The common styles.
*/
export const commonStyles = {
// '.empty-list'
[commonClassName.emptyList]: {
listStyleType: 'none',
margin: 0,
padding: 0
}
export const commonStyles = (theme: Object) => {
return {
// '.empty-list'
[commonClassName.emptyList]: {
listStyleType: 'none',
margin: 0,
padding: 0
},
[commonClassName.overflowMenuItem]: {
alignItems: 'center',
color: theme.palette.text01,
cursor: 'pointer',
display: 'flex',
fontSize: 14,
fontWeight: 400,
height: 40,
lineHeight: '24px',
padding: '8px 16px',
boxSizing: 'border-box',
'& > div': {
display: 'flex',
alignItems: 'center'
},
'&.unclickable': {
cursor: 'default'
},
'&.disabled': {
cursor: 'initial',
color: theme.palette.text03,
'&:hover': {
background: 'none'
},
'& svg': {
fill: theme.palette.text03
}
},
'@media (hover: hover) and (pointer: fine)': {
'&:hover': {
background: theme.palette.action02Hover
},
'&.unclickable:hover': {
background: 'inherit'
}
}
},
[commonClassName.overflowMenuItemIcon]: {
marginRight: '16px',
'& i': {
display: 'inline',
fontSize: 24
},
'@media (hover: hover) and (pointer: fine)': {
'&i:hover': {
backgroundColor: 'initial'
}
},
'& img': {
maxWidth: 24,
maxHeight: 24
},
'& svg': {
fill: theme.palette.text01,
height: 20,
width: 20
}
},
[commonClassName.toolboxIcon]: {
display: 'flex',
borderRadius: 3,
flexDirection: 'column',
fontSize: 24,
height: 48,
justifyContent: 'center',
width: 48,
'@media (hover: hover) and (pointer: fine)': {
'&:hover': {
background: theme.palette.action02Hover
}
},
[theme.breakpoints.down('320')]: {
height: 36,
width: 36
},
'&.toggled': {
background: theme.palette.ui02
},
'&.disabled': {
cursor: 'initial !important',
backgroundColor: `${theme.palette.action02Disabled} !important`,
'& svg': {
fill: `${theme.palette.text03} !important`
}
}
},
[commonClassName.toolboxButton]: {
color: theme.palette.text01,
cursor: 'pointer',
display: 'inline-block',
lineHeight: '48px',
textAlign: 'center'
},
[commonClassName.toolboxContentItems]: {
background: theme.palette.ui01,
borderRadius: 6,
margin: '0 auto',
padding: 6,
textAlign: 'center',
pointerEvents: 'all',
boxShadow: '0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15)',
'& > div': {
marginLeft: 8,
'&:first-child': {
marginLeft: 0
}
}
}
};
};
/**

View File

@ -10,7 +10,7 @@ import { createColorTokens } from './utils';
* @param {Object} arg - The ui tokens.
* @returns {Object}
*/
export function createWebTheme({ font, colors, colorMap, shape, spacing, typography }: Object) {
export function createWebTheme({ font, colors, colorMap, shape, spacing, typography, breakpoints }: Object) {
return createMuiTheme({
props: {
// disable ripple effect on buttons globally
@ -27,7 +27,8 @@ export function createWebTheme({ font, colors, colorMap, shape, spacing, typogra
typography: {
font,
...typography
}
},
breakpoints
});
}
@ -46,3 +47,4 @@ export function formatCommonClasses(stylesObj: Object) {
return formatted;
}

View File

@ -1,5 +1,9 @@
// @flow
import { createMuiTheme } from '@material-ui/core/styles';
import { loadConfig } from '../base/lib-jitsi-meet';
import { font, colors, colorMap, spacing, shape, typography, breakpoints } from '../base/ui/Tokens';
import { createColorTokens } from '../base/ui/utils';
/**
* Extracts the fqn part from a path, where fqn represents
@ -45,3 +49,89 @@ export async function getDynamicBrandingUrl() {
export function isDynamicBrandingDataLoaded(state: Object) {
return state['features/dynamic-branding'].customizationReady;
}
/**
* Creates MUI branding theme based on the custom theme json.
*
* @param {Object} customTheme - The branded custom theme.
* @returns {Object} - The MUI theme.
*/
export function createMuiBrandingTheme(customTheme: Object) {
const {
palette: customPalette,
shape: customShape,
typography: customTypography,
breakpoints: customBreakpoints,
spacing: customSpacing
} = customTheme;
const newPalette = createColorTokens(colorMap, colors);
if (customPalette) {
overwriteRecurrsive(newPalette, customPalette);
}
const newShape = { ...shape };
if (customShape) {
overwriteRecurrsive(newShape, customShape);
}
const newTypography = {
font: { ...font },
...typography
};
if (customTypography) {
overwriteRecurrsive(newTypography, customTypography);
}
const newBreakpoints = { ...breakpoints };
if (customBreakpoints) {
overwriteRecurrsive(newBreakpoints, customBreakpoints);
}
let newSpacing = [ ...spacing ];
if (customSpacing && customSpacing.length) {
newSpacing = customSpacing;
}
return createMuiTheme({
props: {
// disable ripple effect on buttons globally
MuiButtonBase: {
disableRipple: true
}
},
// use token spacing array
spacing: newSpacing
}, {
palette: newPalette,
shape: newShape,
typography: newTypography,
breakpoints: newBreakpoints
});
}
/**
* Overwrites recursively values from object 2 into object 1 based on common keys.
* (Merges object2 into object1).
*
* @param {Object} obj1 - The object holding the merged values.
* @param {Object} obj2 - The object to compare to and take values from.
* @returns {void}
*/
function overwriteRecurrsive(obj1: Object, obj2: Object) {
Object.keys(obj2).forEach(key => {
if (obj1.hasOwnProperty(key)) {
if (typeof obj1[key] === 'object') {
overwriteRecurrsive(obj1[key], obj2[key]);
} else {
obj1[key] = obj2[key];
}
}
});
}

View File

@ -3,7 +3,9 @@
import { APP_WILL_MOUNT } from '../base/app';
import { MiddlewareRegistry } from '../base/redux';
import { SET_DYNAMIC_BRANDING_DATA } from './actionTypes';
import { fetchCustomBrandingData } from './actions';
import { createMuiBrandingTheme } from './functions';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
@ -12,6 +14,13 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(fetchCustomBrandingData());
break;
}
case SET_DYNAMIC_BRANDING_DATA: {
const { customTheme } = action.value;
if (customTheme) {
action.value.muiBrandedTheme = createMuiBrandingTheme(customTheme);
}
}
}
return next(action);

View File

@ -100,6 +100,14 @@ const DEFAULT_STATE = {
*/
logoImageUrl: '',
/**
* The generated MUI branded theme based on the custom theme json.
*
* @public
* @type {boolean}
*/
muiBrandedTheme: undefined,
/**
* The lobby/prejoin background.
*
@ -140,6 +148,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
inviteDomain,
logoClickUrl,
logoImageUrl,
muiBrandedTheme,
premeetingBackground,
virtualBackgrounds
} = action.value;
@ -153,6 +162,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
inviteDomain,
logoClickUrl,
logoImageUrl,
muiBrandedTheme,
premeetingBackground,
customizationFailed: false,
customizationReady: true,

View File

@ -0,0 +1,109 @@
// @flow
import { withStyles } from '@material-ui/styles';
import React from 'react';
import { Icon } from '../../base/icons';
type Props = {
/**
* The css classes generated from theme.
*/
classes: Object,
/**
* Attribute used in automated testing.
*/
dataTestId: string,
/**
* The button's icon.
*/
icon: HTMLElement,
/**
* The button's label.
*/
label: string,
/**
* Function to be called when button is clicked.
*/
onButtonClick: Function,
/**
* Function to be called on key pressed.
*/
onKeyPressed: Function,
/**
* Used for translation.
*/
t: Function
};
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current UI theme.
*
* @returns {Object}
*/
const styles = theme => {
return {
prejoinPreviewDropdownBtn: {
alignItems: 'center',
color: '#1C2025',
cursor: 'pointer',
display: 'flex',
height: 40,
fontSize: 15,
lineHeight: 24,
padding: '0 16px',
backgroundColor: theme.palette.field02,
'&:hover': {
backgroundColor: theme.palette.field02Hover
}
},
prejoinPreviewDropdownIcon: {
display: 'inline-block',
marginRight: 16,
'& > svg': {
fill: '#1C2025'
}
}
};
};
/**
* Buttons used for pre meeting actions.
*
* @returns {ReactElement}
*/
const DropdownButton = ({
classes,
dataTestId,
icon,
onButtonClick,
onKeyPressed,
label
}: Props) => (
<div
className = { classes.prejoinPreviewDropdownBtn }
data-testid = { dataTestId }
onClick = { onButtonClick }
onKeyPress = { onKeyPressed }
role = 'button'
tabIndex = { 0 }>
<Icon
className = { classes.prejoinPreviewDropdownIcon }
size = { 24 }
src = { icon } />
{label}
</div>
);
export default withStyles(styles)(DropdownButton);

View File

@ -6,7 +6,7 @@ import React, { Component } from 'react';
import { getRoomName } from '../../base/conference';
import { isNameReadOnly } from '../../base/config';
import { translate } from '../../base/i18n';
import { Icon, IconArrowDown, IconArrowUp, IconPhone, IconVolumeOff } from '../../base/icons';
import { IconArrowDown, IconArrowUp, IconPhone, IconVolumeOff } from '../../base/icons';
import { isVideoMutedByUser } from '../../base/media';
import { ActionButton, InputField, PreMeetingScreen } from '../../base/premeeting';
import { connect } from '../../base/redux';
@ -24,6 +24,7 @@ import {
isJoinByPhoneDialogVisible
} from '../functions';
import DropdownButton from './DropdownButton';
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
type Props = {
@ -327,32 +328,18 @@ class Prejoin extends Component<Props, State> {
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { <div className = 'prejoin-preview-dropdown-btns'>
<div
className = 'prejoin-preview-dropdown-btn'
data-testid = 'prejoin.joinWithoutAudio'
onClick = { joinConferenceWithoutAudio }
onKeyPress = { _onJoinConferenceWithoutAudioKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconVolumeOff } />
{ t('prejoin.joinWithoutAudio') }
</div>
{hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }
onKeyPress = { _showDialogKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'prejoin-preview-dropdown-icon'
data-testid = 'prejoin.joinByPhone'
size = { 24 }
src = { IconPhone } />
{ t('prejoin.joinAudioByPhone') }
</div>}
<DropdownButton
dataTestId = 'prejoin.joinWithoutAudio'
icon = { IconVolumeOff }
label = { t('prejoin.joinWithoutAudio') }
onButtonClick = { joinConferenceWithoutAudio }
onKeyPressed = { _onJoinConferenceWithoutAudioKeyPress } />
{hasJoinByPhoneButton && <DropdownButton
dataTestId = 'prejoin.joinByPhone'
icon = { IconPhone }
label = { t('prejoin.joinAudioByPhone') }
onButtonClick = { _showDialog }
onKeyPressed = { _showDialogKeyPress } />}
</div> }
isOpen = { showJoinByPhoneButtons }
onClose = { _onDropdownClose }>

View File

@ -1,5 +1,6 @@
// @flow
import { withStyles } from '@material-ui/core/styles';
import React, { Component, Fragment } from 'react';
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
@ -224,6 +225,11 @@ type Props = {
*/
_virtualSource: Object,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* Invoked to active other features of the app.
*/
@ -248,6 +254,17 @@ type Props = {
declare var APP: Object;
const styles = theme => {
return {
overflowMenu: {
fontSize: 14,
listStyleType: 'none',
padding: '8px 0',
backgroundColor: theme.palette.ui03
}
};
};
/**
* Implements the conference toolbox on React/Web.
*
@ -1200,10 +1217,11 @@ class Toolbox extends Component<Props> {
const {
_isMobile,
_overflowMenuVisible,
_reactionsEnabled,
_toolbarButtons,
classes,
showDominantSpeakerName,
t,
_reactionsEnabled
t
} = this.props;
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
@ -1240,7 +1258,7 @@ class Toolbox extends Component<Props> {
}>
<ul
aria-label = { t(toolbarAccLabel) }
className = 'overflow-menu'
className = { classes.overflowMenu }
id = 'overflow-menu'
onKeyDown = { this._onEscKey }
role = 'menu'>
@ -1346,4 +1364,4 @@ function _mapStateToProps(state, ownProps) {
};
}
export default translate(connect(_mapStateToProps)(Toolbox));
export default translate(connect(_mapStateToProps)(withStyles(styles)(Toolbox)));

View File

@ -0,0 +1,185 @@
{
"spacing": [ 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 ],
"breakpoints": {
"values": {
"0": 0,
"320": 320,
"400": 400,
"480": 480
}
},
"palette": {
"uiBackground": "#040404",
"ui01": "#141414",
"ui02": "#292929",
"ui03": "#3D3D3D",
"ui04": "#525252",
"ui05": "#666",
"action01": "#0056E0",
"screen01Header": "#17A0DB",
"action01Hover": "#246FE5",
"action01Active": "#0045B3",
"action01Focus": "#99BBF3",
"action01Disabled": "#00225A",
"action02": "#3D3D3D",
"action02Hover": "#525252",
"action02Active": "#292929",
"action02Focus": "#858585",
"action02Disabled": "#141414",
"action03": "transparent",
"action03Hover": "#525252",
"action03Active": "#292929",
"action03Focus": "#858585",
"action03Disabled": "transparent",
"actionDanger": "#CB2233",
"actionDangerHover": "#E04757",
"actionDangerActive": "#A21B29",
"actionDangerFocus": "#EAA7AD",
"actionDangerDisabled": "#7A141F",
"bottomSheet": "#111111",
"text01": "#FFF",
"text02": "#C2C2C2",
"text03": "#858585",
"text04": "#AAAAAA",
"textError": "#E04757",
"icon01": "#FFF",
"icon02": "#C2C2C2",
"icon03": "#858585",
"iconError": "#E04757",
"field01": "#040404",
"field01Hover": "#292929",
"field01Focus": "#0056E0",
"field01Disabled": "#525252",
"field02": "#FFF",
"dividerColor": "#AAAAAA",
"field02Hover": "#CCDDF9",
"field02Focus": "#0056E0",
"field02Disabled": "#666",
"section01": "#E0E0E0",
"section01Active": "#0045B3",
"section01Inactive": "#040404",
"border01": "#A3A3A3",
"border02": "#666",
"border03": "#3D3D3D",
"borderError": "#E04757",
"link01": "#669AEC",
"link01Hover": "#99BBF3",
"link01Active": "#246FE5",
"success01": "#1EC26A",
"success02": "#1EC26A",
"warning01": "#F8AE1A",
"warning02": "#ED9E1B"
},
"typography": {
"font": {
"weightRegular": "400",
"weightSemiBold": "600"
},
"labelRegular": {
"fontSize": 12,
"lineHeight": 16,
"fontWeight": "400",
"letterSpacing": 0.16
},
"labelBold": {
"fontSize": 12,
"lineHeight": 16,
"fontWeight": "600",
"letterSpacing": 0.16
},
"labelButton": {
"fontSize": 14,
"lineHeight": 24,
"fontWeight": "600",
"letterSpacing": 0
},
"labelButtonLarge": {
"fontSize": 16,
"lineHeight": 24,
"fontWeight": "600",
"letterSpacing": 0
},
"bodyShortRegular": {
"fontSize": 14,
"lineHeight": 18,
"fontWeight": "400",
"letterSpacing": 0
},
"bodyShortBold": {
"fontSize": 14,
"lineHeight": 18,
"fontWeight": "600",
"letterSpacing": 0
},
"bodyShortRegularLarge": {
"fontSize": 16,
"lineHeight": 24,
"fontWeight": "400",
"letterSpacing": 0
},
"bodyShortBoldLarge": {
"fontSize": 16,
"lineHeight": 24,
"fontWeight": "600",
"letterSpacing": 0
},
"bodyLongRegular": {
"fontSize": 14,
"lineHeight": 24,
"fontWeight": "400",
"letterSpacing": 0
},
"bodyLongBold": {
"fontSize": 14,
"lineHeight": 24,
"fontWeight": "600",
"letterSpacing": 0
},
"heading1": {
"fontSize": 54,
"lineHeight": 64,
"fontWeight": "600",
"letterSpacing": 0
},
"heading2": {
"fontSize": 42,
"lineHeight": 50,
"fontWeight": "600",
"letterSpacing": 0
},
"heading3": {
"fontSize": 32,
"lineHeight": 40,
"fontWeight": "600",
"letterSpacing": 0
},
"heading4": {
"fontSize": 28,
"lineHeight": 36,
"fontWeight": "600",
"letterSpacing": 0
},
"heading5": {
"fontSize": 20,
"lineHeight": 28,
"fontWeight": "600",
"letterSpacing": 0
},
"heading6": {
"fontSize": 16,
"lineHeight": 26,
"fontWeight": "600",
"letterSpacing": 0
},
"heading7": {
"fontSize": 14,
"lineHeight": 24,
"fontWeight": "600",
"letterSpacing": 0
}
},
"shape": {
"borderRadius": 6,
"boxShadow": "inset 0px -1px 0px rgba(255, 255, 255, 0.15)"
}
}