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

fix: handle forward-delete properly in the amount text input #25815

Merged
merged 10 commits into from
Aug 29, 2023
5 changes: 5 additions & 0 deletions src/components/AmountTextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ const propTypes = {

/** Function to call when selection in text input is changed */
onSelectionChange: PropTypes.func,

/** Function to call to handle key presses in the text input */
onKeyPress: PropTypes.func,
};

const defaultProps = {
forwardedRef: undefined,
selection: undefined,
onSelectionChange: () => {},
onKeyPress: () => {},
};

function AmountTextInput(props) {
Expand All @@ -51,6 +55,7 @@ function AmountTextInput(props) {
selection={props.selection}
onSelectionChange={props.onSelectionChange}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
onKeyPress={props.onKeyPress}
/>
);
}
Expand Down
119 changes: 0 additions & 119 deletions src/components/TextInputWithCurrencySymbol.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import AmountTextInput from '../AmountTextInput';
import CurrencySymbolButton from '../CurrencySymbolButton';
import * as CurrencyUtils from '../../libs/CurrencyUtils';
import useLocalize from '../../hooks/useLocalize';
import * as MoneyRequestUtils from '../../libs/MoneyRequestUtils';
import * as textInputWithCurrencySymbolPropTypes from './textInputWithCurrencySymbolPropTypes';

function BaseTextInputWithCurrencySymbol(props) {
const {fromLocaleDigit} = useLocalize();
const currencySymbol = CurrencyUtils.getLocalizedCurrencySymbol(props.selectedCurrencyCode);
const isCurrencySymbolLTR = CurrencyUtils.isCurrencySymbolLTR(props.selectedCurrencyCode);

const currencySymbolButton = (
<CurrencySymbolButton
currencySymbol={currencySymbol}
onCurrencyButtonPress={props.onCurrencyButtonPress}
/>
);

/**
* Set a new amount value properly formatted
*
* @param {String} text - Changed text from user input
*/
const setFormattedAmount = (text) => {
const newAmount = MoneyRequestUtils.addLeadingZero(MoneyRequestUtils.replaceAllDigits(text, fromLocaleDigit));
props.onChangeAmount(newAmount);
};

const amountTextInput = (
<AmountTextInput
formattedAmount={props.formattedAmount}
onChangeAmount={setFormattedAmount}
placeholder={props.placeholder}
ref={props.forwardedRef}
selection={props.selection}
onSelectionChange={(e) => {
props.onSelectionChange(e);
}}
onKeyPress={props.onKeyPress}
/>
);

if (isCurrencySymbolLTR) {
return (
<>
{currencySymbolButton}
{amountTextInput}
</>
);
}

return (
<>
{amountTextInput}
{currencySymbolButton}
</>
);
}

BaseTextInputWithCurrencySymbol.propTypes = textInputWithCurrencySymbolPropTypes.propTypes;
BaseTextInputWithCurrencySymbol.defaultProps = textInputWithCurrencySymbolPropTypes.defaultProps;
BaseTextInputWithCurrencySymbol.displayName = 'BaseTextInputWithCurrencySymbol';

export default React.forwardRef((props, ref) => (
<BaseTextInputWithCurrencySymbol
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
));
37 changes: 37 additions & 0 deletions src/components/TextInputWithCurrencySymbol/index.android.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, {useState, useEffect} from 'react';
import BaseTextInputWithCurrencySymbol from './BaseTextInputWithCurrencySymbol';
import * as textInputWithCurrencySymbolPropTypes from './textInputWithCurrencySymbolPropTypes';

function TextInputWithCurrencySymbol(props) {
const [skipNextSelectionChange, setSkipNextSelectionChange] = useState(false);

useEffect(() => {
setSkipNextSelectionChange(true);
}, [props.formattedAmount]);

return (
<BaseTextInputWithCurrencySymbol
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onSelectionChange={(e) => {
if (skipNextSelectionChange) {
setSkipNextSelectionChange(false);
return;
}
props.onSelectionChange(e);
}}
/>
);
}

TextInputWithCurrencySymbol.propTypes = textInputWithCurrencySymbolPropTypes.propTypes;
TextInputWithCurrencySymbol.defaultProps = textInputWithCurrencySymbolPropTypes.defaultProps;
TextInputWithCurrencySymbol.displayName = 'TextInputWithCurrencySymbol';

