feat(ui-components) Add button component (#11868)

This commit is contained in:
Robert Pintilii 2022-07-20 09:19:59 +03:00 committed by GitHub
parent 718d32990d
commit b08ed3ade4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 365 additions and 349 deletions

4
custom.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.svg' {
const content: any;
export default content;
}

View File

@ -17,7 +17,9 @@ module.exports = {
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-prototype-builtins': 'off'
'no-prototype-builtins': 'off',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': [ 'error' ]
},
'plugins': [ '@typescript-eslint' ],
'extends': [

View File

@ -0,0 +1,195 @@
import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import React from 'react';
import Icon from '../../icons/components/Icon';
import { BUTTON_TYPES } from '../../react/constants';
import { withPixelLineHeight } from '../../styles/functions.web';
import { ButtonProps } from './types';
interface IButtonProps extends ButtonProps {
/**
* Whether or not the button should be full width.
*/
fullWidth?: boolean,
/**
* The id of the button.
*/
id?: string;
/**
* Click callback.
*/
onClick: () => void;
/**
* Which size the button should be.
*/
size?: 'small' | 'medium' | 'large';
}
const useStyles = makeStyles((theme: any) => {
return {
button: {
backgroundColor: theme.palette.action01,
color: theme.palette.text01,
borderRadius: theme.shape.borderRadius,
padding: '10px 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 0,
...withPixelLineHeight(theme.typography.bodyShortBold),
transition: 'background .2s',
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.action01Hover
},
'&:active': {
backgroundColor: theme.palette.action01Active
},
'&:focus': {
outline: 0,
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
},
'& svg': {
fill: theme.palette.icon01
}
},
primary: {},
secondary: {
backgroundColor: theme.palette.action02,
color: theme.palette.text04,
'&:hover': {
backgroundColor: theme.palette.action02Hover
},
'&:active': {
backgroundColor: theme.palette.action02Active
},
'& svg': {
fill: theme.palette.icon04
}
},
tertiary: {
backgroundColor: theme.palette.action03,
'&:hover': {
backgroundColor: theme.palette.action03Hover
},
'&:active': {
backgroundColor: theme.palette.action03Active
}
},
destructive: {
backgroundColor: theme.palette.actionDanger,
'&:hover': {
backgroundColor: theme.palette.actionDangerHover
},
'&:active': {
backgroundColor: theme.palette.actionDangerActive
}
},
disabled: {
backgroundColor: theme.palette.disabled01,
color: theme.palette.text03,
'&:hover': {
backgroundColor: theme.palette.disabled01,
color: theme.palette.text03
},
'&:active': {
backgroundColor: theme.palette.disabled01,
color: theme.palette.text03
},
'& svg': {
fill: theme.palette.icon03
}
},
iconButton: {
padding: '10px'
},
textWithIcon: {
marginLeft: `${theme.spacing(2)}px`
},
small: {
padding: '8px 16px',
...withPixelLineHeight(theme.typography.labelBold),
'&.iconButton': {
padding: '6px'
}
},
medium: {},
large: {
padding: '13px 16px',
...withPixelLineHeight(theme.typography.bodyShortBoldLarge),
'&.iconButton': {
padding: '14px'
}
},
fullWidth: {
width: '100%'
}
};
});
const Button = ({
accessibilityLabel,
disabled,
fullWidth,
icon,
id,
label,
onClick,
size = 'medium',
type = BUTTON_TYPES.PRIMARY
}: IButtonProps) => {
const styles = useStyles();
return (
<button
aria-label = { accessibilityLabel }
className = { clsx(styles.button, styles[type],
disabled && styles.disabled,
icon && !label && `${styles.iconButton} iconButton`,
styles[size], fullWidth && styles.fullWidth) }
disabled = { disabled }
{ ...(id ? { id } : {}) }
onClick = { onClick }
type = 'button'>
{icon && <Icon
size = { 20 }
src = { icon } />}
{label && <span className = { icon ? styles.textWithIcon : '' }>{label}</span>}
</button>
);
};
export default Button;

