From 7804df230f1d5f3deeca9d7fb113c3a3a17782ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 5 Jun 2023 18:57:49 +0200 Subject: [PATCH 01/28] initial rewrite of BaseTextInput to FC --- src/components/TextInput/BaseTextInput.js | 658 +++++++++--------- .../TextInput/baseTextInputPropTypes.js | 12 +- 2 files changed, 344 insertions(+), 326 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 91ab2162674f..12cbd9a1fcd6 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import React, {Component} from 'react'; +import React, {useState, useRef, useEffect, useCallback} from 'react'; import {Animated, View, AppState, Keyboard, StyleSheet} from 'react-native'; import Str from 'expensify-common/lib/str'; import RNTextInput from '../RNTextInput'; @@ -21,64 +21,72 @@ import isInputAutoFilled from '../../libs/isInputAutoFilled'; import * as Pressables from '../Pressable'; const PressableWithoutFeedback = Pressables.PressableWithoutFeedback; -class BaseTextInput extends Component { - constructor(props) { - super(props); - - const value = props.value || props.defaultValue || ''; - const activeLabel = props.forceActiveLabel || value.length > 0 || Boolean(props.prefixCharacter); - - this.state = { - isFocused: false, - labelTranslateY: new Animated.Value(activeLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y), - labelScale: new Animated.Value(activeLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE), - passwordHidden: props.secureTextEntry, - textInputWidth: 0, - prefixWidth: 0, - selection: props.selection, - height: variables.componentSizeLarge, - - // Value should be kept in state for the autoGrow feature to work - https://github.com/Expensify/App/pull/8232#issuecomment-1077282006 - value, - }; - this.input = null; - this.isLabelActive = activeLabel; - this.onPress = this.onPress.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.setValue = this.setValue.bind(this); - this.togglePasswordVisibility = this.togglePasswordVisibility.bind(this); - this.dismissKeyboardWhenBackgrounded = this.dismissKeyboardWhenBackgrounded.bind(this); - this.storePrefixLayoutDimensions = this.storePrefixLayoutDimensions.bind(this); +function dismissKeyboardWhenBackgrounded(nextAppState) { + if (!nextAppState.match(/inactive|background/)) { + return; } - componentDidMount() { - if (this.props.disableKeyboard) { - this.appStateSubscription = AppState.addEventListener('change', this.dismissKeyboardWhenBackgrounded); + Keyboard.dismiss(); +} + +function BaseTextInput(props) { + const initialActiveLabel = props.forceActiveLabel || props.value.length > 0 || Boolean(props.prefixCharacter); + + const [isFocused, setIsFocused] = useState(false); + const [labelTranslateY] = useState(() => new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)); + const [labelScale] = useState(() => new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)); + const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); + const [textInputWidth, setTextInputWidth] = useState(0); + const [textInputHeight, setTextInputHeight] = useState(); + const [prefixWidth, setPrefixWidth] = useState(0); + const [height, setHeight] = useState(variables.componentSizeLarge); + const [width, setWidth] = useState(); + + const input = useRef(null); + const isLabelActive = useRef(initialActiveLabel); + + useEffect(() => { + let appStateSubscription; + if (props.disableKeyboard) { + appStateSubscription = AppState.addEventListener('change', dismissKeyboardWhenBackgrounded); } // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 - if (!this.props.autoFocus || !this.input) { + if (!props.autoFocus || !input.current) { return; } - if (this.props.shouldDelayFocus) { - this.focusTimeout = setTimeout(() => this.input.focus(), CONST.ANIMATED_TRANSITION); + let focusTimeout; + if (props.shouldDelayFocus) { + focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); return; } - this.input.focus(); - } + input.current.focus(); + + return () => { + if (focusTimeout) { + clearTimeout(focusTimeout); + } + + if (!props.disableKeyboard || !appStateSubscription) { + return; + } + + appStateSubscription.remove(); + }; + }, [props.autoFocus, props.disableKeyboard, props.shouldDelayFocus]); - componentDidUpdate(prevProps) { + /* + componentDidUpdate(prevProps) { // Activate or deactivate the label when value is changed programmatically from outside - const inputValue = _.isUndefined(this.props.value) ? this.input.value : this.props.value; - if ((_.isUndefined(inputValue) || this.state.value === inputValue) && _.isEqual(prevProps.selection, this.props.selection)) { + const inputValue = _.isUndefined(props.value) ? this.input.value : props.value; + if ((_.isUndefined(inputValue) || props.value === inputValue) && _.isEqual(prevProps.selection, props.selection)) { return; } // eslint-disable-next-line react/no-did-update-set-state - this.setState({value: inputValue, selection: this.props.selection}); + this.setState({value: inputValue, selection: props.selection}); // 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 (inputValue === '') { @@ -87,315 +95,317 @@ class BaseTextInput extends Component { if (inputValue) { this.activateLabel(); - } else if (!this.state.isFocused) { + } else if (!props.isFocused) { this.deactivateLabel(); } } - - componentWillUnmount() { - if (this.focusTimeout) { - clearTimeout(this.focusTimeout); - } - - if (!this.props.disableKeyboard || !this.appStateSubscription) { + */ + + const propIsDisabled = props.disabled; + const propOnPress = props.onPress; + const onPress = useCallback( + (event) => { + if (propIsDisabled) { + return; + } + + if (propOnPress) { + propOnPress(event); + } + + if (!event.isDefaultPrevented()) { + input.current.focus(); + } + }, + [propIsDisabled, propOnPress], + ); + + const animateLabel = useCallback( + (translateY, scale) => { + Animated.parallel([ + Animated.spring(props.labelTranslateY, { + toValue: translateY, + duration: styleConst.LABEL_ANIMATION_DURATION, + useNativeDriver: true, + }), + Animated.spring(props.labelScale, { + toValue: scale, + duration: styleConst.LABEL_ANIMATION_DURATION, + useNativeDriver: true, + }), + ]).start(); + }, + [props.labelScale, props.labelTranslateY], + ); + + const activateLabel = useCallback(() => { + if (props.value.length < 0 || isLabelActive) { return; } - this.appStateSubscription.remove(); - } + animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); + isLabelActive.current = true; + }, [animateLabel, props.value.length]); - onPress(event) { - if (this.props.disabled) { + const deactivateLabel = useCallback(() => { + if (props.forceActiveLabel || props.value.length !== 0 || props.prefixCharacter) { return; } - if (this.props.onPress) { - this.props.onPress(event); - } - - if (!event.isDefaultPrevented()) { - this.input.focus(); - } - } - - onFocus(event) { - if (this.props.onFocus) { - this.props.onFocus(event); - } - this.setState({isFocused: true}); - this.activateLabel(); - } - - onBlur(event) { - if (this.props.onBlur) { - this.props.onBlur(event); - } - this.setState({isFocused: false}); - - // If the text has been supplied by Chrome autofill, the value state is not synced with the value - // as Chrome doesn't trigger a change event. When there is autofill text, don't deactivate label. - if (!isInputAutoFilled(this.input)) { - this.deactivateLabel(); - } - } - + animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); + isLabelActive.current = false; + }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value.length]); + + const propOnFocus = props.onFocus; + const onFocus = useCallback( + (event) => { + if (propOnFocus) { + propOnFocus(event); + } + setIsFocused(true); + activateLabel(); + }, + [activateLabel, propOnFocus], + ); + + const propOnBlur = props.onBlur; + const onBlur = useCallback( + (event) => { + if (propOnBlur) { + propOnBlur(event); + } + setIsFocused(false); + + // If the text has been supplied by Chrome autofill, the value state is not synced with the value + // as Chrome doesn't trigger a change event. When there is autofill text, don't deactivate label. + if (!isInputAutoFilled(input.current)) { + deactivateLabel(); + } + }, + [deactivateLabel, propOnBlur], + ); + + const onLayout = useCallback( + (event) => { + if (!props.autoGrowHeight && props.multiline) { + return; + } + + const layout = event.nativeEvent.layout; + + setWidth((prevWidth) => (props.autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight) => (!props.multiline ? layout.height : prevHeight)); + }, + [props.autoGrowHeight, props.multiline], + ); + + // I feel like this is the region where imperative functions are starting: + + const propOnInputChange = props.onInputChange; /** * Set Value & activateLabel * * @param {String} value * @memberof BaseTextInput */ - setValue(value) { - if (this.props.onInputChange) { - this.props.onInputChange(value); - } - this.setState({value}); - Str.result(this.props.onChangeText, value); - this.activateLabel(); - } - - activateLabel() { - if (this.state.value.length < 0 || this.isLabelActive) { - return; - } - - this.animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); - this.isLabelActive = true; - } - - deactivateLabel() { - if (this.props.forceActiveLabel || this.state.value.length !== 0 || this.props.prefixCharacter) { - return; - } - - this.animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); - this.isLabelActive = false; - } - - dismissKeyboardWhenBackgrounded(nextAppState) { - if (!nextAppState.match(/inactive|background/)) { - return; - } - - Keyboard.dismiss(); - } - - animateLabel(translateY, scale) { - Animated.parallel([ - Animated.spring(this.state.labelTranslateY, { - toValue: translateY, - duration: styleConst.LABEL_ANIMATION_DURATION, - useNativeDriver: true, - }), - Animated.spring(this.state.labelScale, { - toValue: scale, - duration: styleConst.LABEL_ANIMATION_DURATION, - useNativeDriver: true, - }), - ]).start(); - } - - togglePasswordVisibility() { - this.setState((prevState) => ({passwordHidden: !prevState.passwordHidden})); - } - - storePrefixLayoutDimensions(event) { - this.setState({prefixWidth: Math.abs(event.nativeEvent.layout.width)}); - } - - render() { - // eslint-disable-next-line react/forbid-foreign-prop-types - const inputProps = _.omit(this.props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(this.props.label.length); - const isEditable = _.isUndefined(this.props.editable) ? !this.props.disabled : this.props.editable; - const inputHelpText = this.props.errorText || this.props.hint; - const placeholder = this.props.prefixCharacter || this.state.isFocused || !hasLabel || (hasLabel && this.props.forceActiveLabel) ? this.props.placeholder : null; - const maxHeight = StyleSheet.flatten(this.props.containerStyles).maxHeight; - const textInputContainerStyles = _.reduce( - [ - styles.textInputContainer, - ...this.props.textInputContainerStyles, - this.props.autoGrow && StyleUtils.getWidthStyle(this.state.textInputWidth), - !this.props.hideFocusedState && this.state.isFocused && styles.borderColorFocus, - (this.props.hasError || this.props.errorText) && styles.borderColorDanger, - this.props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, - ], - (finalStyles, s) => ({...finalStyles, ...s}), - {}, - ); - const isMultiline = this.props.multiline || this.props.autoGrowHeight; - - return ( - <> - - { + if (propOnInputChange) { + propOnInputChange(value); + } + + // TODO: what is the next line used for? + Str.result(props.onChangeText, value); + activateLabel(); + }, + [activateLabel, propOnInputChange, props.onChangeText], + ); + + const togglePasswordVisibility = useCallback(() => { + setPasswordHidden((prevState) => !prevState.passwordHidden); + }, []); + + const storePrefixLayoutDimensions = useCallback((event) => { + setPrefixWidth(Math.abs(event.nativeEvent.layout.width)); + }, []); + + // TODO: don't do that all here? + // eslint-disable-next-line react/forbid-foreign-prop-types + const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); + const hasLabel = Boolean(props.label.length); + const isEditable = _.isUndefined(props.editable) ? !props.disabled : props.editable; + const inputHelpText = props.errorText || props.hint; + const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; + const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; + const textInputContainerStyles = _.reduce( + [ + styles.textInputContainer, + ...props.textInputContainerStyles, + props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !props.hideFocusedState && isFocused && styles.borderColorFocus, + (props.hasError || props.errorText) && styles.borderColorDanger, + props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + ], + (finalStyles, s) => ({...finalStyles, ...s}), + {}, + ); + const isMultiline = props.multiline || props.autoGrowHeight; + + return ( + <> + + + - { - if (!this.props.autoGrowHeight && this.props.multiline) { - return; - } - - const layout = event.nativeEvent.layout; - - this.setState((prevState) => ({ - width: this.props.autoGrowHeight ? layout.width : prevState.width, - height: !isMultiline ? layout.height : prevState.height, - })); - }} - style={[ - 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, - ]} - > - {hasLabel ? ( - <> - {/* Adding this background to the label only for multiline text input, - to prevent text overlapping with label when scrolling */} - {isMultiline && ( - - )} - + {/* Adding this background to the label only for multiline text input, + to prevent text overlapping with label when scrolling */} + {isMultiline && ( + - - ) : null} - - {Boolean(this.props.prefixCharacter) && ( - - - {this.props.prefixCharacter} - - )} - { - if (typeof this.props.innerRef === 'function') { - this.props.innerRef(ref); - } else if (this.props.innerRef && _.has(this.props.innerRef, 'current')) { - this.props.innerRef.current = ref; - } - this.input = ref; - }} - // eslint-disable-next-line - {...inputProps} - autoCorrect={this.props.secureTextEntry ? false : this.props.autoCorrect} - placeholder={placeholder} - placeholderTextColor={themeColors.placeholderText} - underlineColorAndroid="transparent" - style={[ - styles.flex1, - styles.w100, - this.props.inputStyle, - (!hasLabel || isMultiline) && styles.pv0, - this.props.prefixCharacter && StyleUtils.getPaddingLeft(this.state.prefixWidth + styles.pl1.paddingLeft), - this.props.secureTextEntry && styles.secureInput, - - // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear - // once it exceeds the input space (See https://github.com/Expensify/App/issues/13802) - !isMultiline && {height: this.state.height, lineHeight: undefined}, - - // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - this.props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(this.state.textInputHeight, maxHeight), - ]} - multiline={isMultiline} - maxLength={this.props.maxLength} - onFocus={this.onFocus} - onBlur={this.onBlur} - onChangeText={this.setValue} - secureTextEntry={this.state.passwordHidden} - onPressOut={this.props.onPress} - showSoftInputOnFocus={!this.props.disableKeyboard} - keyboardType={getSecureEntryKeyboardType(this.props.keyboardType, this.props.secureTextEntry, this.state.passwordHidden)} - value={this.state.value} - selection={this.state.selection} - editable={isEditable} - // FormSubmit Enter key handler does not have access to direct props. - // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && this.props.submitOnEnter}} + - {Boolean(this.props.secureTextEntry) && ( - e.preventDefault()} + + ) : null} + + {Boolean(props.prefixCharacter) && ( + + - - - )} - {!this.props.secureTextEntry && Boolean(this.props.icon) && ( - - - - )} - + {props.prefixCharacter} + + + )} + { + if (typeof props.innerRef === 'function') { + props.innerRef(ref); + } else if (props.innerRef && _.has(props.innerRef, 'current')) { + // eslint-disable-next-line no-param-reassign + props.innerRef.current = ref; + } + input.current = ref; + }} + // eslint-disable-next-line + {...inputProps} + autoCorrect={props.secureTextEntry ? false : props.autoCorrect} + placeholder={placeholder} + placeholderTextColor={themeColors.placeholderText} + underlineColorAndroid="transparent" + style={[ + styles.flex1, + styles.w100, + props.inputStyle, + (!hasLabel || isMultiline) && styles.pv0, + props.prefixCharacter && StyleUtils.getPaddingLeft(prefixWidth + styles.pl1.paddingLeft), + props.secureTextEntry && styles.secureInput, + + // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear + // once it exceeds the input space (See https://github.com/Expensify/App/issues/13802) + !isMultiline && {height, lineHeight: undefined}, + + // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. + props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + ]} + multiline={isMultiline} + maxLength={props.maxLength} + onFocus={onFocus} + onBlur={onBlur} + onChangeText={setValue} + secureTextEntry={passwordHidden} + onPressOut={props.onPress} + showSoftInputOnFocus={!props.disableKeyboard} + keyboardType={getSecureEntryKeyboardType(props.keyboardType, props.secureTextEntry, passwordHidden)} + value={props.value} + selection={props.selection} + editable={isEditable} + // FormSubmit Enter key handler does not have access to direct props. + // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. + dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} + /> + {Boolean(props.secureTextEntry) && ( + e.preventDefault()} + > + + + )} + {!props.secureTextEntry && Boolean(props.icon) && ( + + + + )} - - {!_.isEmpty(inputHelpText) && ( - - )} - - {/* - 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 or height of the input value given textStyle in this component. - This Text component is intentionally positioned out of the screen. - */} - {(this.props.autoGrow || this.props.autoGrowHeight) && ( - // Add +2 to width so that the first digit of amount do not cut off on mWeb - https://github.com/Expensify/App/issues/8158. - this.setState({textInputWidth: e.nativeEvent.layout.width + 2, textInputHeight: e.nativeEvent.layout.height})} - > - {this.state.value || this.props.placeholder} - + + + {!_.isEmpty(inputHelpText) && ( + )} - - ); - } + + {/* + 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 or height of the input value given textStyle in this component. + This Text component is intentionally positioned out of the screen. + */} + {(props.autoGrow || props.autoGrowHeight) && ( + // Add +2 to width so that the first digit of amount do not cut off on mWeb - https://github.com/Expensify/App/issues/8158. + { + setTextInputWidth(e.nativeEvent.layout.width + 2); + setTextInputHeight(e.nativeEvent.layout.height); + }} + > + {props.value || props.placeholder} + + )} + + ); } +BaseTextInput.displayName = 'BaseTextInput'; BaseTextInput.propTypes = baseTextInputPropTypes.propTypes; BaseTextInput.defaultProps = baseTextInputPropTypes.defaultProps; diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index 45720ceb8c47..899bbe532d3c 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -40,10 +40,18 @@ const propTypes = { /** Disable the virtual keyboard */ disableKeyboard: PropTypes.bool, - /** Autogrow input container length based on the entered text */ + /** + * Autogrow input container length based on the entered text. + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ autoGrow: PropTypes.bool, - /** Autogrow input container height based on the entered text */ + /** + * Autogrow input container height based on the entered text + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ autoGrowHeight: PropTypes.bool, /** Hide the focus styles on TextInput */ From 880c4c008d5defa3a95f197f2f81fd2702f99be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 5 Jun 2023 19:12:29 +0200 Subject: [PATCH 02/28] fix focus/label not working --- src/components/TextInput/BaseTextInput.js | 47 +++++++++-------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 12cbd9a1fcd6..8431073052ac 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -77,30 +77,6 @@ function BaseTextInput(props) { }; }, [props.autoFocus, props.disableKeyboard, props.shouldDelayFocus]); - /* - componentDidUpdate(prevProps) { - // Activate or deactivate the label when value is changed programmatically from outside - const inputValue = _.isUndefined(props.value) ? this.input.value : props.value; - if ((_.isUndefined(inputValue) || props.value === inputValue) && _.isEqual(prevProps.selection, props.selection)) { - return; - } - - // eslint-disable-next-line react/no-did-update-set-state - this.setState({value: inputValue, selection: props.selection}); - - // 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 (inputValue === '') { - this.input.clear(); - } - - if (inputValue) { - this.activateLabel(); - } else if (!props.isFocused) { - this.deactivateLabel(); - } - } - */ - const propIsDisabled = props.disabled; const propOnPress = props.onPress; const onPress = useCallback( @@ -123,23 +99,23 @@ function BaseTextInput(props) { const animateLabel = useCallback( (translateY, scale) => { Animated.parallel([ - Animated.spring(props.labelTranslateY, { + Animated.spring(labelTranslateY, { toValue: translateY, duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver: true, }), - Animated.spring(props.labelScale, { + Animated.spring(labelScale, { toValue: scale, duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver: true, }), ]).start(); }, - [props.labelScale, props.labelTranslateY], + [labelScale, labelTranslateY], ); const activateLabel = useCallback(() => { - if (props.value.length < 0 || isLabelActive) { + if (props.value.length < 0 || isLabelActive.current) { return; } @@ -199,6 +175,21 @@ function BaseTextInput(props) { [props.autoGrowHeight, props.multiline], ); + useEffect(() => { + // Activate or deactivate the label when value is changed programmatically from outside + + // 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 (props.value === '') { + input.current.clear(); + } + + if (props.value) { + activateLabel(); + } else if (!isFocused) { + deactivateLabel(); + } + }, [activateLabel, deactivateLabel, isFocused, props.value]); + // I feel like this is the region where imperative functions are starting: const propOnInputChange = props.onInputChange; From 36eb6eb40529e2d221144bd0ee488cfd81fbc04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 6 Jun 2023 09:36:35 +0200 Subject: [PATCH 03/28] remove comment --- src/components/TextInput/BaseTextInput.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 8431073052ac..0705e607bd4f 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -190,8 +190,6 @@ function BaseTextInput(props) { } }, [activateLabel, deactivateLabel, isFocused, props.value]); - // I feel like this is the region where imperative functions are starting: - const propOnInputChange = props.onInputChange; /** * Set Value & activateLabel From bd6cef713ef2f356215bcbe45b08d467bef60b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 6 Jun 2023 11:04:58 +0200 Subject: [PATCH 04/28] fix text input focus --- src/components/TextInput/BaseTextInput.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 0705e607bd4f..bfddf9bab9c1 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -135,13 +135,13 @@ function BaseTextInput(props) { const propOnFocus = props.onFocus; const onFocus = useCallback( (event) => { + console.log('onFocus'); if (propOnFocus) { propOnFocus(event); } setIsFocused(true); - activateLabel(); }, - [activateLabel, propOnFocus], + [propOnFocus], ); const propOnBlur = props.onBlur; @@ -183,7 +183,7 @@ function BaseTextInput(props) { input.current.clear(); } - if (props.value) { + if (props.value || isFocused) { activateLabel(); } else if (!isFocused) { deactivateLabel(); From 519b1923830f2bd16a1ae10580b7d4247204cdcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 6 Jun 2023 13:35:09 +0200 Subject: [PATCH 05/28] fix focus --- src/components/TextInput/BaseTextInput.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index bfddf9bab9c1..80f8ca128421 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -31,7 +31,8 @@ function dismissKeyboardWhenBackgrounded(nextAppState) { } function BaseTextInput(props) { - const initialActiveLabel = props.forceActiveLabel || props.value.length > 0 || Boolean(props.prefixCharacter); + const inputValue = props.value || ''; + const initialActiveLabel = props.forceActiveLabel || inputValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [labelTranslateY] = useState(() => new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)); @@ -115,27 +116,26 @@ function BaseTextInput(props) { ); const activateLabel = useCallback(() => { - if (props.value.length < 0 || isLabelActive.current) { + if (inputValue.length < 0 || isLabelActive.current) { return; } animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); isLabelActive.current = true; - }, [animateLabel, props.value.length]); + }, [animateLabel, inputValue]); const deactivateLabel = useCallback(() => { - if (props.forceActiveLabel || props.value.length !== 0 || props.prefixCharacter) { + if (props.forceActiveLabel || inputValue.length !== 0 || props.prefixCharacter) { return; } animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value.length]); + }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, inputValue]); const propOnFocus = props.onFocus; const onFocus = useCallback( (event) => { - console.log('onFocus'); if (propOnFocus) { propOnFocus(event); } @@ -179,16 +179,16 @@ function BaseTextInput(props) { // Activate or deactivate the label when value is changed programmatically from outside // 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 (props.value === '') { + if (inputValue === '') { input.current.clear(); } - if (props.value || isFocused) { + if (inputValue || isFocused) { activateLabel(); } else if (!isFocused) { deactivateLabel(); } - }, [activateLabel, deactivateLabel, isFocused, props.value]); + }, [activateLabel, deactivateLabel, inputValue, isFocused]); const propOnInputChange = props.onInputChange; /** From c26fdb9334e6acb5c5cd4f6e8a6a72393ef495ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 7 Jun 2023 10:00:28 +0200 Subject: [PATCH 06/28] fix usage of useCallback --- src/components/TextInput/BaseTextInput.js | 95 ++++++++++------------- 1 file changed, 39 insertions(+), 56 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 80f8ca128421..2208abb38890 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -78,25 +78,6 @@ function BaseTextInput(props) { }; }, [props.autoFocus, props.disableKeyboard, props.shouldDelayFocus]); - const propIsDisabled = props.disabled; - const propOnPress = props.onPress; - const onPress = useCallback( - (event) => { - if (propIsDisabled) { - return; - } - - if (propOnPress) { - propOnPress(event); - } - - if (!event.isDefaultPrevented()) { - input.current.focus(); - } - }, - [propIsDisabled, propOnPress], - ); - const animateLabel = useCallback( (translateY, scale) => { Animated.parallel([ @@ -133,33 +114,39 @@ function BaseTextInput(props) { isLabelActive.current = false; }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, inputValue]); - const propOnFocus = props.onFocus; - const onFocus = useCallback( - (event) => { - if (propOnFocus) { - propOnFocus(event); - } - setIsFocused(true); - }, - [propOnFocus], - ); + const onFocus = (event) => { + if (props.onFocus) { + props.onFocus(event); + } + setIsFocused(true); + }; - const propOnBlur = props.onBlur; - const onBlur = useCallback( - (event) => { - if (propOnBlur) { - propOnBlur(event); - } - setIsFocused(false); + const onBlur = (event) => { + if (props.onBlur) { + props.onBlur(event); + } + setIsFocused(false); - // If the text has been supplied by Chrome autofill, the value state is not synced with the value - // as Chrome doesn't trigger a change event. When there is autofill text, don't deactivate label. - if (!isInputAutoFilled(input.current)) { - deactivateLabel(); - } - }, - [deactivateLabel, propOnBlur], - ); + // If the text has been supplied by Chrome autofill, the value state is not synced with the value + // as Chrome doesn't trigger a change event. When there is autofill text, don't deactivate label. + if (!isInputAutoFilled(input.current)) { + deactivateLabel(); + } + }; + + const onPress = (event) => { + if (props.disabled) { + return; + } + + if (props.onPress) { + props.onPress(event); + } + + if (!event.isDefaultPrevented()) { + input.current.focus(); + } + }; const onLayout = useCallback( (event) => { @@ -190,25 +177,21 @@ function BaseTextInput(props) { } }, [activateLabel, deactivateLabel, inputValue, isFocused]); - const propOnInputChange = props.onInputChange; /** * Set Value & activateLabel * * @param {String} value * @memberof BaseTextInput */ - const setValue = useCallback( - (value) => { - if (propOnInputChange) { - propOnInputChange(value); - } + const setValue = (value) => { + if (props.onInputChange) { + props.onInputChange(value); + } - // TODO: what is the next line used for? - Str.result(props.onChangeText, value); - activateLabel(); - }, - [activateLabel, propOnInputChange, props.onChangeText], - ); + // TODO: what is the next line used for? + Str.result(props.onChangeText, value); + activateLabel(); + }; const togglePasswordVisibility = useCallback(() => { setPasswordHidden((prevState) => !prevState.passwordHidden); From 7a3f3f3fff1320e0159cb1d5c34e2d8ae87ab3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 12 Jun 2023 11:17:27 +0200 Subject: [PATCH 07/28] remove todo --- src/components/TextInput/BaseTextInput.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 2208abb38890..12206f82d163 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -188,7 +188,6 @@ function BaseTextInput(props) { props.onInputChange(value); } - // TODO: what is the next line used for? Str.result(props.onChangeText, value); activateLabel(); }; From 3886aaf84de1d827b3aa07a70ab2e50c43d1e9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 12 Jun 2023 11:19:10 +0200 Subject: [PATCH 08/28] use named import directly --- src/components/TextInput/BaseTextInput.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 12206f82d163..e55b81f78925 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -20,8 +20,6 @@ import FormHelpMessage from '../FormHelpMessage'; import isInputAutoFilled from '../../libs/isInputAutoFilled'; import * as Pressables from '../Pressable'; -const PressableWithoutFeedback = Pressables.PressableWithoutFeedback; - function dismissKeyboardWhenBackgrounded(nextAppState) { if (!nextAppState.match(/inactive|background/)) { return; @@ -225,7 +223,7 @@ function BaseTextInput(props) { return ( <> - - + {!_.isEmpty(inputHelpText) && ( Date: Mon, 12 Jun 2023 11:20:02 +0200 Subject: [PATCH 09/28] use function directly --- src/components/TextInput/BaseTextInput.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index e55b81f78925..2fc5c395981d 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -20,14 +20,6 @@ import FormHelpMessage from '../FormHelpMessage'; import isInputAutoFilled from '../../libs/isInputAutoFilled'; import * as Pressables from '../Pressable'; -function dismissKeyboardWhenBackgrounded(nextAppState) { - if (!nextAppState.match(/inactive|background/)) { - return; - } - - Keyboard.dismiss(); -} - function BaseTextInput(props) { const inputValue = props.value || ''; const initialActiveLabel = props.forceActiveLabel || inputValue.length > 0 || Boolean(props.prefixCharacter); @@ -48,7 +40,13 @@ function BaseTextInput(props) { useEffect(() => { let appStateSubscription; if (props.disableKeyboard) { - appStateSubscription = AppState.addEventListener('change', dismissKeyboardWhenBackgrounded); + appStateSubscription = AppState.addEventListener('change', (nextAppState) => { + if (!nextAppState.match(/inactive|background/)) { + return; + } + + Keyboard.dismiss(); + }); } // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 From 21ded8cef5e59393745387b13972c116db89c277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 12 Jun 2023 11:22:09 +0200 Subject: [PATCH 10/28] only autoFocus on mount --- src/components/TextInput/BaseTextInput.js | 24 +++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 2fc5c395981d..b30da00048b5 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -49,6 +49,17 @@ function BaseTextInput(props) { }); } + return () => { + if (!props.disableKeyboard || !appStateSubscription) { + return; + } + + appStateSubscription.remove(); + }; + }, [props.disableKeyboard]); + + // AutoFocus which only works on mount: + useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 if (!props.autoFocus || !input.current) { return; @@ -62,17 +73,14 @@ function BaseTextInput(props) { input.current.focus(); return () => { - if (focusTimeout) { - clearTimeout(focusTimeout); - } - - if (!props.disableKeyboard || !appStateSubscription) { + if (!focusTimeout) { return; } - - appStateSubscription.remove(); + clearTimeout(focusTimeout); }; - }, [props.autoFocus, props.disableKeyboard, props.shouldDelayFocus]); + // We only want this to run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const animateLabel = useCallback( (translateY, scale) => { From 68ee867dd2fb9af1a8ea75f064c291b7b7437690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 12 Jun 2023 11:27:55 +0200 Subject: [PATCH 11/28] use stylesheet flatten --- src/components/TextInput/BaseTextInput.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index b30da00048b5..7038a4e321f9 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -204,7 +204,6 @@ function BaseTextInput(props) { setPrefixWidth(Math.abs(event.nativeEvent.layout.width)); }, []); - // TODO: don't do that all here? // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); const hasLabel = Boolean(props.label.length); @@ -212,18 +211,14 @@ function BaseTextInput(props) { const inputHelpText = props.errorText || props.hint; const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles = _.reduce( - [ - styles.textInputContainer, - ...props.textInputContainerStyles, - props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), - !props.hideFocusedState && isFocused && styles.borderColorFocus, - (props.hasError || props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, - ], - (finalStyles, s) => ({...finalStyles, ...s}), - {}, - ); + const textInputContainerStyles = StyleSheet.flatten([ + styles.textInputContainer, + ...props.textInputContainerStyles, + props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !props.hideFocusedState && isFocused && styles.borderColorFocus, + (props.hasError || props.errorText) && styles.borderColorDanger, + props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + ]); const isMultiline = props.multiline || props.autoGrowHeight; return ( From 86dea8ce4d5d13ede77bf10c2ba4dfd38c192b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 12 Jun 2023 17:59:11 +0200 Subject: [PATCH 12/28] Update src/components/TextInput/BaseTextInput.js Co-authored-by: Rajat Parashar --- src/components/TextInput/BaseTextInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 7038a4e321f9..5a59db07879f 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -18,7 +18,7 @@ import getSecureEntryKeyboardType from '../../libs/getSecureEntryKeyboardType'; import CONST from '../../CONST'; import FormHelpMessage from '../FormHelpMessage'; import isInputAutoFilled from '../../libs/isInputAutoFilled'; -import * as Pressables from '../Pressable'; +import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; function BaseTextInput(props) { const inputValue = props.value || ''; From 71fb82c0dad473ac551beeb742e4cf3ca0772fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 12 Jun 2023 17:59:28 +0200 Subject: [PATCH 13/28] Update src/components/TextInput/BaseTextInput.js Co-authored-by: Rajat Parashar --- src/components/TextInput/BaseTextInput.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 5a59db07879f..9875dead95d1 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -39,7 +39,9 @@ function BaseTextInput(props) { useEffect(() => { let appStateSubscription; - if (props.disableKeyboard) { + if (!props.disableKeyboard) { + return; + } appStateSubscription = AppState.addEventListener('change', (nextAppState) => { if (!nextAppState.match(/inactive|background/)) { return; From 528d63eb33e74b724c75dbf09fce247f42f2b8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 12 Jun 2023 18:00:18 +0200 Subject: [PATCH 14/28] Update src/components/TextInput/BaseTextInput.js Co-authored-by: Rajat Parashar --- src/components/TextInput/BaseTextInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 9875dead95d1..dded7a117d70 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -226,7 +226,7 @@ function BaseTextInput(props) { return ( <> - Date: Mon, 12 Jun 2023 18:04:34 +0200 Subject: [PATCH 15/28] fix useEffect --- src/components/TextInput/BaseTextInput.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index dded7a117d70..dca8d4b83f26 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -38,21 +38,20 @@ function BaseTextInput(props) { const isLabelActive = useRef(initialActiveLabel); useEffect(() => { - let appStateSubscription; - if (!props.disableKeyboard) { - return; + if (!props.disableKeyboard) { + return; } - appStateSubscription = AppState.addEventListener('change', (nextAppState) => { - if (!nextAppState.match(/inactive|background/)) { - return; - } - Keyboard.dismiss(); - }); - } + const appStateSubscription = AppState.addEventListener('change', (nextAppState) => { + if (!nextAppState.match(/inactive|background/)) { + return; + } + + Keyboard.dismiss(); + }); return () => { - if (!props.disableKeyboard || !appStateSubscription) { + if (!props.disableKeyboard) { return; } @@ -347,7 +346,7 @@ function BaseTextInput(props) { )} - + {!_.isEmpty(inputHelpText) && ( Date: Mon, 12 Jun 2023 18:06:06 +0200 Subject: [PATCH 16/28] useRef for stable values --- src/components/TextInput/BaseTextInput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index dca8d4b83f26..f797df2063c6 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -25,8 +25,8 @@ function BaseTextInput(props) { const initialActiveLabel = props.forceActiveLabel || inputValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); - const [labelTranslateY] = useState(() => new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)); - const [labelScale] = useState(() => new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)); + const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; + const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(); From 0a756fc5534f35c6c2f321f0d316c1c5415107dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 12 Jun 2023 18:19:56 +0200 Subject: [PATCH 17/28] use defaultValue as well --- src/components/TextInput/BaseTextInput.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index f797df2063c6..ff03590844e3 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -21,7 +21,7 @@ import isInputAutoFilled from '../../libs/isInputAutoFilled'; import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; function BaseTextInput(props) { - const inputValue = props.value || ''; + const inputValue = props.value || props.defaultValue || ''; const initialActiveLabel = props.forceActiveLabel || inputValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); @@ -102,22 +102,26 @@ function BaseTextInput(props) { ); const activateLabel = useCallback(() => { - if (inputValue.length < 0 || isLabelActive.current) { + const value = props.value || ''; + + if (value.length < 0 || isLabelActive.current) { return; } animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); isLabelActive.current = true; - }, [animateLabel, inputValue]); + }, [animateLabel, props.value]); const deactivateLabel = useCallback(() => { - if (props.forceActiveLabel || inputValue.length !== 0 || props.prefixCharacter) { + const value = props.value || ''; + + if (props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { return; } animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, inputValue]); + }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); const onFocus = (event) => { if (props.onFocus) { From 7d1cb482a6c8627268743adcaf12edad5a6b8cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 13 Jun 2023 10:20:47 +0200 Subject: [PATCH 18/28] removed unnecessary condition --- src/components/TextInput/BaseTextInput.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index ff03590844e3..60f2ed739740 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -51,10 +51,6 @@ function BaseTextInput(props) { }); return () => { - if (!props.disableKeyboard) { - return; - } - appStateSubscription.remove(); }; }, [props.disableKeyboard]); From 553111e9845d363ae0c7a58ba32aa186c3fe2916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 13 Jun 2023 10:21:33 +0200 Subject: [PATCH 19/28] beautify code order --- src/components/TextInput/BaseTextInput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 60f2ed739740..724e39c0cdce 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -25,14 +25,14 @@ function BaseTextInput(props) { const initialActiveLabel = props.forceActiveLabel || inputValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); - const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(); const [prefixWidth, setPrefixWidth] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); const [width, setWidth] = useState(); + const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; + const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); From 511c5b6e8bbad414952e5cd9205863ae97bf97ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 17 Jun 2023 16:57:53 +0200 Subject: [PATCH 20/28] fix default value --- src/components/TextInput/BaseTextInput.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 724e39c0cdce..6fe66c76f5aa 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -320,6 +320,7 @@ function BaseTextInput(props) { value={props.value} selection={props.selection} editable={isEditable} + defaultValue={props.defaultValue} // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} From 682ad7e548607b3fc15102676a8694f955ca1acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 17 Jun 2023 16:58:17 +0200 Subject: [PATCH 21/28] fix stories --- src/stories/TextInput.stories.js | 65 +++++++++++++++++++------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/stories/TextInput.stories.js b/src/stories/TextInput.stories.js index 64329ffed715..2a9e862d6876 100644 --- a/src/stories/TextInput.stories.js +++ b/src/stories/TextInput.stories.js @@ -60,32 +60,6 @@ PlaceholderInput.args = { placeholder: 'My placeholder text', }; -const AutoGrowInput = Template.bind({}); -AutoGrowInput.args = { - label: 'Autogrow input', - name: 'AutoGrow', - placeholder: 'My placeholder text', - autoGrow: true, - textInputContainerStyles: [ - { - minWidth: 150, - }, - ], -}; - -const AutoGrowHeightInput = Template.bind({}); -AutoGrowHeightInput.args = { - label: 'Autogrowheight input', - name: 'AutoGrowHeight', - placeholder: 'My placeholder text', - autoGrowHeight: true, - textInputContainerStyles: [ - { - maxHeight: 115, - }, - ], -}; - const PrefixedInput = Template.bind({}); PrefixedInput.args = { label: 'Prefixed input', @@ -126,5 +100,44 @@ HintAndErrorInput.args = { hint: 'Type "Oops!" to see the error', }; +// To use autoGrow we need to control the TextInput's value +function AutoGrowSupportInput(args) { + const [value, setValue] = useState(''); + return ( + + ); +} + +const AutoGrowInput = AutoGrowSupportInput.bind({}); +AutoGrowInput.args = { + label: 'Autogrow input', + name: 'AutoGrow', + placeholder: 'My placeholder text', + autoGrow: true, + textInputContainerStyles: [ + { + minWidth: 150, + }, + ], +}; + +const AutoGrowHeightInput = AutoGrowSupportInput.bind({}); +AutoGrowHeightInput.args = { + label: 'Autogrowheight input', + name: 'AutoGrowHeight', + placeholder: 'My placeholder text', + autoGrowHeight: true, + textInputContainerStyles: [ + { + maxHeight: 115, + }, + ], +}; + export default story; export {AutoFocus, DefaultInput, DefaultValueInput, ErrorInput, ForceActiveLabel, PlaceholderInput, AutoGrowInput, AutoGrowHeightInput, PrefixedInput, MaxLengthInput, HintAndErrorInput}; From dc5a7f005392f9e6b2c5b39f1bb37bb51f7df711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 20 Jun 2023 09:27:42 +0200 Subject: [PATCH 22/28] fix update label --- src/components/TextInput/BaseTextInput.js | 22 ++++++++++++++++++---- src/stories/TextInput.stories.js | 7 ++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 6fe66c76f5aa..2c4f5a983e6e 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -167,17 +167,26 @@ function BaseTextInput(props) { [props.autoGrowHeight, props.multiline], ); + const hasValueRef = useRef(inputValue.length > 0); useEffect(() => { - // Activate or deactivate the label when value is changed programmatically from outside + // Handle side effects when the value gets changed programatically from the outside // 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 (inputValue === '') { input.current.clear(); } - if (inputValue || isFocused) { + if (inputValue) { + activateLabel() + } + }, [activateLabel, inputValue]) + + useEffect(() => { + // Activate or deactivate the label when the focus changes + + if (hasValueRef.current || isFocused) { activateLabel(); - } else if (!isFocused) { + } else if (!hasValueRef.current && !isFocused) { deactivateLabel(); } }, [activateLabel, deactivateLabel, inputValue, isFocused]); @@ -194,7 +203,12 @@ function BaseTextInput(props) { } Str.result(props.onChangeText, value); - activateLabel(); + if (value && value.length > 0) { + hasValueRef.current = true + activateLabel(); + } else { + hasValueRef.current = false + } }; const togglePasswordVisibility = useCallback(() => { diff --git a/src/stories/TextInput.stories.js b/src/stories/TextInput.stories.js index 2a9e862d6876..4c24e5b7904e 100644 --- a/src/stories/TextInput.stories.js +++ b/src/stories/TextInput.stories.js @@ -102,7 +102,11 @@ HintAndErrorInput.args = { // To use autoGrow we need to control the TextInput's value function AutoGrowSupportInput(args) { - const [value, setValue] = useState(''); + const [value, setValue] = useState(args.value || ''); + React.useEffect(() => { + setValue(args.value || '') + }, [args.value]) + return ( Date: Tue, 20 Jun 2023 09:28:33 +0200 Subject: [PATCH 23/28] add max width --- src/stories/TextInput.stories.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stories/TextInput.stories.js b/src/stories/TextInput.stories.js index 4c24e5b7904e..72467f0535bf 100644 --- a/src/stories/TextInput.stories.js +++ b/src/stories/TextInput.stories.js @@ -123,12 +123,13 @@ AutoGrowInput.args = { name: 'AutoGrow', placeholder: 'My placeholder text', autoGrow: true, - value: '', textInputContainerStyles: [ { minWidth: 150, + maxWidth: 500, }, ], + value: '', }; const AutoGrowHeightInput = AutoGrowSupportInput.bind({}); From 7c52921b1e1e0ec6f2ade41dbf88ea949115efa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 20 Jun 2023 09:51:57 +0200 Subject: [PATCH 24/28] prettier --- src/components/TextInput/BaseTextInput.js | 8 ++++---- src/stories/TextInput.stories.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 2c4f5a983e6e..b3fe23f99511 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -177,9 +177,9 @@ function BaseTextInput(props) { } if (inputValue) { - activateLabel() + activateLabel(); } - }, [activateLabel, inputValue]) + }, [activateLabel, inputValue]); useEffect(() => { // Activate or deactivate the label when the focus changes @@ -204,10 +204,10 @@ function BaseTextInput(props) { Str.result(props.onChangeText, value); if (value && value.length > 0) { - hasValueRef.current = true + hasValueRef.current = true; activateLabel(); } else { - hasValueRef.current = false + hasValueRef.current = false; } }; diff --git a/src/stories/TextInput.stories.js b/src/stories/TextInput.stories.js index 72467f0535bf..098828c65198 100644 --- a/src/stories/TextInput.stories.js +++ b/src/stories/TextInput.stories.js @@ -104,8 +104,8 @@ HintAndErrorInput.args = { function AutoGrowSupportInput(args) { const [value, setValue] = useState(args.value || ''); React.useEffect(() => { - setValue(args.value || '') - }, [args.value]) + setValue(args.value || ''); + }, [args.value]); return ( Date: Wed, 21 Jun 2023 12:06:09 +0200 Subject: [PATCH 25/28] make ref usage more explanatory --- src/components/TextInput/BaseTextInput.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index b3fe23f99511..63fa036d57f9 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -167,7 +167,6 @@ function BaseTextInput(props) { [props.autoGrowHeight, props.multiline], ); - const hasValueRef = useRef(inputValue.length > 0); useEffect(() => { // Handle side effects when the value gets changed programatically from the outside @@ -181,9 +180,15 @@ function BaseTextInput(props) { } }, [activateLabel, inputValue]); - useEffect(() => { - // Activate or deactivate the label when the focus changes + // We capture whether the input has a value or not in a ref. + // It gets updated when the text gets changed. + const hasValueRef = useRef(inputValue.length > 0); + // Activate or deactivate the label when the focus changes: + useEffect(() => { + // We can't use inputValue here directly, as it might contain + // the defaultValue, which doesn't get updated when the text changes. + // We can't use props.value either, as it might be undefined. if (hasValueRef.current || isFocused) { activateLabel(); } else if (!hasValueRef.current && !isFocused) { From 8db7706a8f1bb04eda424478c08e975d4e9545cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 2 Jul 2023 13:54:31 +0200 Subject: [PATCH 26/28] Repeat: https://github.com/Expensify/App/commit/f71a1f7c8bda15eab014f88d7575048819971a77 --- src/components/TextInput/BaseTextInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index a2893347be52..85d2b1b992c1 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -28,7 +28,7 @@ function BaseTextInput(props) { const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); - const [textInputHeight, setTextInputHeight] = useState(); + const [textInputHeight, setTextInputHeight] = useState(0); const [prefixWidth, setPrefixWidth] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); const [width, setWidth] = useState(); From 74dc4d006f6499af8c217b9e591cfc2907acd950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 2 Jul 2023 13:55:27 +0200 Subject: [PATCH 27/28] Repeat: https://github.com/Expensify/App/commit/a63eee4b177d4f57754baeab47fb58836592fb76#diff-511655d817607a5a2b12918a659da08129dbf4e74dc693f9a93f8ac9df7eb5ef --- src/components/TextInput/BaseTextInput.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 85d2b1b992c1..2522798bcc8f 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -350,6 +350,7 @@ function BaseTextInput(props) { style={styles.textInputIconContainer} onPress={togglePasswordVisibility} onMouseDown={(e) => e.preventDefault()} + accessibilityLabel={props.translate('common.visible')} > Date: Mon, 3 Jul 2023 19:29:12 +0200 Subject: [PATCH 28/28] Update src/components/TextInput/BaseTextInput.js Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- src/components/TextInput/BaseTextInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 2522798bcc8f..c9c61207f81e 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -252,7 +252,7 @@ function BaseTextInput(props) { style={[props.autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, maxHeight), !isMultiline && styles.componentHeightLarge, ...props.containerStyles]} >