ref(ui-components) Use new input and button in chat (#12000)

This commit is contained in:
Robert Pintilii 2022-08-11 16:32:44 +03:00 committed by GitHub
parent ea4be6cb30
commit 0435c3cc64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 119 additions and 223 deletions

View File

@ -119,63 +119,18 @@
.chat-input-container {
padding: 0 16px 16px;
&.populated {
#chat-input {
.send-button {
background: #1B67EC;
cursor: pointer;
margin-left: 0.3rem;
@media (hover: hover) and (pointer: fine) {
&:hover {
background: #3D82FB;
}
}
&:active {
background: #0852D4;
}
path {
fill: #fff;
}
}
}
}
}
#chat-input {
border: 1px solid $chatInputSeparatorColor;
display: flex;
align-items: flex-end;
padding: 4px;
border-radius: 3px;
&:focus-within {
border: 1px solid #619CF4;
}
* {
background-color: transparent;
}
position: relative;
}
.send-button-container {
display: flex;
align-items: center;
}
.send-button {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
width: 40px;
border-radius: 3px;
path {
fill: $chatInputSeparatorColor;
}
.chat-input {
flex: 1;
margin-right: 8px;
}
.smiley-button {
@ -395,7 +350,9 @@
.smiley-input {
display: flex;
position: relative;
position: absolute;
top: 0;
left: 0;
}
.smileys-panel {
@ -403,7 +360,7 @@
box-sizing: border-box;
background-color: rgba(0, 0, 0, .6) !important;
height: auto;
display: none;
display: flex;
overflow: hidden;
position: absolute;
width: calc(#{$sidebarWidth} - 32px);
@ -418,11 +375,6 @@
*/
transition: max-height 0.3s;
&.show-smileys {
display: flex;
max-height: 500%;
}
#smileysContainer {
background-color: $chatBackgroundColor;
border-top: 1px solid $chatInputSeparatorColor;

View File

@ -1,6 +1,7 @@
import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import React, { useCallback } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { isMobileBrowser } from '../../../environment/utils';
import Icon from '../../../icons/components/Icon';
@ -14,10 +15,14 @@ interface IInputProps extends InputProps {
autoFocus?: boolean;
bottomLabel?: string;
className?: string;
iconClick?: () => void;
id?: string;
maxLength?: number;
maxRows?: number;
minRows?: number;
name?: string;
onKeyPress?: (e: React.KeyboardEvent) => void;
textarea?: boolean;
type?: 'text' | 'email' | 'number' | 'password';
}
@ -73,6 +78,10 @@ const useStyles = makeStyles((theme: Theme) => {
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
},
'&.icon-input': {
paddingLeft: '46px'
},
'&.error': {
boxShadow: `0px 0px 0px 2px ${theme.palette.textError}`
}
@ -80,12 +89,13 @@ const useStyles = makeStyles((theme: Theme) => {
icon: {
position: 'absolute',
top: '10px',
top: '50%',
transform: 'translateY(-50%)',
left: '16px'
},
iconInput: {
paddingLeft: '46px'
iconClickable: {
cursor: 'pointer'
},
clearableInput: {
@ -118,7 +128,7 @@ const useStyles = makeStyles((theme: Theme) => {
};
});
const Input = ({
const Input = React.forwardRef<any, IInputProps>(({
accessibilityLabel,
autoFocus,
bottomLabel,
@ -127,20 +137,24 @@ const Input = ({
disabled,
error,
icon,
iconClick,
id,
label,
maxLength,
maxRows,
minRows,
name,
onChange,
onKeyPress,
placeholder,
textarea = false,
type = 'text',
value
}: IInputProps) => {
}: IInputProps, ref) => {
const styles = useStyles();
const isMobile = isMobileBrowser();
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) =>
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) =>
onChange(e.target.value), []);
const clearInput = useCallback(() => onChange(''), []);
@ -149,23 +163,44 @@ const Input = ({
{label && <span className = { clsx(styles.label, isMobile && 'is-mobile') }>{label}</span>}
<div className = { styles.fieldContainer }>
{icon && <Icon
className = { styles.icon }
{ ...(iconClick ? { tabIndex: 0 } : {}) }
className = { clsx(styles.icon, iconClick && styles.iconClickable) }
onClick = { iconClick }
size = { 20 }
src = { icon } />}
<input
aria-label = { accessibilityLabel }
autoFocus = { autoFocus }
className = { clsx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && styles.iconInput) }
disabled = { disabled }
{ ...(id ? { id } : {}) }
maxLength = { maxLength }
name = { name }
onChange = { handleChange }
onKeyPress = { onKeyPress }
placeholder = { placeholder }
type = { type }
value = { value } />
{textarea ? (
<TextareaAutosize
aria-label = { accessibilityLabel }
autoFocus = { autoFocus }
className = { clsx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
disabled = { disabled }
{ ...(id ? { id } : {}) }
maxRows = { maxRows }
minRows = { minRows }
name = { name }
onChange = { handleChange }
onKeyPress = { onKeyPress }
placeholder = { placeholder }
ref = { ref }
value = { value } />
) : (
<input
aria-label = { accessibilityLabel }
autoFocus = { autoFocus }
className = { clsx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
disabled = { disabled }
{ ...(id ? { id } : {}) }
maxLength = { maxLength }
name = { name }
onChange = { handleChange }
onKeyPress = { onKeyPress }
placeholder = { placeholder }
ref = { ref }
type = { type }
value = { value } />
)}
{clearable && !disabled && value !== '' && <button className = { styles.clearButton }>
<Icon
onClick = { clearInput }
@ -179,6 +214,6 @@ const Input = ({
</span>
)}
</div>);
};
});
export default Input;

View File

@ -1,21 +1,30 @@
// @flow
/* eslint-disable lines-around-comment */
import React, { Component } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { WithTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
import { IState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { Icon, IconPlane, IconSmile } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { translate } from '../../../base/i18n/functions';
import { IconPlane, IconSmile } from '../../../base/icons/svg/index';
import { connect } from '../../../base/redux/functions';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
// @ts-ignore
import { areSmileysDisabled } from '../../functions';
// @ts-ignore
import SmileysPanel from './SmileysPanel';
/**
* The type of the React {@code Component} props of {@link ChatInput}.
*/
type Props = {
interface Props extends WithTranslation {
/**
* Whether chat emoticons are disabled.
*/
_areSmileysDisabled: boolean,
/**
* Invoked to send chat messages.
@ -26,24 +35,14 @@ type Props = {
* Optional callback to invoke when the chat textarea has auto-resized to
* fit overflowing text.
*/
onResize: ?Function,
onResize?: Function,
/**
* Callback to invoke on message send.
*/
onSend: Function,
onSend: Function
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* Whether chat emoticons are disabled.
*/
_areSmileysDisabled: boolean
};
}
/**
* The type of the React {@code Component} state of {@link ChatInput}.
@ -67,7 +66,7 @@ type State = {
* @augments Component
*/
class ChatInput extends Component<Props, State> {
_textArea: ?HTMLTextAreaElement;
_textArea?: HTMLTextAreaElement;
state = {
message: '',
@ -83,17 +82,14 @@ class ChatInput extends Component<Props, State> {
constructor(props: Props) {
super(props);
this._textArea = null;
this._textArea = undefined;
// Bind event handlers so they are only bound once for every instance.
this._onDetectSubmit = this._onDetectSubmit.bind(this);
this._onMessageChange = this._onMessageChange.bind(this);
this._onSmileySelect = this._onSmileySelect.bind(this);
this._onSubmitMessage = this._onSubmitMessage.bind(this);
this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
this._onEscHandler = this._onEscHandler.bind(this);
this._onToggleSmileysPanelKeyPress = this._onToggleSmileysPanelKeyPress.bind(this);
this._onSubmitMessageKeyPress = this._onSubmitMessageKeyPress.bind(this);
this._toggleSmileysPanel = this._toggleSmileysPanel.bind(this);
this._setTextAreaRef = this._setTextAreaRef.bind(this);
}
@ -116,62 +112,37 @@ class ChatInput extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const smileysPanelClassName = `${this.state.showSmileysPanel
? 'show-smileys' : 'hide-smileys'} smileys-panel`;
return (
<div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
<div id = 'chat-input' >
{ this.props._areSmileysDisabled ? null : (
<div className = 'smiley-input'>
<div id = 'smileysarea'>
<div id = 'smileys'>
<div
aria-expanded = { this.state.showSmileysPanel }
aria-haspopup = 'smileysContainer'
aria-label = { this.props.t('chat.smileysPanel') }
className = 'smiley-button'
onClick = { this._onToggleSmileysPanel }
onKeyDown = { this._onEscHandler }
onKeyPress = { this._onToggleSmileysPanelKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon src = { IconSmile } />
</div>
</div>
</div>
{!this.props._areSmileysDisabled && this.state.showSmileysPanel && (
<div
className = 'smiley-input'>
<div
className = { smileysPanelClassName } >
className = 'smileys-panel' >
<SmileysPanel
onSmileySelect = { this._onSmileySelect } />
</div>
</div>
) }
<div className = 'usrmsg-form'>
<TextareaAutosize
autoComplete = 'off'
autoFocus = { true }
id = 'usermsg'
maxRows = { 5 }
onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
ref = { this._setTextAreaRef }
tabIndex = { 0 }
value = { this.state.message } />
</div>
<div className = 'send-button-container'>
<div
aria-label = { this.props.t('chat.sendButton') }
className = 'send-button'
onClick = { this._onSubmitMessage }
onKeyPress = { this._onSubmitMessageKeyPress }
role = 'button'
tabIndex = { this.state.message.trim() ? 0 : -1 } >
<Icon src = { IconPlane } />
</div>
</div>
)}
<Input
autoFocus = { true }
className = 'chat-input'
icon = { this.props._areSmileysDisabled ? undefined : IconSmile }
iconClick = { this._toggleSmileysPanel }
maxRows = { 5 }
onChange = { this._onMessageChange }
onKeyPress = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
ref = { this._setTextAreaRef }
textarea = { true }
value = { this.state.message } />
<Button
accessibilityLabel = { this.props.t('chat.sendButton') }
disabled = { !this.state.message.trim() }
icon = { IconPlane }
onClick = { this._onSubmitMessage }
size = { isMobileBrowser() ? 'large' : 'medium' } />
</div>
</div>
);
@ -187,9 +158,6 @@ class ChatInput extends Component<Props, State> {
this._textArea && this._textArea.focus();
}
_onSubmitMessage: () => void;
/**
* Submits the message to the chat window.
*
@ -208,7 +176,6 @@ class ChatInput extends Component<Props, State> {
}
}
_onDetectSubmit: (Object) => void;
/**
* Detects if enter has been pressed. If so, submit the message in the chat
@ -218,7 +185,7 @@ class ChatInput extends Component<Props, State> {
* @private
* @returns {void}
*/
_onDetectSubmit(event) {
_onDetectSubmit(event: any) {
// Composition events used to add accents to characters
// despite their absence from standard US keyboards,
// to build up logograms of many Asian languages
@ -241,37 +208,17 @@ class ChatInput extends Component<Props, State> {
}
}
_onSubmitMessageKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onSubmitMessageKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onSubmitMessage();
}
}
_onMessageChange: (Object) => void;
/**
* Updates the known message the user is drafting.
*
* @param {string} event - Keyboard event.
* @param {string} value - Keyboard event.
* @private
* @returns {void}
*/
_onMessageChange(event) {
this.setState({ message: event.target.value });
_onMessageChange(value: string) {
this.setState({ message: value });
}
_onSmileySelect: (string) => void;
/**
* Appends a selected smileys to the chat message draft.
*
@ -280,7 +227,7 @@ class ChatInput extends Component<Props, State> {
* @private
* @returns {void}
*/
_onSmileySelect(smileyText) {
_onSmileySelect(smileyText: string) {
if (smileyText) {
this.setState({
message: `${this.state.message} ${smileyText}`,
@ -295,57 +242,19 @@ class ChatInput extends Component<Props, State> {
this._focus();
}
_onToggleSmileysPanel: () => void;
/**
* Callback invoked to hide or show the smileys selector.
*
* @private
* @returns {void}
*/
_onToggleSmileysPanel() {
_toggleSmileysPanel() {
if (this.state.showSmileysPanel) {
this._focus();
}
this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
}
_onEscHandler: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onEscHandler(e) {
// Escape handling does not work in onKeyPress
if (this.state.showSmileysPanel && e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
this._onToggleSmileysPanel();
}
}
_onToggleSmileysPanelKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onToggleSmileysPanelKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onToggleSmileysPanel();
}
}
_setTextAreaRef: (?HTMLTextAreaElement) => void;
/**
* Sets the reference to the HTML TextArea.
*
@ -353,7 +262,7 @@ class ChatInput extends Component<Props, State> {
* @private
* @returns {void}
*/
_setTextAreaRef(textAreaElement: ?HTMLTextAreaElement) {
_setTextAreaRef(textAreaElement?: HTMLTextAreaElement) {
this._textArea = textAreaElement;
}
}
@ -367,7 +276,7 @@ class ChatInput extends Component<Props, State> {
* _areSmileysDisabled: boolean
* }}
*/
const mapStateToProps = state => {
const mapStateToProps = (state: IState) => {
return {
_areSmileysDisabled: areSmileysDisabled(state)
};