View File

@ -0,0 +1,29 @@
import { BUTTON_TYPES } from '../../react/constants';
export interface ButtonProps {
/**
* Label used for accessibility.
*/
accessibilityLabel: string;
/**
* Whether or not the button is disabled.
*/
disabled?: boolean;
/**
* The icon to be displayed on the button.
*/
icon?: Function;
/**
* The text to be displayed on the button.
*/
label?: string;
/**
* The type of button to be displayed.
*/
type?: BUTTON_TYPES;
}

View File

@ -1,8 +1,10 @@
// @flow
/* eslint-disable import/order */
import React, { useCallback } from 'react';
// @ts-ignore
import { Container } from '../../react/base';
// @ts-ignore
import { styleTypeToObject } from '../../styles';
type Props = {
@ -15,7 +17,7 @@ type Props = {
/**
* Color of the icon (if not provided by the style object).
*/
color?: ?string,
color?: string,
/**
* Id prop (mainly for autotests).
@ -35,7 +37,7 @@ type Props = {
/**
* The size of the icon (if not provided by the style object).
*/
size?: ?number | string,
size?: number | string,
/**
* The preloaded icon component to render.
@ -110,7 +112,7 @@ export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
* Implements an Icon component that takes a loaded SVG file as prop and renders it as an icon.
*
* @param {Props} props - The props of the component.
* @returns {Reactelement}
* @returns {ReactElement}
*/
export default function Icon(props: Props) {
const {

View File

@ -1,5 +1,3 @@
// @flow
export { default as IconAdd } from './add.svg';
export { default as IconAddPeople } from './link.svg';
export { default as IconArrowBack } from './arrow_back.svg';

View File

@ -11,13 +11,13 @@ import {
import BaseTheme from '../../../ui/components/BaseTheme.native';
// @ts-ignore
import { BUTTON_MODES, BUTTON_TYPES } from '../../constants';
import { ButtonProps } from '../../types';
import { IButtonProps } from '../../types';
// @ts-ignore
import styles from './styles';
const Button: React.FC<ButtonProps> = ({
const Button: React.FC<IButtonProps> = ({
accessibilityLabel,
color: buttonColor,
disabled,
@ -27,7 +27,7 @@ const Button: React.FC<ButtonProps> = ({
onPress,
style,
type
}: ButtonProps) => {
}: IButtonProps) => {
const { t } = useTranslation();
const { CONTAINED } = BUTTON_MODES;
const { DESTRUCTIVE, PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;

View File

@ -1,5 +1,3 @@
// @flow
/**
* Z-index for components that are to be rendered like an overlay, to be over
* everything, such as modal-type of components, or dialogs.
@ -9,12 +7,12 @@ export const OVERLAY_Z_INDEX = 1000;
/**
* The types of the buttons.
*/
export const BUTTON_TYPES = {
PRIMARY: 'primary',
SECONDARY: 'secondary',
TERTIARY: 'tertiary',
DESTRUCTIVE: 'destructive'
};
export enum BUTTON_TYPES {
PRIMARY = 'primary',
SECONDARY = 'secondary',
TERTIARY = 'tertiary',
DESTRUCTIVE = 'destructive'
}
/**
* The modes of the buttons.

View File

@ -1,13 +1,10 @@
export interface ButtonProps {
accessibilityLabel?: string;
import { ButtonProps } from '../components/common/types';
export interface IButtonProps extends ButtonProps {
color?: string;
disabled?: boolean;
icon?: JSX.Element;
label?: string;
labelStyle?: Object|undefined;
onPress?: Function;
style?: Object|undefined;
type?: string;
}
export interface IconButtonProps {

View File

@ -1,7 +1,7 @@
// @flow
import { type StyleType } from './functions.any';
// @ts-ignore
import { StyleType } from './functions.any';
// @ts-ignore
export * from './functions.any';
/**
@ -32,7 +32,7 @@ export function getFixedPlatformStyle(style: StyleType): StyleType {
* @param {Object} base - The base object containing the `lineHeight` property.
* @returns {Object}
*/
export function withPixelLineHeight(base: Object): Object {
export function withPixelLineHeight(base: any): Object {
return {
...base,
lineHeight: `${base.lineHeight}px`

View File

@ -1,36 +1,27 @@
// @flow
import { makeStyles } from '@material-ui/core/styles';
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import Button from '../../../../../base/components/common/Button';
import { BUTTON_TYPES } from '../../../../../base/react/constants';
// @ts-ignore
import { createBreakoutRoom } from '../../../../../breakout-rooms/actions';
import ParticipantPaneBaseButton from '../../../web/ParticipantPaneBaseButton';
const useStyles = makeStyles(() => {
return {
button: {
width: '100%'
}
};
});
export const AddBreakoutRoomButton = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const styles = useStyles();
const onAdd = useCallback(() =>
dispatch(createBreakoutRoom())
, [ dispatch ]);
return (
<ParticipantPaneBaseButton
<Button
accessibilityLabel = { t('breakoutRooms.actions.add') }
className = { styles.button }
onClick = { onAdd }>
{t('breakoutRooms.actions.add')}
</ParticipantPaneBaseButton>
fullWidth = { true }
label = { t('breakoutRooms.actions.add') }
onClick = { onAdd }
type = { BUTTON_TYPES.SECONDARY } />
);
};

View File

@ -1,42 +0,0 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { autoAssignToBreakoutRooms } from '../../../../../breakout-rooms/actions';
import ParticipantPaneBaseButton from '../../../web/ParticipantPaneBaseButton';
const useStyles = makeStyles(theme => {
return {
button: {
color: theme.palette.link01,
width: '100%',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent'
}
}
};
});
export const AutoAssignButton = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const styles = useStyles();
const onAutoAssign = useCallback(() => {
dispatch(autoAssignToBreakoutRooms());
}, [ dispatch ]);
return (
<ParticipantPaneBaseButton
accessibilityLabel = { t('breakoutRooms.actions.autoAssign') }
className = { styles.button }
onClick = { onAutoAssign }>
{t('breakoutRooms.actions.autoAssign')}
</ParticipantPaneBaseButton>
);
};

View File

@ -0,0 +1,27 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import Button from '../../../../../base/components/common/Button';
import { BUTTON_TYPES } from '../../../../../base/react/constants';
// @ts-ignore
import { autoAssignToBreakoutRooms } from '../../../../../breakout-rooms/actions';
export const AutoAssignButton = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const onAutoAssign = useCallback(() => {
dispatch(autoAssignToBreakoutRooms());
}, [ dispatch ]);
return (
<Button
accessibilityLabel = { t('breakoutRooms.actions.autoAssign') }
fullWidth = { true }
label = { t('breakoutRooms.actions.autoAssign') }
onClick = { onAutoAssign }
type = { BUTTON_TYPES.TERTIARY } />
);
};

View File

@ -1,44 +0,0 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { createBreakoutRoomsEvent, sendAnalytics } from '../../../../../analytics';
import { moveToRoom } from '../../../../../breakout-rooms/actions';
import ParticipantPaneBaseButton from '../../../web/ParticipantPaneBaseButton';
const useStyles = makeStyles(theme => {
return {
button: {
color: theme.palette.textError,
backgroundColor: 'transparent',
width: '100%',
'&:hover': {
backgroundColor: 'transparent'
}
}
};
});
export const LeaveButton = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const styles = useStyles();
const onLeave = useCallback(() => {
sendAnalytics(createBreakoutRoomsEvent('leave'));
dispatch(moveToRoom());
}, [ dispatch ]);
return (
<ParticipantPaneBaseButton
accessibilityLabel = { t('breakoutRooms.actions.leaveBreakoutRoom') }
className = { styles.button }
onClick = { onLeave }>
{t('breakoutRooms.actions.leaveBreakoutRoom')}
</ParticipantPaneBaseButton>
);
};

View File

@ -0,0 +1,30 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
// @ts-ignore
import { createBreakoutRoomsEvent, sendAnalytics } from '../../../../../analytics';
import Button from '../../../../../base/components/common/Button';
import { BUTTON_TYPES } from '../../../../../base/react/constants';
// @ts-ignore
import { moveToRoom } from '../../../../../breakout-rooms/actions';
export const LeaveButton = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const onLeave = useCallback(() => {
sendAnalytics(createBreakoutRoomsEvent('leave'));
dispatch(moveToRoom());
}, [ dispatch ]);
return (
<Button
accessibilityLabel = { t('breakoutRooms.actions.leaveBreakoutRoom') }
fullWidth = { true }
label = { t('breakoutRooms.actions.leaveBreakoutRoom') }
onClick = { onLeave }
type = { BUTTON_TYPES.DESTRUCTIVE } />
);
};

View File

@ -1,57 +0,0 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import ParticipantPaneBaseButton from './ParticipantPaneBaseButton';
type Props = {
/**
* Label used for accessibility.
*/
accessibilityLabel: String,
/**
* Children of the component.
*/
children: string | React$Node,
/**
* Button id.
*/
id?: string,
/**
* Whether or not the button is icon button (no text).
*/
isIconButton?: boolean,
/**
* Click handler.
*/
onClick: Function
}
const useStyles = makeStyles(theme => {
return {
button: {
padding: `${theme.spacing(2)}px`
}
};
});
const FooterButton = ({ accessibilityLabel, children, id, isIconButton = false, onClick }: Props) => {
const styles = useStyles();
return (<ParticipantPaneBaseButton
accessibilityLabel = { accessibilityLabel }
className = { isIconButton ? styles.button : '' }
id = { id }
onClick = { onClick }>
{children}
</ParticipantPaneBaseButton>
);
};
export default FooterButton;

View File

@ -1,48 +0,0 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { Icon, IconInviteMore } from '../../../base/icons';
import { beginAddPeople } from '../../../invite';
import ParticipantPaneBaseButton from './ParticipantPaneBaseButton';
const useStyles = makeStyles(theme => {
return {
button: {
width: '100%',
'& > *:not(:last-child)': {
marginRight: `${theme.spacing(2)}px`
}
}
};
});
export const InviteButton = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const styles = useStyles();
const onInvite = useCallback(() => {
sendAnalytics(createToolbarEvent('invite'));
dispatch(beginAddPeople());
}, [ dispatch ]);
return (
<ParticipantPaneBaseButton
accessibilityLabel = { t('participantsPane.actions.invite') }
className = { styles.button }
onClick = { onInvite }
primary = { true }>
<Icon
size = { 20 }
src = { IconInviteMore } />
<span>{t('participantsPane.actions.invite')}</span>
</ParticipantPaneBaseButton>
);
};

View File

@ -0,0 +1,32 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
// @ts-ignore
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import Button from '../../../base/components/common/Button';
import { IconInviteMore } from '../../../base/icons/svg/index';
import { BUTTON_TYPES } from '../../../base/react/constants';
// @ts-ignore
import { beginAddPeople } from '../../../invite';
export const InviteButton = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const onInvite = useCallback(() => {
sendAnalytics(createToolbarEvent('invite'));
dispatch(beginAddPeople());
}, [ dispatch ]);
return (
<Button
accessibilityLabel = { t('participantsPane.actions.invite') }
fullWidth = { true }
icon = { IconInviteMore }
label = { t('participantsPane.actions.invite') }
onClick = { onInvite }
type = { BUTTON_TYPES.PRIMARY } />
);
};

View File

@ -1,98 +0,0 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
type Props = {
/**
* Label used for accessibility.
*/
accessibilityLabel: String,
/**
* Additional class name for custom styles.
*/
className?: string,
/**
* Children of the component.
*/
children: string | React$Node,
/**
* Button id.
*/
id?: string,
/**
* Click handler.
*/
onClick: Function,
/**
* Whether or not the button should have primary button style.
*/
primary?: boolean
}
const useStyles = makeStyles(theme => {
return {
button: {
alignItems: 'center',
backgroundColor: theme.palette.ui03,
border: 0,
borderRadius: `${theme.shape.borderRadius}px`,
display: 'flex',
justifyContent: 'center',
minHeight: '40px',
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`,
...theme.typography.labelButton,
lineHeight: `${theme.typography.labelButton.lineHeight}px`,
'&:hover': {
backgroundColor: theme.palette.ui04
},
[`@media (max-width: ${participantsPaneTheme.MD_BREAKPOINT})`]: {
...theme.typography.labelButtonLarge,
lineHeight: `${theme.typography.labelButtonLarge.lineHeight}px`,
minWidth: '48px',
minHeight: '48px'
}
},
buttonPrimary: {
backgroundColor: theme.palette.action01,
'&:hover': {
backgroundColor: theme.palette.action01Hover
}
}
};
});
const ParticipantPaneBaseButton = ({
accessibilityLabel,
className,
children,
id,
onClick,
primary = false
}: Props) => {
const styles = useStyles();
return (
<button
aria-label = { accessibilityLabel }
className = { `${styles.button} ${primary ? styles.buttonPrimary : ''} ${className ?? ''}` }
id = { id }
onClick = { onClick }>
{children}
</button>
);
};
export default ParticipantPaneBaseButton;

View File

@ -3,11 +3,13 @@
import { withStyles } from '@material-ui/core';
import React, { Component } from 'react';
import Button from '../../../base/components/common/Button';
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { Icon, IconClose, IconHorizontalPoints } from '../../../base/icons';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { BUTTON_TYPES } from '../../../base/react/constants';
import { connect } from '../../../base/redux';
import { isAddBreakoutRoomButtonVisible } from '../../../breakout-rooms/functions';
import { MuteEveryoneDialog } from '../../../video-menu/components/';
@ -21,7 +23,6 @@ import {
import { AddBreakoutRoomButton } from '../breakout-rooms/components/web/AddBreakoutRoomButton';
import { RoomList } from '../breakout-rooms/components/web/RoomList';
import FooterButton from './FooterButton';
import { FooterContextMenu } from './FooterContextMenu';
import LobbyParticipants from './LobbyParticipants';
import MeetingParticipants from './MeetingParticipants';
@ -258,21 +259,20 @@ class ParticipantsPane extends Component<Props, State> {
{_showFooter && (
<div className = { classes.footer }>
{_showMuteAllButton && (
<FooterButton
<Button
accessibilityLabel = { t('participantsPane.actions.muteAll') }
onClick = { this._onMuteAll }>
{t('participantsPane.actions.muteAll')}
</FooterButton>
label = { t('participantsPane.actions.muteAll') }
onClick = { this._onMuteAll }
type = { BUTTON_TYPES.SECONDARY } />
)}
{_showMoreActionsButton && (
<div className = { classes.footerMoreContainer }>
<FooterButton
<Button
accessibilityLabel = { t('participantsPane.actions.moreModerationActions') }
icon = { IconHorizontalPoints }
id = 'participants-pane-context-menu'
isIconButton = { true }
onClick = { this._onToggleContext }>
<Icon src = { IconHorizontalPoints } />
</FooterButton>
onClick = { this._onToggleContext }
type = { BUTTON_TYPES.SECONDARY } />
<FooterContextMenu
isOpen = { contextOpen }
onDrawerClose = { this._onDrawerClose }

View File

@ -1,5 +1,5 @@
{
"include": ["react/features/**/*.ts", "react/features/**/*.tsx"],
"include": ["react/features/**/*.ts", "react/features/**/*.tsx", "./custom.d.ts"],
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"module": "es6",