Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename TextInputFocusable to Composer ⌨️ & add Stories For TextInputs and fix bugs #7984

Merged
merged 17 commits into from
Mar 11, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import Onyx from 'react-native-onyx';
import '../assets/css/fonts.css';
import ComposeProviders from '../src/components/ComposeProviders';
import HTMLEngineProvider from '../src/components/HTMLEngineProvider';
import OnyxProvider from '../src/components/OnyxProvider';
import {LocaleContextProvider} from '../src/components/withLocalize';
import ONYXKEYS from '../src/ONYXKEYS';
Expand All @@ -16,6 +17,7 @@ const decorators = [
components={[
OnyxProvider,
LocaleContextProvider,
HTMLEngineProvider,
]}
>
<Story />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const defaultProps = {
forwardedRef: null,
};

class TextInputFocusable extends React.Component {
class Composer extends React.Component {
componentDidMount() {
// This callback prop is used by the parent component using the constructor to
// get a ref to the inner textInput element e.g. if we do
Expand Down Expand Up @@ -76,11 +76,11 @@ class TextInputFocusable extends React.Component {
}
}

TextInputFocusable.displayName = 'TextInputFocusable';
TextInputFocusable.propTypes = propTypes;
TextInputFocusable.defaultProps = defaultProps;
Composer.displayName = 'TextInputFocusable';
Composer.propTypes = propTypes;
Composer.defaultProps = defaultProps;

export default React.forwardRef((props, ref) => (
/* eslint-disable-next-line react/jsx-props-no-spreading */
<TextInputFocusable {...props} forwardedRef={ref} />
<Composer {...props} forwardedRef={ref} />
));
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const defaultProps = {
},
};

class TextInputFocusable extends React.Component {
class Composer extends React.Component {
componentDidMount() {
// This callback prop is used by the parent component using the constructor to
// get a ref to the inner textInput element e.g. if we do
Expand Down Expand Up @@ -88,10 +88,10 @@ class TextInputFocusable extends React.Component {
}
}

TextInputFocusable.propTypes = propTypes;
TextInputFocusable.defaultProps = defaultProps;
Composer.propTypes = propTypes;
Composer.defaultProps = defaultProps;

export default React.forwardRef((props, ref) => (
/* eslint-disable-next-line react/jsx-props-no-spreading */
<TextInputFocusable {...props} forwardedRef={ref} />
<Composer {...props} forwardedRef={ref} />
));
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ const IMAGE_EXTENSIONS = {
};

/**
* Enable Markdown parsing.
* On web we like to have the Text Input field always focused so the user can easily type a new chat
*/
class TextInputFocusable extends React.Component {
class Composer extends React.Component {
constructor(props) {
super(props);

Expand Down Expand Up @@ -366,10 +367,10 @@ class TextInputFocusable extends React.Component {
}
}

TextInputFocusable.propTypes = propTypes;
TextInputFocusable.defaultProps = defaultProps;
Composer.propTypes = propTypes;
Composer.defaultProps = defaultProps;

export default withLocalize(React.forwardRef((props, ref) => (
/* eslint-disable-next-line react/jsx-props-no-spreading */
<TextInputFocusable {...props} forwardedRef={ref} />
<Composer {...props} forwardedRef={ref} />
)));
4 changes: 2 additions & 2 deletions src/components/EmojiPicker/EmojiPickerMenu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import themeColors from '../../../styles/themes/default';
import emojis from '../../../../assets/emojis';
import EmojiPickerMenuItem from '../EmojiPickerMenuItem';
import Text from '../../Text';
import TextInputFocusable from '../../TextInputFocusable';
import Composer from '../../Composer';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
import compose from '../../../libs/compose';
Expand Down Expand Up @@ -434,7 +434,7 @@ class EmojiPickerMenu extends Component {
>
{!this.props.isSmallScreenWidth && (
<View style={[styles.pt4, styles.ph4, styles.pb1]}>
<TextInputFocusable
<Composer
textAlignVertical="top"
placeholder={this.props.translate('common.search')}
placeholderTextColor={themeColors.textSupporting}
Expand Down
53 changes: 35 additions & 18 deletions src/components/TextInput/BaseTextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class BaseTextInput extends Component {
constructor(props) {
super(props);

this.value = props.value || props.defaultValue || '';
const activeLabel = props.forceActiveLabel || this.value.length > 0 || props.prefixCharacter;
const value = props.value || props.defaultValue || '';
const activeLabel = props.forceActiveLabel || value.length > 0 || props.prefixCharacter;

this.state = {
isFocused: false,
Expand All @@ -29,6 +29,7 @@ class BaseTextInput extends Component {
passwordHidden: props.secureTextEntry,
textInputWidth: 0,
prefixWidth: 0,
value,
};

this.input = null;
Expand Down Expand Up @@ -61,12 +62,13 @@ class BaseTextInput extends Component {
componentDidUpdate() {
// Activate or deactivate the label when value is changed programmatically from outside
// Only update when value prop is provided
if (this.props.value === undefined || this.value === this.props.value) {
if (this.props.value === undefined || this.state.value === this.props.value) {
return;
}

this.value = this.props.value;
this.input.setNativeProps({text: this.value});
// eslint-disable-next-line react/no-did-update-set-state
this.setState({value: this.props.value});
this.input.setNativeProps({text: this.props.value});

// In some cases, When the value prop is empty, it is not properly updated on the TextInput due to its uncontrolled nature, thus manually clearing the TextInput.
if (this.props.value === '') {
Expand Down Expand Up @@ -125,13 +127,13 @@ class BaseTextInput extends Component {
if (this.props.onInputChange) {
this.props.onInputChange(value);
}
this.value = value;
this.setState({value});
Str.result(this.props.onChangeText, value);
this.activateLabel();
}

activateLabel() {
if (this.value.length < 0 || this.isLabelActive) {
if (this.state.value.length < 0 || this.isLabelActive) {
return;
}

Expand All @@ -143,7 +145,7 @@ class BaseTextInput extends Component {
}

deactivateLabel() {
if (this.props.forceActiveLabel || this.value.length !== 0 || this.props.prefixCharacter) {
if (this.props.forceActiveLabel || this.state.value.length !== 0 || this.props.prefixCharacter) {
return;
}

Expand Down Expand Up @@ -188,6 +190,13 @@ class BaseTextInput extends Component {
const hasLabel = Boolean(this.props.label.length);
const inputHelpText = this.props.errorText || this.props.hint;
const formHelpStyles = this.props.errorText ? styles.formError : styles.formHelp;
const textInputContainerStyles = _.reduce([
styles.textInputContainer,
...this.props.textInputContainerStyles,
this.props.autoGrow && StyleUtils.getAutoGrowTextInputStyle(this.state.textInputWidth),
!this.props.hideFocusedState && this.state.isFocused && styles.borderColorFocus,
(this.props.hasError || this.props.errorText) && styles.borderColorDanger,
], (finalStyles, s) => ({...finalStyles, ...s}), {});

return (
<>
Expand All @@ -201,11 +210,10 @@ class BaseTextInput extends Component {
<TouchableWithoutFeedback onPress={this.onPress} focusable={false}>
<View
style={[
styles.textInputContainer,
...this.props.textInputContainerStyles,
this.props.autoGrow && StyleUtils.getAutoGrowTextInputStyle(this.state.textInputWidth),
!this.props.hideFocusedState && this.state.isFocused && styles.borderColorFocus,
(this.props.hasError || this.props.errorText) && styles.borderColorDanger,
textInputContainerStyles,

// When autoGrow is on and minWidth is not supplied, add a minWidth to allow the input to be focusable.
this.props.autoGrow && !textInputContainerStyles.minWidth && styles.mnw2,
NikkiWines marked this conversation as resolved.
Show resolved Hide resolved
]}
>
{hasLabel ? (
Expand Down Expand Up @@ -242,8 +250,17 @@ class BaseTextInput extends Component {
}}
// eslint-disable-next-line
{...inputProps}
defaultValue={this.value}
placeholder={(this.props.prefixCharacter || this.state.isFocused || !this.props.label) ? this.props.placeholder : null}
defaultValue={this.state.value}
placeholder={
(
NikkiWines marked this conversation as resolved.
Show resolved Hide resolved
this.props.prefixCharacter
|| this.state.isFocused
|| !hasLabel
|| (hasLabel && this.props.forceActiveLabel)
)
? this.props.placeholder
: null
}
placeholderTextColor={themeColors.placeholderText}
underlineColorAndroid="transparent"
style={[
Expand Down Expand Up @@ -293,7 +310,7 @@ class BaseTextInput extends Component {
)}
{!_.isNull(this.props.maxLength) && (
<Text style={[formHelpStyles, styles.flex1, styles.textAlignRight]}>
{this.value.length}
{this.state.value.length}
/
{this.props.maxLength}
</Text>
Expand All @@ -309,10 +326,10 @@ class BaseTextInput extends Component {
*/}
{this.props.autoGrow && (
<Text
style={[...this.props.inputStyle, styles.hiddenElementOutsideOfWindow]}
style={[...this.props.inputStyle, styles.hiddenElementOutsideOfWindow, styles.visibilityHidden]}
onLayout={e => this.setState({textInputWidth: e.nativeEvent.layout.width})}
>
{this.props.value || this.props.placeholder}
{this.state.value || this.props.placeholder}
</Text>
)}
</>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/home/report/ReportActionCompose.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {withOnyx} from 'react-native-onyx';
import lodashIntersection from 'lodash/intersection';
import styles from '../../../styles/styles';
import themeColors from '../../../styles/themes/default';
import TextInputFocusable from '../../../components/TextInputFocusable';
import Composer from '../../../components/Composer';
import ONYXKEYS from '../../../ONYXKEYS';
import Icon from '../../../components/Icon';
import * as Expensicons from '../../../components/Icon/Expensicons';
Expand Down Expand Up @@ -510,7 +510,7 @@ class ReportActionCompose extends React.Component {
</>
)}
</AttachmentPicker>
<TextInputFocusable
<Composer
autoFocus={this.shouldFocusInputOnScreenFocus || _.size(this.props.reportActions) === 1}
multiline
ref={this.setTextInputRef}
Expand Down
4 changes: 2 additions & 2 deletions src/pages/home/report/ReportActionItemMessageEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import _ from 'underscore';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import reportActionPropTypes from './reportActionPropTypes';
import styles from '../../../styles/styles';
import TextInputFocusable from '../../../components/TextInputFocusable';
import Composer from '../../../components/Composer';
import * as Report from '../../../libs/actions/Report';
import * as ReportScrollManager from '../../../libs/ReportScrollManager';
import toggleReportActionComposeView from '../../../libs/toggleReportActionComposeView';
Expand Down Expand Up @@ -158,7 +158,7 @@ class ReportActionItemMessageEdit extends React.Component {
return (
<View style={styles.chatItemMessage}>
<View style={[styles.chatItemComposeBox, styles.flexRow, styles.chatItemComposeBoxColor]}>
<TextInputFocusable
<Composer
multiline
ref={(el) => {
this.textInput = el;
Expand Down
120 changes: 120 additions & 0 deletions src/stories/Composer.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import React, {useState} from 'react';
import lodashGet from 'lodash/get';
import {View, Image} from 'react-native';
import Composer from '../components/Composer';
import RenderHTML from '../components/RenderHTML';
import Text from '../components/Text';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
import * as StyleUtils from '../styles/StyleUtils';
import CONST from '../CONST';

/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
const story = {
title: 'Components/Composer',
component: Composer,
};

const parser = new ExpensiMark();

const Default = (args) => {
const [pastedFile, setPastedFile] = useState(null);
const [comment, setComment] = useState(args.defaultValue);
const renderedHTML = parser.replace(comment);
NikkiWines marked this conversation as resolved.
Show resolved Hide resolved
const [droppingFile, setDroppingFile] = useState(false);
const [isComposerDroppingTarget, setIsComposerDroppingTarget] = useState(false);

return (
<View>
<View style={[styles.border, styles.p4, droppingFile && isComposerDroppingTarget && styles.borderColorFocus]}>
<Composer
// eslint-disable-next-line react/jsx-props-no-spreading
{...args}
multiline
textAlignVertical="top"
onChangeText={setComment}
onDragOver={(e, isOriginComposer) => {
setIsComposerDroppingTarget(isOriginComposer);
setDroppingFile(true);
}}
onDragLeave={() => {
setIsComposerDroppingTarget(false);
setDroppingFile(false);
}}
onDrop={(e) => {
e.preventDefault();

const file = lodashGet(e, ['dataTransfer', 'files', 0]);
if (!file) {
return;
}

setPastedFile(file);
}}
onPasteFile={setPastedFile}
style={[styles.textInputCompose, styles.w100]}
/>
</View>
<View style={[styles.flexRow, styles.mv5, styles.flexWrap, styles.w100]}>
<View
style={[
styles.border,
styles.noLeftBorderRadius,
styles.noRightBorderRadius,
styles.p5,
styles.flex1,
droppingFile && !isComposerDroppingTarget && styles.borderColorFocus,
]}
nativeID={CONST.REPORT.DROP_NATIVE_ID}
>
<Text style={[styles.mb2, styles.formLabel]}>Entered Comment (Drop Enabled)</Text>
<Text>{comment}</Text>
</View>
<View
style={[
styles.p5,
styles.borderBottom,
styles.borderRight,
styles.borderTop,
styles.flex1,
]}
>
<Text style={[styles.mb2, styles.formLabel]}>Rendered Comment</Text>
{Boolean(renderedHTML) && <RenderHTML html={renderedHTML} />}
{pastedFile && (
<View style={styles.mv3}>
<Image
source={{uri: URL.createObjectURL(pastedFile)}}
resizeMode="contain"
style={StyleUtils.getWidthAndHeightStyle(250, 250)}
/>
</View>
)}
</View>
</View>
</View>
);
};

Default.args = {
autoFocus: true,
placeholder: 'Compose Text Here',
placeholderTextColor: themeColors.placeholderText,
defaultValue: `Composer can do the following:

* It can contain MD e.g. *bold* _italic_
NikkiWines marked this conversation as resolved.
Show resolved Hide resolved
* Supports Pasted Images via Ctrl+V
* Supports Drag N Drop for files.`,
isDisabled: false,
maxLines: 16,
};

export default story;
export {
Default,
};
Loading