Skip to content

Commit

Permalink
Merge pull request Expensify#25815 from akinwale/task-25578
Browse files Browse the repository at this point in the history
fix: handle forward-delete properly in the amount text input
  • Loading branch information
marcaaron authored Aug 29, 2023
2 parents a5ab459 + fd28e88 commit fd755ee
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 120 deletions.
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

0 comments on commit fd755ee

Please sign in to comment.