Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { USD_DENOM } from 'web-marketplace/src/config/allowedBaseDenoms';

import { localizeNumber } from '../inputs/new/EditableInput/EditableInput.utils';

export function SupCurrencyAndAmount({
price,
currencyCode,
Expand All @@ -12,9 +14,11 @@ export function SupCurrencyAndAmount({
return currencyCode && currencyCode === USD_DENOM ? (
<span>
<span className="align-top text-[11px] leading-normal">$</span>
<span className={className}>{Number(price).toFixed(2)}</span>
<span className={className}>
{localizeNumber(+Number(price).toFixed(2))}
</span>
</span>
) : (
<span className={className}>{price}</span>
<span className={className}>{localizeNumber(+price)}</span>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from 'react';
import { EditButtonIcon } from 'web-components/src/components/buttons/EditButtonIcon';
import { TextButton } from 'web-components/src/components/buttons/TextButton';

import { sanitizeValue } from './EditableInput.utils';
import { localizeNumber, sanitizeValue } from './EditableInput.utils';

interface EditableInputProps {
value: string;
Expand Down Expand Up @@ -41,12 +41,14 @@ export const EditableInput = ({
const [editable, setEditable] = useState(false);
const [initialValue, setInitialValue] = useState(value.toString());
const [currentValue, setCurrentValue] = useState(value);
const [decimalSeparator, setDecimalSeparator] = useState('.');
const wrapperRef = useRef(null);
const formattedValue = +currentValue.replace('.', ',');

const amountValid = +currentValue <= maxValue && +currentValue > 0;
const amountValid = formattedValue <= maxValue && formattedValue > 0;

const isUpdateDisabled =
!amountValid || error?.hasError || +initialValue === +currentValue;
!amountValid || error?.hasError || +initialValue === formattedValue;

useEffect(() => {
setInitialValue(value);
Expand Down Expand Up @@ -86,6 +88,11 @@ export const EditableInput = ({
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const value = e.target.value;
if (value.includes(',')) {
setDecimalSeparator(',');
} else {
setDecimalSeparator('.');
}
const sanitizedValue = sanitizeValue(value);
if (+sanitizedValue > maxValue && onInvalidValue) {
onInvalidValue();
Expand All @@ -102,7 +109,7 @@ export const EditableInput = ({

const handleOnUpdate = () => {
if (isUpdateDisabled) return;
onChange(+currentValue);
onChange(formattedValue);
toggleEditable();
};

Expand Down Expand Up @@ -132,7 +139,11 @@ export const EditableInput = ({
<input
type="text"
className="h-50 py-20 px-15 w-[100px] border border-solid border-grey-300 text-base font-normal font-sans focus:outline-none"
value={currentValue}
value={
decimalSeparator === ','
? currentValue.replace('.', ',')
: currentValue
}
onChange={handleOnChange}
onKeyDown={handleKeyDown}
aria-label={inputAriaLabel}
Expand Down Expand Up @@ -172,7 +183,7 @@ export const EditableInput = ({
</>
) : (
<div className="flex justify-between h-[47px] items-center">
<span>{currentValue}</span>
<span>{localizeNumber(+currentValue)}</span>
{isEditable && (
<EditButtonIcon
onClick={toggleEditable}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,71 @@
/*
* Sanitizes a string to ensure it represents a valid numeric format.
*
* @example
* sanitizeValue('.5') // returns '0.5'
* sanitizeValue('01.23') // returns '1.23'
* sanitizeValue('1,23') // returns '1,23'
* sanitizeValue('abc') // returns ''
* sanitizeValue('00') // returns '0'
*/

export const sanitizeValue = (value: string): string => {
// Convert a leading '.' or ',' to '0.' or '0,'
if (value.startsWith('.')) {
return '0.';
}
if (value === '0' || value.startsWith('0.')) {
// Disallow 0.[a-z]
if (/^0\.[a-zA-Z]/.test(value)) {
return '0.';
if (value.startsWith(',')) {
return '0,';
}

// Remove any character that is not a digit, comma, or period.
let sanitized = value.replace(/[^0-9.,]/g, '');

// Avoid having both a comma and a period at the same time
// If both comma and period exist, decide which one to keep by checking which comes first.
const commaIndex = sanitized.indexOf(',');
const periodIndex = sanitized.indexOf('.');
if (commaIndex !== -1 && periodIndex !== -1) {
if (commaIndex < periodIndex) {
// A comma appears first: remove all periods.
sanitized = sanitized.replace(/\./g, '');
} else {
// A period appears first: remove all commas.
sanitized = sanitized.replace(/,/g, '');
}
}

// Ensure only one instance of the remaining decimal separator is present.
if (sanitized.includes('.')) {
sanitized = sanitized.replace(/(\..*?)\..*/g, '$1');
}
if (sanitized.includes(',')) {
sanitized = sanitized.replace(/(,.*?),(.*)/g, '$1');
}

// Remove leading zeros, unless the number starts with '0.' or '0,'
if (!(sanitized.startsWith('0.') || sanitized.startsWith('0,'))) {
// If the entire string is zeros (e.g. "00"), return "0"
if (/^0+$/.test(sanitized)) {
sanitized = '0';
} else {
// If zeros are followed by other digits (e.g. "01"), remove the leading zeros.
sanitized = sanitized.replace(/^0+/, '');
}
return value.replace(/(\..*?)\..*/g, '$1');
}
// Strip leading zeros, non digits and multiple dots
const sanitized = value
.replace(/[^0-9.]/g, '')
.replace(/^0+/, '')
.replace(/(\..*?)\..*/g, '$1');

return sanitized ? sanitized : '';
};

export const localizeNumber = (
input: number,
locale = navigator.language || 'en-US',
) => {
const number = Number(input);

// Format the number for the given locale
return new Intl.NumberFormat(locale, {
useGrouping: true,
minimumFractionDigits: 2,
}).format(number);
};
4 changes: 3 additions & 1 deletion web-components/src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export function getFormattedNumber(
number: number,
options?: Intl.NumberFormatOptions | undefined,
): string {
return new Intl.NumberFormat('en-US', options).format(number);
return new Intl.NumberFormat(navigator.language || 'en-US', options).format(
number,
);
}

export function getFormattedPeriod(start: string, end: string | Date): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('OrderSummaryCard', () => {
it('displays the number of credits', () => {
render(<OrderSummaryCard {...orderSummary} />);

const numberOfCredits = screen.getByText('5');
const numberOfCredits = screen.getByText('5.00');
expect(numberOfCredits).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe('Order Component', () => {
/>,
);
expect(screen.getByText(/2000/i)).toBeInTheDocument();
expect(screen.getByText(/400000/i)).toBeInTheDocument();
expect(screen.getByText(/400,000.00/i)).toBeInTheDocument();
expect(screen.getAllByText(/usd/i)).toHaveLength(2);
});

Expand Down