diff --git a/react/features/base/components/common/Button.tsx b/react/features/base/components/common/Button.tsx index cc16c68eaa..8a59b9093b 100644 --- a/react/features/base/components/common/Button.tsx +++ b/react/features/base/components/common/Button.tsx @@ -5,6 +5,7 @@ import React from 'react'; import Icon from '../../icons/components/Icon'; import { BUTTON_TYPES } from '../../react/constants'; import { withPixelLineHeight } from '../../styles/functions.web'; +import { Theme } from '../../ui/types'; import { ButtonProps } from './types'; @@ -31,7 +32,7 @@ interface IButtonProps extends ButtonProps { size?: 'small' | 'medium' | 'large'; } -const useStyles = makeStyles((theme: any) => { +const useStyles = makeStyles((theme: Theme) => { return { button: { backgroundColor: theme.palette.action01, diff --git a/react/features/base/components/common/Input.tsx b/react/features/base/components/common/Input.tsx new file mode 100644 index 0000000000..94877b1a18 --- /dev/null +++ b/react/features/base/components/common/Input.tsx @@ -0,0 +1,167 @@ +import { makeStyles } from '@material-ui/core'; +import clsx from 'clsx'; +import React, { useCallback } from 'react'; + +import { isMobileBrowser } from '../../environment/utils'; +import Icon from '../../icons/components/Icon'; +import { IconCloseCircle } from '../../icons/svg/index'; +import { withPixelLineHeight } from '../../styles/functions.web'; +import { Theme } from '../../ui/types'; + +import { InputProps } from './types'; + +interface IInputProps extends InputProps { + bottomLabel?: string; + className?: string; + type?: 'text' | 'email' | 'number' | 'password'; +} + +const useStyles = makeStyles((theme: Theme) => { + return { + inputContainer: { + display: 'flex', + flexDirection: 'column' + }, + + label: { + color: theme.palette.text01, + ...withPixelLineHeight(theme.typography.bodyShortRegular), + marginBottom: `${theme.spacing(2)}px`, + + '&.is-mobile': { + ...withPixelLineHeight(theme.typography.bodyShortRegularLarge) + } + }, + + fieldContainer: { + position: 'relative', + display: 'flex' + }, + + input: { + backgroundColor: theme.palette.ui03, + color: theme.palette.text01, + ...withPixelLineHeight(theme.typography.bodyShortRegular), + padding: '10px 16px', + borderRadius: theme.shape.borderRadius, + border: 0, + height: '40px', + boxSizing: 'border-box', + width: '100%', + + '&::placeholder': { + color: theme.palette.text02 + }, + + '&:focus': { + outline: 0, + boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}` + }, + + '&:disabled': { + color: theme.palette.text03 + }, + + '&.is-mobile': { + height: '48px', + padding: '13px 16px', + ...withPixelLineHeight(theme.typography.bodyShortRegularLarge) + }, + + '&.error': { + boxShadow: `0px 0px 0px 2px ${theme.palette.textError}` + } + }, + + icon: { + position: 'absolute', + top: '10px', + left: '16px' + }, + + iconInput: { + paddingLeft: '46px' + }, + + clearableInput: { + paddingRight: '46px' + }, + + clearButton: { + position: 'absolute', + right: '16px', + top: '10px', + cursor: 'pointer', + backgroundColor: theme.palette.action03, + border: 0, + padding: 0 + }, + + bottomLabel: { + marginTop: `${theme.spacing(2)}px`, + ...withPixelLineHeight(theme.typography.labelRegular), + color: theme.palette.text02, + + '&.is-mobile': { + ...withPixelLineHeight(theme.typography.bodyShortRegular) + }, + + '&.error': { + color: theme.palette.textError + } + } + }; +}); + +const Input = ({ + bottomLabel, + className, + clearable = false, + disabled, + error, + icon, + label, + onChange, + placeholder, + type = 'text', + value +}: IInputProps) => { + const styles = useStyles(); + const isMobile = isMobileBrowser(); + + const handleChange = useCallback((e: React.ChangeEvent) => + onChange(e.target.value), []); + + const clearInput = useCallback(() => onChange(''), []); + + return (
+ {label && {label}} +
+ {icon && } + + {clearable && !disabled && value !== '' && } +
+ {bottomLabel && ( + + {bottomLabel} + + )} +
); +}; + +export default Input; diff --git a/react/features/base/components/common/types.ts b/react/features/base/components/common/types.ts index 43b4abb154..a5416b4f2e 100644 --- a/react/features/base/components/common/types.ts +++ b/react/features/base/components/common/types.ts @@ -27,3 +27,46 @@ export interface ButtonProps { */ type?: BUTTON_TYPES; } + +export interface InputProps { + + /** + * Whether the input is be clearable. (show clear button). + */ + clearable?: boolean; + + /** + * Whether the input is be disabled. + */ + disabled?: boolean; + + /** + * Whether the input is in error state. + */ + error?: boolean; + + /** + * The icon to be displayed on the input. + */ + icon?: Function; + + /** + * The label of the input. + */ + label?: string; + + /** + * Change callback. + */ + onChange: (value: string) => void; + + /** + * The input placeholder text. + */ + placeholder?: string; + + /** + * The value of the input. + */ + value: string | number; +} diff --git a/react/features/base/environment/utils.js b/react/features/base/environment/utils.ts similarity index 74% rename from react/features/base/environment/utils.js rename to react/features/base/environment/utils.ts index 5258675913..b88c5f2395 100644 --- a/react/features/base/environment/utils.js +++ b/react/features/base/environment/utils.ts @@ -1,5 +1,4 @@ -// @flow - +// @ts-ignore import Platform from '../react/Platform'; /** @@ -28,8 +27,8 @@ export function isIosMobileBrowser() { * * @returns {Promise[]} */ -export function checkChromeExtensionsInstalled(config: Object = {}) { - const isExtensionInstalled = info => new Promise(resolve => { +export function checkChromeExtensionsInstalled(config: any = {}) { + const isExtensionInstalled = (info: any) => new Promise(resolve => { const img = new Image(); img.src = `chrome-extension://${info.id}/${info.path}`; @@ -41,9 +40,9 @@ export function checkChromeExtensionsInstalled(config: Object = {}) { resolve(false); }; }); - const extensionInstalledFunction = info => isExtensionInstalled(info); + const extensionInstalledFunction = (info: any) => isExtensionInstalled(info); return Promise.all( - (config.chromeExtensionsInfo || []).map(info => extensionInstalledFunction(info)) + (config.chromeExtensionsInfo || []).map((info: any) => extensionInstalledFunction(info)) ); } diff --git a/react/features/base/react/components/native/Input.tsx b/react/features/base/react/components/native/Input.tsx new file mode 100644 index 0000000000..0d58954b6b --- /dev/null +++ b/react/features/base/react/components/native/Input.tsx @@ -0,0 +1,102 @@ +import React, { useCallback, useState } from 'react'; +import { + NativeSyntheticEvent, + StyleProp, + Text, + TextInput, + TextInputChangeEventData, + TouchableOpacity, + View, + ViewStyle +} from 'react-native'; + +import { InputProps } from '../../../components/common/types'; +import Icon from '../../../icons/components/Icon'; +import { IconCloseCircle } from '../../../icons/svg'; +// eslint-disable-next-line lines-around-comment +// @ts-ignore +import BaseTheme from '../../../ui/components/BaseTheme.native'; + +import styles from './inputStyles'; + +interface IInputProps extends InputProps { + + /** + * Custom styles to be applied to the component. + */ + customStyles?: CustomStyles; +} + +interface CustomStyles { + container?: Object; + input?: Object; +} + +const Input = ({ + clearable, + customStyles, + disabled, + error, + icon, + label, + onChange, + placeholder, + value +}: IInputProps) => { + const [ focused, setFocused ] = useState(false); + const handleChange = useCallback((e: NativeSyntheticEvent) => { + const { nativeEvent: { text } } = e; + + onChange(text); + }, []); + + const clearInput = useCallback(() => { + onChange(''); + }, []); + + const blur = useCallback(() => { + setFocused(false); + }, []); + + const focus = useCallback(() => { + setFocused(true); + }, []); + + return ( + {label && {label}} + }> + {icon && } + + {clearable && !disabled && value !== '' && ( + }> + + + )} + + ); +}; + +export default Input; diff --git a/react/features/base/react/components/native/inputStyles.ts b/react/features/base/react/components/native/inputStyles.ts new file mode 100644 index 0000000000..ff69eca05d --- /dev/null +++ b/react/features/base/react/components/native/inputStyles.ts @@ -0,0 +1,74 @@ +// @ts-ignore +import BaseTheme from '../../../ui/components/BaseTheme.native'; + +export default { + inputContainer: { + display: 'flex', + flexDirection: 'column' + }, + + label: { + ...BaseTheme.typography.bodyShortRegularLarge, + lineHeight: 0, + color: BaseTheme.palette.text01, + marginBottom: 8 + }, + + fieldContainer: { + position: 'relative' + }, + + icon: { + position: 'absolute', + zIndex: 1, + top: 13, + left: 16 + }, + + input: { + backgroundColor: BaseTheme.palette.ui03, + color: BaseTheme.palette.text01, + paddingVertical: 13, + paddingHorizontal: BaseTheme.spacing[3], + borderRadius: BaseTheme.shape.borderRadius, + ...BaseTheme.typography.bodyShortRegularLarge, + lineHeight: 0, + height: 48, + borderWidth: 2, + borderColor: BaseTheme.palette.ui03 + }, + + inputDisabled: { + color: BaseTheme.palette.text03 + }, + + inputFocused: { + borderColor: BaseTheme.palette.focus01 + }, + + inputError: { + borderColor: BaseTheme.palette.textError + }, + + iconInput: { + paddingLeft: BaseTheme.spacing[6] + }, + + clearableInput: { + paddingRight: BaseTheme.spacing[6] + }, + + clearButton: { + backgroundColor: 'transparent', + borderWidth: 0, + position: 'absolute', + right: 0, + top: 13, + width: 40, + height: 48 + }, + + clearIcon: { + color: BaseTheme.palette.icon01 + } +}; diff --git a/react/features/participants-pane/components/native/MeetingParticipantList.js b/react/features/participants-pane/components/native/MeetingParticipantList.js index 70a8f6bbba..811df77336 100644 --- a/react/features/participants-pane/components/native/MeetingParticipantList.js +++ b/react/features/participants-pane/components/native/MeetingParticipantList.js @@ -8,14 +8,13 @@ import { translate } from '../../../base/i18n'; import { Icon, IconInviteMore } from '../../../base/icons'; import { getLocalParticipant, getParticipantCountWithFake, getRemoteParticipants } from '../../../base/participants'; import Button from '../../../base/react/components/native/Button'; +import Input from '../../../base/react/components/native/Input'; import { BUTTON_TYPES } from '../../../base/react/constants'; import { connect } from '../../../base/redux'; -import BaseTheme from '../../../base/ui/components/BaseTheme.native'; import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions'; import { doInvitePeople } from '../../../invite/actions.native'; import { participantMatchesSearch, shouldRenderInviteButton } from '../../functions'; -import ClearableInput from './ClearableInput'; import CollapsibleList from './CollapsibleList'; import MeetingParticipantItem from './MeetingParticipantItem'; import styles from './styles'; @@ -235,10 +234,13 @@ class MeetingParticipantList extends PureComponent { style = { styles.inviteButton } type = { BUTTON_TYPES.PRIMARY } /> } - + value = { this.props.searchString } /> { - return { - clearableInput: { - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-start', - height: '20px', - border: `1px solid ${theme.palette.ui05}`, - backgroundColor: theme.palette.uiBackground, - position: 'relative', - borderRadius: '6px', - padding: '10px 16px', - - '&.focused': { - outline: `3px solid ${theme.palette.field01Focus}` - } - }, - clearButton: { - backgroundColor: 'transparent', - border: 0, - position: 'absolute', - right: '10px', - top: '11px', - padding: 0, - - '& svg': { - fill: theme.palette.icon02 - } - }, - input: { - backgroundColor: 'transparent', - border: 0, - width: '100%', - height: '100%', - borderRadius: '6px', - fontSize: '14px', - lineHeight: '20px', - textAlign: 'center', - caretColor: theme.palette.text01, - color: theme.palette.text01, - - '&::placeholder': { - color: theme.palette.text03 - } - } - }; -}); - -/** - * Implements a pre-styled clearable input field. - * - * @param {Props} props - The props of the component. - * @returns {ReactElement} - */ -function ClearableInput({ - autoFocus = false, - autoComplete, - className = '', - id, - onChange, - onSubmit, - placeholder, - testId, - type = 'text', - value -}: Props) { - const classes = useStyles(); - const [ val, setVal ] = useState(value || ''); - const [ focused, setFocused ] = useState(false); - const inputRef = React.createRef(); - - useEffect(() => { - if (value && value !== val) { - setVal(value); - } - }, [ value ]); - - - /** - * Callback for the onBlur event of the field. - * - * @returns {void} - */ - const _onBlur = useCallback(() => { - setFocused(false); - }); - - /** - * Callback for the onChange event of the field. - * - * @param {Object} evt - The static event. - * @returns {void} - */ - const _onChange = useCallback(evt => { - const newValue = evt.target.value; - - setVal(newValue); - onChange && onChange(newValue); - }, [ onChange ]); - - /** - * Callback for the onFocus event of the field. - * - * @returns {void} - */ - const _onFocus = useCallback(() => { - setFocused(true); - }); - - /** - * Joins the conference on 'Enter'. - * - * @param {Event} event - Key down event object. - * @returns {void} - */ - const _onKeyDown = useCallback(event => { - onSubmit && event.key === 'Enter' && onSubmit(); - }, [ onSubmit ]); - - /** - * Clears the input. - * - * @returns {void} - */ - const _clearInput = useCallback(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - setVal(''); - onChange && onChange(''); - }, [ onChange ]); - - return ( -
- - {val !== '' && ( - - )} -
- ); -} - -export default ClearableInput; diff --git a/react/features/participants-pane/components/web/MeetingParticipants.js b/react/features/participants-pane/components/web/MeetingParticipants.js index 34873292c6..49d00c1ec6 100644 --- a/react/features/participants-pane/components/web/MeetingParticipants.js +++ b/react/features/participants-pane/components/web/MeetingParticipants.js @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { rejectParticipantAudio } from '../../../av-moderation/actions'; +import Input from '../../../base/components/common/Input'; import useContextMenu from '../../../base/components/context-menu/useContextMenu'; import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json'; import { isToolbarButtonEnabled } from '../../../base/config/functions.web'; @@ -22,7 +23,6 @@ import { muteRemote } from '../../../video-menu/actions.any'; import { getSortedParticipantIds, shouldRenderInviteButton } from '../../functions'; import { useParticipantDrawer } from '../../hooks'; -import ClearableInput from './ClearableInput'; import { InviteButton } from './InviteButton'; import MeetingParticipantContextMenu from './MeetingParticipantContextMenu'; import MeetingParticipantItems from './MeetingParticipantItems'; @@ -39,6 +39,13 @@ const useStyles = makeStyles(theme => { ...theme.typography.labelButtonLarge, lineHeight: `${theme.typography.labelButtonLarge.lineHeight}px` } + }, + + search: { + '& input': { + textAlign: 'center', + paddingRight: '16px' + } } }; }); @@ -107,7 +114,9 @@ function MeetingParticipants({ : t('participantsPane.headings.participantsList', { count: participantsCount })} {showInviteButton && } -