feat(ui-components) Add Input Component (#11882)

This commit is contained in:
Robert Pintilii 2022-07-26 13:58:28 +03:00 committed by GitHub
parent 0a385c561d
commit c5115f99f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 421 additions and 235 deletions

View File

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

View File

@ -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<HTMLInputElement>) =>
onChange(e.target.value), []);
const clearInput = useCallback(() => onChange(''), []);
return (<div className = { clsx(styles.inputContainer, className) }>
{label && <span className = { clsx(styles.label, isMobile && 'is-mobile') }>{label}</span>}
<div className = { styles.fieldContainer }>
{icon && <Icon
className = { styles.icon }
size = { 20 }
src = { icon } />}
<input
className = { clsx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && styles.iconInput) }
disabled = { disabled }
onChange = { handleChange }
placeholder = { placeholder }
type = { type }
value = { value } />
{clearable && !disabled && value !== '' && <button className = { styles.clearButton }>
<Icon
onClick = { clearInput }
size = { 20 }
src = { IconCloseCircle } />
</button>}
</div>
{bottomLabel && (
<span className = { clsx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
{bottomLabel}
</span>
)}
</div>);
};
export default Input;

View File

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

View File

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

View File

@ -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<TextInputChangeEventData>) => {
const { nativeEvent: { text } } = e;
onChange(text);
}, []);
const clearInput = useCallback(() => {
onChange('');
}, []);
const blur = useCallback(() => {
setFocused(false);
}, []);
const focus = useCallback(() => {
setFocused(true);
}, []);
return (<View style = { [ styles.inputContainer, customStyles?.container ] }>
{label && <Text style = { styles.label }>{label}</Text>}
<View style = { styles.fieldContainer as StyleProp<ViewStyle> }>
{icon && <Icon
size = { 22 }
src = { icon }
style = { styles.icon } />}
<TextInput
editable = { !disabled }
onBlur = { blur }
onChange = { handleChange }
onFocus = { focus }
placeholder = { placeholder }
placeholderTextColor = { BaseTheme.palette.text02 }
style = { [ styles.input,
disabled && styles.inputDisabled,
clearable && styles.clearableInput,
icon && styles.iconInput,
focused && styles.inputFocused,
error && styles.inputError,
customStyles?.input
] }
value = { `${value}` } />
{clearable && !disabled && value !== '' && (
<TouchableOpacity
onPress = { clearInput }
style = { styles.clearButton as StyleProp<ViewStyle> }>
<Icon
size = { 22 }
src = { IconCloseCircle }
style = { styles.clearIcon } />
</TouchableOpacity>
)}
</View>
</View>);
};
export default Input;

View File

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

View File

@ -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<Props> {
style = { styles.inviteButton }
type = { BUTTON_TYPES.PRIMARY } />
}
<ClearableInput
<Input
clearable = { true }
customStyles = {{ container: styles.inputContainer,
input: styles.centerInput }}
onChange = { this._onSearchStringChange }
placeholder = { t('participantsPane.search') }
selectionColor = { BaseTheme.palette.text01 } />
value = { this.props.searchString } />
<FlatList
bounces = { false }
data = { [ _localParticipant?.id, ..._sortedRemoteParticipants ] }

View File

@ -310,5 +310,16 @@ export default {
paddingLeft: BaseTheme.spacing[3],
paddingRight: BaseTheme.spacing[3],
fontSize: 16
},
inputContainer: {
marginLeft: BaseTheme.spacing[3],
marginRight: BaseTheme.spacing[3],
marginBottom: BaseTheme.spacing[4]
},
centerInput: {
paddingRight: BaseTheme.spacing[3],
textAlign: 'center'
}
};

View File

@ -1,222 +0,0 @@
// @flow
import { makeStyles } from '@material-ui/core';
import React, { useCallback, useEffect, useState } from 'react';
import { Icon, IconCloseSolid } from '../../../base/icons';
type Props = {
/**
* String for html autocomplete attribute.
*/
autoComplete?: string,
/**
* If the input should be focused on display.
*/
autoFocus?: boolean,
/**
* Class name to be appended to the default class list.
*/
className?: string,
/**
* Input id.
*/
id?: string,
/**
* Callback for the onChange event of the field.
*/
onChange: Function,
/**
* Callback to be used when the user hits Enter in the field.
*/
onSubmit?: Function,
/**
* Placeholder text for the field.
*/
placeholder: string,
/**
* The field type (e.g. Text, password...etc).
*/
type?: string,
/**
* TestId of the button. Can be used to locate element when testing UI.
*/
testId?: string,
/**
* Externally provided value.
*/
value?: string
};
const useStyles = makeStyles(theme => {
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 (
<div className = { `${classes.clearableInput} ${focused ? 'focused' : ''} ${className || ''}` }>
<input
autoComplete = { autoComplete }
autoFocus = { autoFocus }
className = { classes.input }
data-testid = { testId ? testId : undefined }
id = { id }
onBlur = { _onBlur }
onChange = { _onChange }
onFocus = { _onFocus }
onKeyDown = { _onKeyDown }
placeholder = { placeholder }
ref = { inputRef }
type = { type }
value = { val } />
{val !== '' && (
<button
className = { classes.clearButton }
onClick = { _clearInput }>
<Icon
size = { 20 }
src = { IconCloseSolid } />
</button>
)}
</div>
);
}
export default ClearableInput;

View File

@ -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 })}
</div>
{showInviteButton && <InviteButton />}
<ClearableInput
<Input
className = { styles.search }
clearable = { true }
onChange = { setSearchString }
placeholder = { t('participantsPane.search') }
value = { searchString } />