Skip to content

Commit d61bd81

Browse files
author
jorgen
committed
add formatter
1 parent a6d7c42 commit d61bd81

File tree

5 files changed

+187
-13
lines changed

5 files changed

+187
-13
lines changed

src/utils/format.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,3 +343,57 @@ export function parseUnitsWithExtendedDecimals(
343343
}
344344
return ethers.utils.parseUnits(valueToParse, decimals);
345345
}
346+
347+
export function formatNumberWithSeparators(
348+
value: string,
349+
maxDecimals: number = 18
350+
): string {
351+
if (!value || value === ".") return value;
352+
353+
const parts = value.split(".");
354+
const integerPart = parts[0] || "0";
355+
const decimalPart = parts[1];
356+
357+
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
358+
359+
if (parts.length === 2) {
360+
const limitedDecimals = decimalPart.substring(0, maxDecimals);
361+
return `${formattedInteger}.${limitedDecimals}`;
362+
} else if (value.endsWith(".")) {
363+
return `${formattedInteger}.`;
364+
}
365+
366+
return formattedInteger;
367+
}
368+
369+
export function parseFormattedNumber(value: string): string {
370+
return value.replace(/,/g, "");
371+
}
372+
373+
export function calculateCursorPosition(
374+
previousValue: string,
375+
newValue: string,
376+
previousCursor: number,
377+
wasDeleting: boolean
378+
): number {
379+
const commasBefore = (
380+
previousValue.slice(0, previousCursor).match(/,/g) || []
381+
).length;
382+
const rawCursorPosition = previousCursor - commasBefore;
383+
384+
let newCursor = 0;
385+
let rawPosition = 0;
386+
387+
for (let i = 0; i < newValue.length && rawPosition < rawCursorPosition; i++) {
388+
if (newValue[i] !== ",") {
389+
rawPosition++;
390+
}
391+
newCursor++;
392+
}
393+
394+
if (wasDeleting && newValue[newCursor] === ",") {
395+
newCursor++;
396+
}
397+
398+
return newCursor;
399+
}

