diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.js index 3f12fdacaa62..8181cb67e9a9 100644 --- a/src/components/BigNumberPad.js +++ b/src/components/BigNumberPad.js @@ -1,7 +1,8 @@ -import React, {PureComponent} from 'react'; +import React from 'react'; import { Text, TouchableOpacity, View, } from 'react-native'; +import _ from 'underscore'; import PropTypes from 'prop-types'; import styles from '../styles/styles'; @@ -17,57 +18,30 @@ const padNumbers = [ ['.', '0', '<'], ]; -class BigNumberPad extends PureComponent { - /** - * Creates set of buttons for given row - * - * @param {number} row - * @returns {View} - */ - createNumberPadRow(row) { - const self = this; - const numberPadRow = padNumbers[row].map((column, index) => self.createNumberPadButton(row, index)); - return ( - - {numberPadRow} +const BigNumberPad = ({numberPressed}) => ( + + {_.map(padNumbers, (row, rowIndex) => ( + + {_.map(row, (column, columnIndex) => { + // Adding margin between buttons except first column to + // avoid unccessary space before the first column. + const marginLeft = columnIndex > 0 ? styles.ml3 : {}; + return ( + numberPressed(column)} + > + + {column} + + + ); + })} - ); - } - - /** - * Creates a button for given row and column - * - * @param {number} row - * @param {number} column - * @returns {View} - */ - createNumberPadButton(row, column) { - // Adding margin between buttons except first column to - // avoid unccessary space before the first column. - const marginLeft = column > 0 ? styles.ml3 : {}; - return ( - this.props.numberPressed(padNumbers[row][column])} - > - - {padNumbers[row][column]} - - - ); - } - - render() { - const self = this; - const numberPad = padNumbers.map((row, index) => self.createNumberPadRow(index)); - return ( - - {numberPad} - - ); - } -} + ))} + +); BigNumberPad.propTypes = propTypes; BigNumberPad.displayName = 'BigNumberPad'; diff --git a/src/components/TextInputAutoWidth.js b/src/components/TextInputAutoWidth.js new file mode 100644 index 000000000000..71fad594821b --- /dev/null +++ b/src/components/TextInputAutoWidth.js @@ -0,0 +1,71 @@ +import React from 'react'; +import {View, Text} from 'react-native'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import styles, {getAutoGrowTextInputStyle} from '../styles/styles'; +import TextInputFocusable from './TextInputFocusable'; + +const propTypes = { + + // The value of the comment box + value: PropTypes.string.isRequired, + + // A ref to forward to the text input + forwardedRef: PropTypes.func.isRequired, + + // General styles to apply to the text input + // eslint-disable-next-line react/forbid-prop-types + inputStyle: PropTypes.object, + + // Styles to apply to the text input + // eslint-disable-next-line react/forbid-prop-types + textStyle: PropTypes.object.isRequired, +}; + +const defaultProps = { + inputStyle: {}, +}; + +class TextInputAutoWidth extends React.Component { + constructor(props) { + super(props); + + this.state = { + textInputWidth: 0, + }; + } + + render() { + const propsWithoutStyles = _.omit(this.props, ['inputStyle', 'textStyle']); + return ( + + + {/* + Text input component doesn't support auto grow by default. + We're using a hidden text input to achieve that. + This text view is used to calculate width of the input value given textStyle in this component. + This text component is intentionally positioned out of the screen. + */} + this.setState({textInputWidth: e.nativeEvent.layout.width})} + > + {this.props.value} + + + ); + } +} + +TextInputAutoWidth.propTypes = propTypes; +TextInputAutoWidth.defaultProps = defaultProps; + +export default React.forwardRef((props, ref) => ( + /* eslint-disable-next-line react/jsx-props-no-spreading */ + +)); diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js index 764ca1984ba1..ba75dc946c52 100644 --- a/src/components/TextInputFocusable/index.js +++ b/src/components/TextInputFocusable/index.js @@ -8,7 +8,10 @@ const propTypes = { maxLines: PropTypes.number, // The default value of the comment box - defaultValue: PropTypes.string.isRequired, + defaultValue: PropTypes.string, + + // The value of the comment box + value: PropTypes.string, // Callback method to handle pasting a file onPasteFile: PropTypes.func, @@ -44,6 +47,8 @@ const propTypes = { }; const defaultProps = { + defaultValue: '', + value: '', maxLines: -1, onPasteFile: () => {}, shouldClear: false, @@ -76,8 +81,12 @@ class TextInputFocusable extends React.Component { this.state = { numberOfLines: 1, selection: { - start: this.props.defaultValue.length, - end: this.props.defaultValue.length, + start: this.props.defaultValue + ? `${this.props.defaultValue}`.length + : `${this.props.value}`.length, + end: this.props.defaultValue + ? `${this.props.defaultValue}`.length + : `${this.props.value}`.length, }, }; } @@ -211,6 +220,9 @@ class TextInputFocusable extends React.Component { const propStyles = StyleSheet.flatten(this.props.style); propStyles.outline = 'none'; const propsWithoutStyles = _.omit(this.props, 'style'); + const propsWithoutValueOrDefaultValue = this.props.defaultValue + ? _.omit(propsWithoutStyles, 'value') + : _.omit(propsWithoutStyles, 'defaultValue'); return ( this.textInput = el} @@ -221,7 +233,7 @@ class TextInputFocusable extends React.Component { numberOfLines={this.state.numberOfLines} style={propStyles} /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...propsWithoutStyles} + {...propsWithoutValueOrDefaultValue} disabled={this.props.isDisabled} /> ); diff --git a/src/pages/iou/IOUModal.js b/src/pages/iou/IOUModal.js index f3d67ed1a2e0..1e2375be27cd 100644 --- a/src/pages/iou/IOUModal.js +++ b/src/pages/iou/IOUModal.js @@ -39,16 +39,14 @@ class IOUModal extends Component { this.navigateToPreviousStep = this.navigateToPreviousStep.bind(this); this.navigateToNextStep = this.navigateToNextStep.bind(this); - this.updateAmount = this.updateAmount.bind(this); this.currencySelected = this.currencySelected.bind(this); - this.addParticipants = this.addParticipants.bind(this); + this.state = { currentStepIndex: 0, participants: [], amount: '', selectedCurrency: 'USD', - isAmountPageNextButtonDisabled: true, }; } @@ -102,33 +100,6 @@ class IOUModal extends Component { }); } - /** - * Update amount with number or Backspace pressed. - * Validate new amount with decimal number regex up to 6 digits and 2 decimal digit - * - * @param {String} buttonPressed - */ - updateAmount(buttonPressed) { - // Backspace button is pressed - if (buttonPressed === '<' || buttonPressed === 'Backspace') { - if (this.state.amount.length > 0) { - this.setState(prevState => ({ - amount: prevState.amount.substring(0, prevState.amount.length - 1), - isAmountPageNextButtonDisabled: prevState.amount.length === 1, - })); - } - } else { - const decimalNumberRegex = new RegExp(/^\d{1,6}(\.\d{0,2})?$/, 'i'); - const amount = this.state.amount + buttonPressed; - if (decimalNumberRegex.test(amount)) { - this.setState({ - amount, - isAmountPageNextButtonDisabled: false, - }); - } - } - } - /** * Update the currency state * @@ -174,12 +145,12 @@ class IOUModal extends Component { {currentStep === Steps.IOUAmount && ( { + this.setState({amount}); + this.navigateToNextStep(); + }} currencySelected={this.currencySelected} - amount={this.state.amount} selectedCurrency={this.state.selectedCurrency} - isNextButtonDisabled={this.state.isAmountPageNextButtonDisabled} /> )} {currentStep === Steps.IOUParticipants && ( diff --git a/src/pages/iou/steps/IOUAmountPage.js b/src/pages/iou/steps/IOUAmountPage.js index 8d32220eced4..e266b6359451 100644 --- a/src/pages/iou/steps/IOUAmountPage.js +++ b/src/pages/iou/steps/IOUAmountPage.js @@ -12,15 +12,12 @@ import styles from '../../../styles/styles'; import themeColors from '../../../styles/themes/default'; import BigNumberPad from '../../../components/BigNumberPad'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; -import TextInputFocusable from '../../../components/TextInputFocusable'; +import TextInputAutoWidth from '../../../components/TextInputAutoWidth'; const propTypes = { // Callback to inform parent modal of success onStepComplete: PropTypes.func.isRequired, - // Callback to inform parent modal with key pressed - numberPressed: PropTypes.func.isRequired, - // Currency selection will be implemented later // eslint-disable-next-line react/no-unused-prop-types currencySelected: PropTypes.func.isRequired, @@ -28,12 +25,6 @@ const propTypes = { // User's currency preference selectedCurrency: PropTypes.string.isRequired, - // Amount value entered by user - amount: PropTypes.string.isRequired, - - // To disable/enable Next button based on amount validity - isNextButtonDisabled: PropTypes.bool.isRequired, - /* Window Dimensions Props */ ...windowDimensionsPropTypes, @@ -54,8 +45,9 @@ class IOUAmountPage extends React.Component { constructor(props) { super(props); + this.updateAmountIfValidInput = this.updateAmountIfValidInput.bind(this); this.state = { - textInputWidth: 0, + amount: '', }; } @@ -65,6 +57,37 @@ class IOUAmountPage extends React.Component { } } + /** + * Update amount with number or Backspace pressed. + * Validate new amount with decimal number regex up to 6 digits and 2 decimal digit to enable Next button + * + * @param {String} key + */ + updateAmountIfValidInput(key) { + // Backspace button is pressed + if (key === '<' || key === 'Backspace') { + if (this.state.amount.length > 0) { + this.setState(prevState => ({ + amount: prevState.amount.substring(0, prevState.amount.length - 1), + })); + } + return; + } + + this.setState((prevState) => { + const newValue = `${prevState.amount}${key}`; + + // Regex to validate decimal number with up to 3 decimal numbers + const decimalNumberRegex = new RegExp(/^\d+(\.\d{0,3})?$/, 'i'); + if (!decimalNumberRegex.test(newValue)) { + return prevState; + } + return { + amount: newValue, + }; + }); + } + render() { return ( @@ -81,38 +104,29 @@ class IOUAmountPage extends React.Component { {this.props.selectedCurrency} {this.props.isSmallScreenWidth - ? {this.props.amount} + ? {this.state.amount} : ( - - { - this.props.numberPressed(event.key); - event.preventDefault(); - }} - ref={el => this.textInput = el} - defaultValue={this.props.amount} - textAlign="left" - /> - this.setState({textInputWidth: e.nativeEvent.layout.width})} - > - {this.props.amount} - - + { + this.updateAmountIfValidInput(event.key); + event.preventDefault(); + }} + ref={el => this.textInput = el} + value={this.state.amount} + /> )} {this.props.isSmallScreenWidth - ? + ? : } this.props.onStepComplete(this.state.amount)} + disabled={this.state.amount.length === 0} > Next diff --git a/src/styles/styles.js b/src/styles/styles.js index 0ea54b06c18b..d0de58cff84f 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1245,12 +1245,14 @@ const styles = { fontFamily: fontFamily.GTA_BOLD, fontWeight: fontWeightBold, fontSize: variables.iouAmountTextSize, + color: themeColors.heading, }, iouAmountTextInput: addOutlineWidth({ fontFamily: fontFamily.GTA_BOLD, fontWeight: fontWeightBold, fontSize: variables.iouAmountTextSize, + color: themeColors.heading, }, 0), noScrollbars: { @@ -1264,6 +1266,14 @@ const styles = { alignItems: 'center', zIndex: 10, }, + + hiddenElementOutsideOfWindow: { + position: 'fixed', + top: 0, + left: 0, + opacity: 0, + transform: 'translateX(-100%)', + }, }; const baseCodeTagStyles = { @@ -1431,6 +1441,19 @@ function getZoomSizingStyle(isZoomed) { }; } +/** + * Returns auto grow text input style + * + * @param {Number} width + * @return {Object} + */ +function getAutoGrowTextInputStyle(width) { + return { + minWidth: 5, + width, + }; +} + /** * Returns a style with backgroundColor and borderColor set to the same color * @@ -1558,6 +1581,7 @@ export { getNavigationModalCardStyle, getZoomCursorStyle, getZoomSizingStyle, + getAutoGrowTextInputStyle, getBackgroundAndBorderStyle, getBackgroundColorStyle, getButtonBackgroundColorStyle,