Skip to content

Commit

Permalink
Merge pull request #28611 from wojtus7/revert-24012-magic-code-blur
Browse files Browse the repository at this point in the history
[CP Staging] Revert "Blur Magic Code input when click outside"

(cherry picked from commit dccc07b)
  • Loading branch information
mountiny authored and OSBotify committed Oct 2, 2023
1 parent b9d51c0 commit 3ace98e
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 115 deletions.
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

0 comments on commit 3ace98e

Please sign in to comment.