src/views/SwapAndBridge/components/TokenInput/DestinationTokenInput.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg";
2+
import { FormattedTokenInput } from "./FormattedTokenInput";
23
import { ChangeAccountModal } from "../ChangeAccountModal";
34
import SelectorButton from "../ChainTokenSelector/SelectorButton";
45
import { BalanceSelector } from "../BalanceSelector";
56
import {
6-
TokenAmountInput,
77
TokenAmountInputTitle,
88
TokenAmountInputWrapper,
99
TokenAmountStack,
@@ -63,14 +63,15 @@ export const DestinationTokenInput = ({
6363
value={displayValue}
6464
error={false}
6565
>
66-
<TokenAmountInput
66+
<FormattedTokenInput
6767
id="destination-amount-input"
6868
name="destination-amount-input"
69-
placeholder="0.00"
7069
value={displayValue}
71-
onChange={(e) => handleInputChange(e.target.value)}
70+
onChange={handleInputChange}
71+
placeholder="0.00"
7272
disabled={inputDisabled}
7373
error={false}
74+
maxDecimals={18}
7475
/>
7576
</TokenAmountInputWrapper>
7677
<UnitToggleButtonWrapper>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { useRef, useEffect, ChangeEvent, forwardRef } from "react";
2+
import { TokenAmountInput } from "./styles";
3+
import {
4+
formatNumberWithSeparators,
5+
parseFormattedNumber,
6+
calculateCursorPosition,
7+
} from "utils/format";
8+
9+
interface FormattedTokenInputProps {
10+
id: string;
11+
name: string;
12+
value: string;
13+
onChange: (rawValue: string) => void;
14+
onFocus?: () => void;
15+
onBlur?: () => void;
16+
placeholder?: string;
17+
disabled?: boolean;
18+
error: boolean;
19+
maxDecimals?: number;
20+
"data-testid"?: string;
21+
}
22+
23+
export const FormattedTokenInput = forwardRef<
24+
HTMLInputElement,
25+
FormattedTokenInputProps
26+
>(
27+
(
28+
{
29+
id,
30+
name,
31+
value,
32+
onChange,
33+
onFocus,
34+
onBlur,
35+
placeholder = "0.00",
36+
disabled = false,
37+
error,
38+
maxDecimals = 18,
39+
"data-testid": dataTestId,
40+
},
41+
forwardedRef
42+
) => {
43+
const inputRef = useRef<HTMLInputElement>(null);
44+
const lastValueRef = useRef<string>(value);
45+
const lastCursorRef = useRef<number>(0);
46+
47+
useEffect(() => {
48+
if (forwardedRef) {
49+
if (typeof forwardedRef === "function") {
50+
forwardedRef(inputRef.current);
51+
} else {
52+
forwardedRef.current = inputRef.current;
53+
}
54+
}
55+
}, [forwardedRef]);
56+
57+
useEffect(() => {
58+
lastValueRef.current = value;
59+
}, [value]);
60+
61+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
62+
const input = e.target;
63+
const newRawValue = input.value;
64+
const cursorPosition = input.selectionStart || 0;
65+
66+
lastCursorRef.current = cursorPosition;
67+
68+
const unformatted = parseFormattedNumber(newRawValue);
69+
70+
if (unformatted && !/^\d*\.?\d*$/.test(unformatted)) {
71+
return;
72+
}
73+
74+
onChange(unformatted);
75+
};
76+
77+
useEffect(() => {
78+
if (inputRef.current && document.activeElement === inputRef.current) {
79+
const input = inputRef.current;
80+
81+
if (lastValueRef.current !== value) {
82+
const wasDeleting = value.length < lastValueRef.current.length;
83+
const newCursor = calculateCursorPosition(
84+
lastValueRef.current,
85+
value,
86+
lastCursorRef.current,
87+
wasDeleting
88+
);
89+
90+
input.setSelectionRange(newCursor, newCursor);
91+
}
92+
}
93+
}, [value]);
94+
95+
return (
96+
<TokenAmountInput
97+
ref={inputRef}
98+
id={id}
99+
name={name}
100+
data-testid={dataTestId}
101+
type="text"
102+
inputMode="decimal"
103+
placeholder={placeholder}
104+
value={value}
105+
onChange={handleChange}
106+
onFocus={onFocus}
107+
onBlur={onBlur}
108+
disabled={disabled}
109+
error={error}
110+
autoComplete="off"
111+
autoCorrect="off"
112+
spellCheck="false"
113+
/>
114+
);
115+
}
116+
);
117+
118+
FormattedTokenInput.displayName = "FormattedTokenInput";

src/views/SwapAndBridge/components/TokenInput/OriginTokenInput.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useEffect, useRef } from "react";
22
import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg";
3+
import { FormattedTokenInput } from "./FormattedTokenInput";
34
import SelectorButton from "../ChainTokenSelector/SelectorButton";
45
import { BalanceSelector } from "../BalanceSelector";
56
import {
6-
TokenAmountInput,
77
TokenAmountInputTitle,
88
TokenAmountInputWrapper,
99
TokenAmountStack,
@@ -79,16 +79,17 @@ export const OriginTokenInput = ({
7979
value={displayValue}
8080
error={insufficientBalance}
8181
>
82-
<TokenAmountInput
82+
<FormattedTokenInput
83+
ref={amountInputRef}
8384
id="origin-amount-input"
8485
name="origin-amount-input"
8586
data-testid="bridge-amount-input"
86-
ref={amountInputRef}
87-
placeholder="0.00"
8887
value={displayValue}
89-
onChange={(e) => handleInputChange(e.target.value)}
88+
onChange={handleInputChange}
89+
placeholder="0.00"
9090
disabled={inputDisabled}
9191
error={insufficientBalance}
92+
maxDecimals={18}
9293
/>
9394
</TokenAmountInputWrapper>
9495
<UnitToggleButtonWrapper>

src/views/SwapAndBridge/hooks/useTokenAmountInput.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
convertUSDToToken,
77
formatAmountForDisplay,
88
formatUSD,
9+
formatNumberWithSeparators,
910
isValidNumberInput,
1011
parseInputValue,
1112
} from "utils";
@@ -86,7 +87,9 @@ export const useTokenAmountInput = ({
8687
return "";
8788
}
8889

89-
return formatAmountForDisplay(amount, token, unit);
90+
const rawFormatted = formatAmountForDisplay(amount, token, unit);
91+
92+
return formatNumberWithSeparators(rawFormatted, 18);
9093
}, [
9194
isUserInput,
9295
isUpdateLoading,
@@ -153,9 +156,6 @@ export const useTokenAmountInput = ({
153156

154157
const handleInputChange = useCallback(
155158
(value: string) => {
156-
if (!isValidNumberInput(value)) {
157-
return;
158-
}
159159
handleSetInputValue(value);
160160
},
161161
[handleSetInputValue]

0 commit comments

Comments
 (0)