Skip to content

Commit

Permalink
[ops] offramp-zh-fe and CommaNumberInput.tsx changes for variable dec…
Browse files Browse the repository at this point in the history
  • Loading branch information
ch-brian authored and Lightspark Eng committed Oct 30, 2024
1 parent 49f7b1e commit 0ee90a7
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 42 deletions.
15 changes: 8 additions & 7 deletions packages/core/src/utils/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: */
Expand All @@ -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),
});
}
}

Expand Down
104 changes: 70 additions & 34 deletions packages/ui/src/components/CommaNumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,63 @@ 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<TextInputProps, "onChange"> & {
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<HTMLInputElement | null> =
useRef<HTMLInputElement | null>(null);
const cursorIndex = useRef<number | null>(null);
const [valueWithCommas, setValueWithCommas] = useState(value);
const [valueWithCommaDecimal, setValueWithCommaDecimal] = useState(value);

const handleOnChange = useCallback(
(newValue: string, event: React.ChangeEvent<HTMLInputElement> | null) => {
event?.preventDefault();
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) {
Expand All @@ -64,7 +76,7 @@ export function CommaNumberInput(props: CommaNumberInputProps): ReactElement {
);
cursorIndex.current = null;
}
}, [valueWithCommas]);
}, [valueWithCommaDecimal]);

const handleKeyDown = useCallback(
(keyValue: string, event: React.KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -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<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
if (maxValue) {
handleOnChange(maxValue.toString(), null);
}
if (onRightButtonClick) {
onRightButtonClick(e);
}
},
[handleOnChange, maxValue, onRightButtonClick],
);
return (
<TextInput
placeholder="Enter a number"
{...rest}
onKeyDown={handleKeyDown}
onChange={handleOnChange}
value={valueWithCommas}
value={valueWithCommaDecimal}
inputRef={inputRef}
pattern="[0-9,]*"
pattern="[0-9,.]*"
inputMode="numeric"
rightButtonText={props.maxValue ? "Max" : undefined}
onRightButtonClick={() => {
if (props.maxValue) {
handleOnChange(props.maxValue.toString(), null);
}
}}
onRightButtonClick={handleRightButtonClick}
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export type TextInputProps = {
hintTooltip?: string | undefined;
label?: string;
rightButtonText?: string | undefined;
onRightButtonClick?: () => void;
onRightButtonClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
typography?: PartialSimpleTypographyProps | undefined;
select?:
| {
Expand Down
21 changes: 21 additions & 0 deletions packages/ui/src/utils/strings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 0ee90a7

Please sign in to comment.