diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 1db1acddc5d7..454aacc8a03b 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -2,7 +2,6 @@ import React, {useEffect, useImperativeHandle, useRef, useState, forwardRef} fro import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import {TapGestureHandler} from 'react-native-gesture-handler'; import styles from '../styles/styles'; import * as StyleUtils from '../styles/StyleUtils'; import * as ValidationUtils from '../libs/ValidationUtils'; @@ -13,9 +12,6 @@ import FormHelpMessage from './FormHelpMessage'; import {withNetwork} from './OnyxProvider'; import networkPropTypes from './networkPropTypes'; import useNetwork from '../hooks/useNetwork'; -import * as Browser from '../libs/Browser'; - -const TEXT_INPUT_EMPTY_STATE = ''; const propTypes = { /** Information about the network */ @@ -95,40 +91,22 @@ const composeToString = (value) => _.map(value, (v) => (v === undefined || v === const getInputPlaceholderSlots = (length) => Array.from(Array(length).keys()); function MagicCodeInput(props) { - const inputRefs = useRef(); - const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); + const inputRefs = useRef([]); + const [input, setInput] = useState(''); const [focusedIndex, setFocusedIndex] = useState(0); const [editIndex, setEditIndex] = useState(0); - const shouldFocusLast = useRef(false); - const inputWidth = useRef(0); - const lastFocusedIndex = useRef(0); const blurMagicCodeInput = () => { - inputRefs.current.blur(); + inputRefs.current[editIndex].blur(); setFocusedIndex(undefined); }; - const focusMagicCodeInput = () => { - setFocusedIndex(0); - lastFocusedIndex.current = 0; - setEditIndex(0); - inputRefs.current.focus(); - }; - useImperativeHandle(props.innerRef, () => ({ focus() { - focusMagicCodeInput(); - }, - resetFocus() { - setInput(TEXT_INPUT_EMPTY_STATE); - focusMagicCodeInput(); + inputRefs.current[0].focus(); }, clear() { - setInput(TEXT_INPUT_EMPTY_STATE); - setFocusedIndex(0); - lastFocusedIndex.current = 0; - setEditIndex(0); - inputRefs.current.focus(); + inputRefs.current[0].focus(); props.onChangeText(''); }, blur() { @@ -159,37 +137,17 @@ function MagicCodeInput(props) { }, [props.value, props.shouldSubmitOnComplete]); /** - * Focuses on the input when it is pressed. + * Callback for the onFocus event, updates the indexes + * of the currently focused input. * * @param {Object} event * @param {Number} index */ - const onFocus = (event) => { - if (shouldFocusLast.current) { - setInput(TEXT_INPUT_EMPTY_STATE); - setFocusedIndex(lastFocusedIndex.current); - setEditIndex(lastFocusedIndex.current); - } + const onFocus = (event, index) => { event.preventDefault(); - }; - - /** - * Callback for the onPress event, updates the indexes - * of the currently focused input. - * - * @param {Number} index - */ - const onPress = (index) => { - shouldFocusLast.current = false; - // TapGestureHandler works differently on mobile web and native app - // On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually - if (!Browser.isMobileChrome() && !Browser.isMobileSafari()) { - inputRefs.current.focus(); - } - setInput(TEXT_INPUT_EMPTY_STATE); + setInput(''); setFocusedIndex(index); setEditIndex(index); - lastFocusedIndex.current = index; }; /** @@ -217,9 +175,7 @@ function MagicCodeInput(props) { let numbers = decomposeString(props.value, props.maxLength); numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, props.maxLength)]; - setFocusedIndex(updatedFocusedIndex); - setEditIndex(updatedFocusedIndex); - setInput(TEXT_INPUT_EMPTY_STATE); + inputRefs.current[updatedFocusedIndex].focus(); const finalInput = composeToString(numbers); props.onChangeText(finalInput); @@ -240,7 +196,7 @@ function MagicCodeInput(props) { // If the currently focused index already has a value, it will delete // that value but maintain the focus on the same input. if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { - setInput(TEXT_INPUT_EMPTY_STATE); + setInput(''); numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, props.maxLength)]; setEditIndex(focusedIndex); props.onChangeText(composeToString(numbers)); @@ -259,37 +215,24 @@ function MagicCodeInput(props) { } const newFocusedIndex = Math.max(0, focusedIndex - 1); - - // Saves the input string so that it can compare to the change text - // event that will be triggered, this is a workaround for mobile that - // triggers the change text on the event after the key press. - setInput(TEXT_INPUT_EMPTY_STATE); - setFocusedIndex(newFocusedIndex); - setEditIndex(newFocusedIndex); props.onChangeText(composeToString(numbers)); if (!_.isUndefined(newFocusedIndex)) { - inputRefs.current.focus(); + inputRefs.current[newFocusedIndex].focus(); } } if (keyValue === 'ArrowLeft' && !_.isUndefined(focusedIndex)) { const newFocusedIndex = Math.max(0, focusedIndex - 1); - setInput(TEXT_INPUT_EMPTY_STATE); - setFocusedIndex(newFocusedIndex); - setEditIndex(newFocusedIndex); - inputRefs.current.focus(); + inputRefs.current[newFocusedIndex].focus(); } else if (keyValue === 'ArrowRight' && !_.isUndefined(focusedIndex)) { const newFocusedIndex = Math.min(focusedIndex + 1, props.maxLength - 1); - setInput(TEXT_INPUT_EMPTY_STATE); - setFocusedIndex(newFocusedIndex); - setEditIndex(newFocusedIndex); - inputRefs.current.focus(); + inputRefs.current[newFocusedIndex].focus(); } else if (keyValue === 'Enter') { // We should prevent users from submitting when it's offline. if (props.network.isOffline) { return; } - setInput(TEXT_INPUT_EMPTY_STATE); + setInput(''); props.onFulfill(props.value); } }; @@ -297,48 +240,6 @@ function MagicCodeInput(props) { return ( <> - { - onPress(Math.floor(e.nativeEvent.x / (inputWidth.current / props.maxLength))); - }} - > - {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */} - - { - inputWidth.current = e.nativeEvent.layout.width; - }} - ref={(ref) => (inputRefs.current = ref)} - autoFocus={props.autoFocus} - inputMode="numeric" - textContentType="oneTimeCode" - name={props.name} - maxLength={props.maxLength} - value={input} - hideFocusedState - autoComplete={props.autoComplete} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onChangeText={(value) => { - onChangeText(value); - }} - onKeyPress={onKeyPress} - onFocus={onFocus} - onBlur={() => { - shouldFocusLast.current = true; - lastFocusedIndex.current = focusedIndex; - setFocusedIndex(undefined); - }} - selectionColor="transparent" - inputStyle={[styles.inputTransparent]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} - style={[styles.inputTransparent]} - textInputContainerStyles={[styles.borderNone]} - /> - - {_.map(getInputPlaceholderSlots(props.maxLength), (index) => ( {decomposeString(props.value, props.maxLength)[index] || ''} + {/* Hide the input above the text. Cannot set opacity to 0 as it would break pasting on iOS Safari. */} + + { + inputRefs.current[index] = ref; + // Setting attribute type to "search" to prevent Password Manager from appearing in Mobile Chrome + if (ref && ref.setAttribute) { + ref.setAttribute('type', 'search'); + } + }} + autoFocus={index === 0 && props.autoFocus} + inputMode="numeric" + textContentType="oneTimeCode" + name={props.name} + maxLength={props.maxLength} + value={input} + hideFocusedState + autoComplete={index === 0 ? props.autoComplete : 'off'} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + onChangeText={(value) => { + // Do not run when the event comes from an input that is + // not currently being responsible for the input, this is + // necessary to avoid calls when the input changes due to + // deleted characters. Only happens in mobile. + if (index !== editIndex || _.isUndefined(focusedIndex)) { + return; + } + onChangeText(value); + }} + onKeyPress={onKeyPress} + onFocus={(event) => onFocus(event, index)} + // Manually set selectionColor to make caret transparent. + // We cannot use caretHidden as it breaks the pasting function on Android. + selectionColor="transparent" + textInputContainerStyles={[styles.borderNone]} + inputStyle={[styles.inputTransparent]} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + /> + ))} diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js index 300bd23cc2e5..bcea33d9c366 100644 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js @@ -111,7 +111,6 @@ function BaseValidateCodeForm(props) { const resendValidateCode = () => { User.requestContactMethodValidateCode(props.contactMethod); setValidateCode(''); - inputValidateCodeRef.current.clear(); inputValidateCodeRef.current.focus(); };