diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index 7b92ced4..3720cc38 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -516,6 +516,14 @@ export function formatCurrencyStr( const currentLocale = getCurrentLocale(); switch (unit) { + case CurrencyUnit.MXN: + case CurrencyUnit.USD: + return num.toLocaleString(currentLocale, { + style: "currency", + currency: defaultCurrencyCode, + notation: compact ? ("compact" as const) : undefined, + maximumFractionDigits: getDefaultMaxFractionDigits(2, 2), + }); case CurrencyUnit.BITCOIN: /* In most cases product prefers 4 precision digtis for BTC. In a few places full precision (8 digits) are preferred, e.g. for a transaction details page: */ @@ -540,13 +548,6 @@ export function formatCurrencyStr( notation: compact ? ("compact" as const) : undefined, maximumFractionDigits: getDefaultMaxFractionDigits(0, 0), })}`; - case CurrencyUnit.USD: - return num.toLocaleString(currentLocale, { - style: "currency", - currency: defaultCurrencyCode, - notation: compact ? ("compact" as const) : undefined, - maximumFractionDigits: getDefaultMaxFractionDigits(2, 2), - }); } } diff --git a/packages/ui/src/components/CommaNumberInput.tsx b/packages/ui/src/components/CommaNumberInput.tsx index dcbf13bf..0884c964 100644 --- a/packages/ui/src/components/CommaNumberInput.tsx +++ b/packages/ui/src/components/CommaNumberInput.tsx @@ -10,23 +10,26 @@ import { } from "react"; import { addCommasToDigits, + addCommasToVariableDecimal, removeChars, removeCommas, removeLeadingZeros, + removeNonDigit, } from "../utils/strings.js"; import { TextInput, type TextInputProps } from "./TextInput.js"; export type CommaNumberInputProps = Omit & { onChange: (newValue: string) => void; maxValue?: number | undefined; + decimals?: number | undefined; }; export function CommaNumberInput(props: CommaNumberInputProps): ReactElement { - const { onChange, value, ...rest } = props; + const { onChange, value, decimals = 0, ...rest } = props; const inputRef: React.MutableRefObject = useRef(null); const cursorIndex = useRef(null); - const [valueWithCommas, setValueWithCommas] = useState(value); + const [valueWithCommaDecimal, setValueWithCommaDecimal] = useState(value); const handleOnChange = useCallback( (newValue: string, event: React.ChangeEvent | null) => { @@ -34,27 +37,36 @@ export function CommaNumberInput(props: CommaNumberInputProps): ReactElement { if (!inputRef.current) { return; } - const existingCommasRemoved = removeCommas(newValue); + const cursorPosition = inputRef.current.selectionStart || 0; + const existingCommasRemoved = removeNonDigit(newValue); const validInputChange = existingCommasRemoved.match(/^\d*$/); - if (validInputChange && validInputChange[0] !== value) { + if (validInputChange) { onChange(existingCommasRemoved); const leadingZerosRemoved = removeLeadingZeros(existingCommasRemoved); - const newValueWithCommas = addCommasToDigits(leadingZerosRemoved); - const diff = newValueWithCommas.length - valueWithCommas.length; - if (diff) { - cursorIndex.current = - (inputRef.current.selectionStart || 0) + (diff > 1 ? 1 : 0); - } + const decimalIfRelevant = + decimals > 0 + ? (Number(leadingZerosRemoved) / 10 ** decimals).toFixed(decimals) + : leadingZerosRemoved; + const newValueWithCommaDecimal = addCommasToDigits(decimalIfRelevant); + const cursorDistanceFromEnd = newValue.length - cursorPosition; + cursorIndex.current = + newValueWithCommaDecimal.length - cursorDistanceFromEnd; } }, - [onChange, valueWithCommas, value], + [onChange, decimals], ); useEffect(() => { const existingCommasRemoved = removeCommas(value); const leadingZerosRemoved = removeLeadingZeros(existingCommasRemoved); - setValueWithCommas(addCommasToDigits(leadingZerosRemoved)); - }, [value]); + setValueWithCommaDecimal( + addCommasToDigits( + decimals > 0 + ? (Number(leadingZerosRemoved) / 10 ** decimals).toFixed(decimals) + : leadingZerosRemoved, + ), + ); + }, [value, decimals]); useLayoutEffect(() => { if (cursorIndex.current !== null) { @@ -64,7 +76,7 @@ export function CommaNumberInput(props: CommaNumberInputProps): ReactElement { ); cursorIndex.current = null; } - }, [valueWithCommas]); + }, [valueWithCommaDecimal]); const handleKeyDown = useCallback( (keyValue: string, event: React.KeyboardEvent) => { @@ -84,46 +96,70 @@ export function CommaNumberInput(props: CommaNumberInputProps): ReactElement { if (selectionStart === 0 && isBackspace) { return; } - const isComma = - // if deleting a comma assume we want to delete the next character - valueWithCommas[selectionStart + (isBackspace ? -1 : 0)] === ","; + // if backspacing - the index we want to delete is 1 before current pos. + const toDeleteIdx = selectionStart + (isBackspace ? -1 : 0); + const isCommaDecimal = [",", "."].includes( + valueWithCommaDecimal[toDeleteIdx], + ); const deleteCharIndex = isBackspace - ? Math.max(selectionStart - (isComma ? 2 : 1), 0) - : selectionStart + (isComma ? 1 : 0); - newValue = addCommasToDigits( - removeChars(valueWithCommas, deleteCharIndex), + ? Math.max(selectionStart - (isCommaDecimal ? 2 : 1), 0) + : selectionStart + (isCommaDecimal ? 1 : 0); + newValue = addCommasToVariableDecimal( + removeChars(valueWithCommaDecimal, deleteCharIndex), + decimals, ); - const diff = valueWithCommas.length - newValue.length; - newCursorIndex = deleteCharIndex - (diff > 1 ? 1 : 0); + const diff = valueWithCommaDecimal.length - newValue.length; + if (diff > 1) { + // comma and number removed. move cursor back. + newCursorIndex = deleteCharIndex - 1; + } else if (diff === 1) { + newCursorIndex = deleteCharIndex; + } else if (diff === 0) { + // when decimals are present, there will be a fixed min length. + // Cursor needs to get incremented to account. + // given: 0.1X1 backspace at X, deleteCharIndex is 2. cursor is at 3. + // after backspace, 0.0X1 is expected, where cursor position is still 3. + newCursorIndex = deleteCharIndex + 1; + } } else { // deleting a range of characters - newValue = addCommasToDigits( - removeChars(valueWithCommas, selectionStart, selectionEnd), + newValue = addCommasToVariableDecimal( + removeChars(valueWithCommaDecimal, selectionStart, selectionEnd), + decimals, ); } - onChange(removeCommas(newValue)); + onChange(removeNonDigit(newValue)); cursorIndex.current = clamp(newCursorIndex, 0, newValue.length); } }, - [valueWithCommas, onChange], + [valueWithCommaDecimal, decimals, onChange], ); + const { maxValue, onRightButtonClick } = props; + const handleRightButtonClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + if (maxValue) { + handleOnChange(maxValue.toString(), null); + } + if (onRightButtonClick) { + onRightButtonClick(e); + } + }, + [handleOnChange, maxValue, onRightButtonClick], + ); return ( { - if (props.maxValue) { - handleOnChange(props.maxValue.toString(), null); - } - }} + onRightButtonClick={handleRightButtonClick} /> ); } diff --git a/packages/ui/src/components/TextInput.tsx b/packages/ui/src/components/TextInput.tsx index 56dff571..3a3e70d0 100644 --- a/packages/ui/src/components/TextInput.tsx +++ b/packages/ui/src/components/TextInput.tsx @@ -91,7 +91,7 @@ export type TextInputProps = { hintTooltip?: string | undefined; label?: string; rightButtonText?: string | undefined; - onRightButtonClick?: () => void; + onRightButtonClick?: (e: React.MouseEvent) => void; typography?: PartialSimpleTypographyProps | undefined; select?: | { diff --git a/packages/ui/src/utils/strings.tsx b/packages/ui/src/utils/strings.tsx index a9d74453..d7b3b44d 100644 --- a/packages/ui/src/utils/strings.tsx +++ b/packages/ui/src/utils/strings.tsx @@ -36,6 +36,27 @@ export function removeCommas(value: string): string { return value.replace(/,/g, ""); } +export function removeNonDigit(value: string): string { + // Remove any non-digit characters (including decimal point) + const numericValue = value.replace(/[^\d]/g, ""); + // Remove leading zeros + return numericValue.replace(/^0+/, "") || "0"; +} + +export function addCommasToVariableDecimal( + value: string | number, + decimals: number, +): string { + const val = removeNonDigit(value.toString()); + return decimals === 0 + ? addCommasToDigits(val) + : addCommasToDigits( + val.padStart(decimals + 1, "0").slice(0, -decimals) + + "." + + val.padStart(decimals + 1, "0").slice(-decimals), + ); +} + export function addCommasToDigits(value: string | number): string { if (typeof value === "number") { value = value.toString();