feat(ui-components) Add Input Component (#11882)
This commit is contained in:
parent
0a385c561d
commit
c5115f99f0
|
@ -5,6 +5,7 @@ import React from 'react';
|
||||||
import Icon from '../../icons/components/Icon';
|
import Icon from '../../icons/components/Icon';
|
||||||
import { BUTTON_TYPES } from '../../react/constants';
|
import { BUTTON_TYPES } from '../../react/constants';
|
||||||
import { withPixelLineHeight } from '../../styles/functions.web';
|
import { withPixelLineHeight } from '../../styles/functions.web';
|
||||||
|
import { Theme } from '../../ui/types';
|
||||||
|
|
||||||
import { ButtonProps } from './types';
|
import { ButtonProps } from './types';
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ interface IButtonProps extends ButtonProps {
|
||||||
size?: 'small' | 'medium' | 'large';
|
size?: 'small' | 'medium' | 'large';
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: any) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
return {
|
return {
|
||||||
button: {
|
button: {
|
||||||
backgroundColor: theme.palette.action01,
|
backgroundColor: theme.palette.action01,
|
||||||
|
|
|
@ -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;
|
|
@ -27,3 +27,46 @@ export interface ButtonProps {
|
||||||
*/
|
*/
|
||||||
type?: BUTTON_TYPES;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// @flow
|
// @ts-ignore
|
||||||
|
|
||||||
import Platform from '../react/Platform';
|
import Platform from '../react/Platform';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,8 +27,8 @@ export function isIosMobileBrowser() {
|
||||||
*
|
*
|
||||||
* @returns {Promise[]}
|
* @returns {Promise[]}
|
||||||
*/
|
*/
|
||||||
export function checkChromeExtensionsInstalled(config: Object = {}) {
|
export function checkChromeExtensionsInstalled(config: any = {}) {
|
||||||
const isExtensionInstalled = info => new Promise(resolve => {
|
const isExtensionInstalled = (info: any) => new Promise(resolve => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|
||||||
img.src = `chrome-extension://${info.id}/${info.path}`;
|
img.src = `chrome-extension://${info.id}/${info.path}`;
|
||||||
|
@ -41,9 +40,9 @@ export function checkChromeExtensionsInstalled(config: Object = {}) {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const extensionInstalledFunction = info => isExtensionInstalled(info);
|
const extensionInstalledFunction = (info: any) => isExtensionInstalled(info);
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
(config.chromeExtensionsInfo || []).map(info => extensionInstalledFunction(info))
|
(config.chromeExtensionsInfo || []).map((info: any) => extensionInstalledFunction(info))
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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;
|
|
@ -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
|
||||||
|
}
|
||||||
|
};
|
|
@ -8,14 +8,13 @@ import { translate } from '../../../base/i18n';
|
||||||
import { Icon, IconInviteMore } from '../../../base/icons';
|
import { Icon, IconInviteMore } from '../../../base/icons';
|
||||||
import { getLocalParticipant, getParticipantCountWithFake, getRemoteParticipants } from '../../../base/participants';
|
import { getLocalParticipant, getParticipantCountWithFake, getRemoteParticipants } from '../../../base/participants';
|
||||||
import Button from '../../../base/react/components/native/Button';
|
import Button from '../../../base/react/components/native/Button';
|
||||||
|
import Input from '../../../base/react/components/native/Input';
|
||||||
import { BUTTON_TYPES } from '../../../base/react/constants';
|
import { BUTTON_TYPES } from '../../../base/react/constants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
|
||||||
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
|
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
|
||||||
import { doInvitePeople } from '../../../invite/actions.native';
|
import { doInvitePeople } from '../../../invite/actions.native';
|
||||||
import { participantMatchesSearch, shouldRenderInviteButton } from '../../functions';
|
import { participantMatchesSearch, shouldRenderInviteButton } from '../../functions';
|
||||||
|
|
||||||
import ClearableInput from './ClearableInput';
|
|
||||||
import CollapsibleList from './CollapsibleList';
|
import CollapsibleList from './CollapsibleList';
|
||||||
import MeetingParticipantItem from './MeetingParticipantItem';
|
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
@ -235,10 +234,13 @@ class MeetingParticipantList extends PureComponent<Props> {
|
||||||
style = { styles.inviteButton }
|
style = { styles.inviteButton }
|
||||||
type = { BUTTON_TYPES.PRIMARY } />
|
type = { BUTTON_TYPES.PRIMARY } />
|
||||||
}
|
}
|
||||||
<ClearableInput
|
<Input
|
||||||
|
clearable = { true }
|
||||||
|
customStyles = {{ container: styles.inputContainer,
|
||||||
|
input: styles.centerInput }}
|
||||||
onChange = { this._onSearchStringChange }
|
onChange = { this._onSearchStringChange }
|
||||||
placeholder = { t('participantsPane.search') }
|
placeholder = { t('participantsPane.search') }
|
||||||
selectionColor = { BaseTheme.palette.text01 } />
|
value = { this.props.searchString } />
|
||||||
<FlatList
|
<FlatList
|
||||||
bounces = { false }
|
bounces = { false }
|
||||||
data = { [ _localParticipant?.id, ..._sortedRemoteParticipants ] }
|
data = { [ _localParticipant?.id, ..._sortedRemoteParticipants ] }
|
||||||
|
|
|
@ -310,5 +310,16 @@ export default {
|
||||||
paddingLeft: BaseTheme.spacing[3],
|
paddingLeft: BaseTheme.spacing[3],
|
||||||
paddingRight: BaseTheme.spacing[3],
|
paddingRight: BaseTheme.spacing[3],
|
||||||
fontSize: 16
|
fontSize: 16
|
||||||
|
},
|
||||||
|
|
||||||
|
inputContainer: {
|
||||||
|
marginLeft: BaseTheme.spacing[3],
|
||||||
|
marginRight: BaseTheme.spacing[3],
|
||||||
|
marginBottom: BaseTheme.spacing[4]
|
||||||
|
},
|
||||||
|
|
||||||
|
centerInput: {
|
||||||
|
paddingRight: BaseTheme.spacing[3],
|
||||||
|
textAlign: 'center'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
|
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { rejectParticipantAudio } from '../../../av-moderation/actions';
|
import { rejectParticipantAudio } from '../../../av-moderation/actions';
|
||||||
|
import Input from '../../../base/components/common/Input';
|
||||||
import useContextMenu from '../../../base/components/context-menu/useContextMenu';
|
import useContextMenu from '../../../base/components/context-menu/useContextMenu';
|
||||||
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
|
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
|
||||||
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
||||||
|
@ -22,7 +23,6 @@ import { muteRemote } from '../../../video-menu/actions.any';
|
||||||
import { getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
|
import { getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
|
||||||
import { useParticipantDrawer } from '../../hooks';
|
import { useParticipantDrawer } from '../../hooks';
|
||||||
|
|
||||||
import ClearableInput from './ClearableInput';
|
|
||||||
import { InviteButton } from './InviteButton';
|
import { InviteButton } from './InviteButton';
|
||||||
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
||||||
import MeetingParticipantItems from './MeetingParticipantItems';
|
import MeetingParticipantItems from './MeetingParticipantItems';
|
||||||
|
@ -39,6 +39,13 @@ const useStyles = makeStyles(theme => {
|
||||||
...theme.typography.labelButtonLarge,
|
...theme.typography.labelButtonLarge,
|
||||||
lineHeight: `${theme.typography.labelButtonLarge.lineHeight}px`
|
lineHeight: `${theme.typography.labelButtonLarge.lineHeight}px`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
search: {
|
||||||
|
'& input': {
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingRight: '16px'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -107,7 +114,9 @@ function MeetingParticipants({
|
||||||
: t('participantsPane.headings.participantsList', { count: participantsCount })}
|
: t('participantsPane.headings.participantsList', { count: participantsCount })}
|
||||||
</div>
|
</div>
|
||||||
{showInviteButton && <InviteButton />}
|
{showInviteButton && <InviteButton />}
|
||||||
<ClearableInput
|
<Input
|
||||||
|
className = { styles.search }
|
||||||
|
clearable = { true }
|
||||||
onChange = { setSearchString }
|
onChange = { setSearchString }
|
||||||
placeholder = { t('participantsPane.search') }
|
placeholder = { t('participantsPane.search') }
|
||||||
value = { searchString } />
|
value = { searchString } />
|
||||||
|
|
Loading…
Reference in New Issue