export default React.forwardRef((props, ref) => (
<TextInputWithCurrencySymbol
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
));
24 changes: 24 additions & 0 deletions src/components/TextInputWithCurrencySymbol/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import BaseTextInputWithCurrencySymbol from './BaseTextInputWithCurrencySymbol';
import * as textInputWithCurrencySymbolPropTypes from './textInputWithCurrencySymbolPropTypes';

function TextInputWithCurrencySymbol(props) {
return (
<BaseTextInputWithCurrencySymbol
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);
}

TextInputWithCurrencySymbol.propTypes = textInputWithCurrencySymbolPropTypes.propTypes;
TextInputWithCurrencySymbol.defaultProps = textInputWithCurrencySymbolPropTypes.defaultProps;
TextInputWithCurrencySymbol.displayName = 'TextInputWithCurrencySymbol';

export default React.forwardRef((props, ref) => (
<TextInputWithCurrencySymbol
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import PropTypes from 'prop-types';
import refPropTypes from '../refPropTypes';

const propTypes = {
/** A ref to forward to amount text input */
forwardedRef: refPropTypes,

/** Formatted amount in local currency */
formattedAmount: PropTypes.string.isRequired,

/** Function to call when amount in text input is changed */
onChangeAmount: PropTypes.func,

/** Function to call when currency button is pressed */
onCurrencyButtonPress: PropTypes.func,

/** Placeholder value for amount text input */
placeholder: PropTypes.string.isRequired,

/** Currency code of user's selected currency */
selectedCurrencyCode: PropTypes.string.isRequired,

/** Selection Object */
selection: PropTypes.shape({
start: PropTypes.number,
end: PropTypes.number,
}),

/** Function to call when selection in text input is changed */
onSelectionChange: PropTypes.func,

/** Function to call to handle key presses in the text input */
onKeyPress: PropTypes.func,
};

const defaultProps = {
forwardedRef: undefined,
onChangeAmount: () => {},
onCurrencyButtonPress: () => {},
selection: undefined,
onSelectionChange: () => {},
onKeyPress: () => {},
};

export {propTypes, defaultProps};
24 changes: 23 additions & 1 deletion src/pages/iou/steps/MoneyRequestAmountForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import TextInputWithCurrencySymbol from '../../../components/TextInputWithCurren
import useLocalize from '../../../hooks/useLocalize';
import CONST from '../../../CONST';
import refPropTypes from '../../../components/refPropTypes';
import getOperatingSystem from '../../../libs/getOperatingSystem';
import * as Browser from '../../../libs/Browser';
import useWindowDimensions from '../../../hooks/useWindowDimensions';

const propTypes = {
Expand Down Expand Up @@ -75,6 +77,8 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
end: selectedAmountAsString.length,
});

const forwardDeletePressedRef = useRef(false);

/**
* Event occurs when a user presses a mouse button over an DOM element.
*
Expand Down Expand Up @@ -125,7 +129,8 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
}
setCurrentAmount((prevAmount) => {
const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces);
setSelection((prevSelection) => getNewSelection(prevSelection, prevAmount.length, strippedAmount.length));
const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current;
setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length));
return strippedAmount;
});
};
Expand Down Expand Up @@ -175,6 +180,22 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
onSubmitButtonPress(currentAmount);
}, [onSubmitButtonPress, currentAmount]);

/**
* Input handler to check for a forward-delete key (or keyboard shortcut) press.
*/
const textInputKeyPress = ({nativeEvent}) => {
const key = nativeEvent.key.toLowerCase();
if (Browser.isMobileSafari() && key === CONST.PLATFORM_SPECIFIC_KEYS.CTRL.DEFAULT) {
// Optimistically anticipate forward-delete on iOS Safari (in cases where the Mac Accessiblity keyboard is being
// used for input). If the Control-D shortcut doesn't get sent, the ref will still be reset on the next key press.
forwardDeletePressedRef.current = true;
return;
}
// Control-D on Mac is a keyboard shortcut for forward-delete. See https://support.apple.com/en-us/HT201236 for Mac keyboard shortcuts.
// Also check for the keyboard shortcut on iOS in cases where a hardware keyboard may be connected to the device.
forwardDeletePressedRef.current = key === 'delete' || (_.contains([CONST.OS.MAC_OS, CONST.OS.IOS], getOperatingSystem()) && nativeEvent.ctrlKey && key === 'd');
};

const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit);
const buttonText = isEditing ? translate('common.save') : translate('common.next');

Expand Down Expand Up @@ -207,6 +228,7 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
}
setSelection(e.nativeEvent.selection);
}}
onKeyPress={textInputKeyPress}
/>
</View>
<View
Expand Down
Loading