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

[CP Staging] Revert "Blur Magic Code input when click outside" #28611

Merged
merged 1 commit into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
168 changes: 54 additions & 114 deletions src/components/MagicCodeInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 */
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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));
Expand All @@ -259,86 +215,31 @@ 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);
}
};

return (
<>
<View style={[styles.magicCodeInputContainer]}>
<TapGestureHandler
onBegan={(e) => {
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 */}
<View
style={[StyleSheet.absoluteFillObject, styles.w100, styles.h100, styles.invisibleOverlay]}
collapsable={false}
>
<TextInput
onLayout={(e) => {
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]}
/>
</View>
</TapGestureHandler>
{_.map(getInputPlaceholderSlots(props.maxLength), (index) => (
<View
key={index}
Expand All @@ -354,6 +255,45 @@ function MagicCodeInput(props) {
>
<Text style={[styles.magicCodeInput, styles.textAlignCenter]}>{decomposeString(props.value, props.maxLength)[index] || ''}</Text>
</View>
{/* Hide the input above the text. Cannot set opacity to 0 as it would break pasting on iOS Safari. */}
<View style={[StyleSheet.absoluteFillObject, styles.w100, styles.bgTransparent]}>
<TextInput
ref={(ref) => {
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}
/>
</View>
</View>
))}
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ function BaseValidateCodeForm(props) {
const resendValidateCode = () => {
User.requestContactMethodValidateCode(props.contactMethod);
setValidateCode('');
inputValidateCodeRef.current.clear();
inputValidateCodeRef.current.focus();
};

Expand Down
Loading