Skip to content

Commit

Permalink
feat(hooks): add use-currency-formatter
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsimao committed Sep 29, 2023
1 parent 302eb0b commit 9b66015
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 24 deletions.
4 changes: 2 additions & 2 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ function getAbsolutePath(value: string): any {
const config: StorybookConfig = {
stories: [
'../packages/components/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../packages/icons/**/src/stories/*.stories.@(js|jsx|mjs|ts|tsx)',
'../packages/hooks/src/**/*.stories.@(js|jsx|mjs|ts|tsx)'
'../packages/hooks/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../packages/icons/**/src/stories/*.stories.@(js|jsx|mjs|ts|tsx)'
],
addons: [
getAbsolutePath('@storybook/addon-links'),
Expand Down
5 changes: 3 additions & 2 deletions packages/components/src/TokenInput/BaseTokenInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useLabel } from '@react-aria/label';
import { mergeProps } from '@react-aria/utils';
import { forwardRef, ReactNode } from 'react';
import { useCurrencyFormatter } from '@interlay/hooks';

import { Flex } from '../Flex';
import { NumberInput, NumberInputProps } from '../NumberInput';
import { formatUSD } from '../utils/format';

import { TokenInputLabel } from './TokenInputLabel';
import { StyledUSDAdornment } from './TokenInput.style';
Expand Down Expand Up @@ -36,12 +36,13 @@ const BaseTokenInput = forwardRef<HTMLInputElement, BaseTokenInputProps>(
},
ref
): JSX.Element => {
const format = useCurrencyFormatter();
const { labelProps, fieldProps } = useLabel({ label, ...props });

const hasLabel = !!label || !!balance;

const bottomAdornment = valueUSD !== undefined && (
<StyledUSDAdornment $isDisabled={isDisabled}>{formatUSD(valueUSD, { compact: true })}</StyledUSDAdornment>
<StyledUSDAdornment $isDisabled={isDisabled}>{format(valueUSD)}</StyledUSDAdornment>
);

return (
Expand Down
6 changes: 4 additions & 2 deletions packages/components/src/TokenInput/TokenListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { formatUSD } from '../utils/format';
import { useCurrencyFormatter } from '@interlay/hooks';

import { CoinIcon } from '../CoinIcon';
import { Flex } from '../Flex';
import { useSelectModalContext } from '../Select/SelectModalContext';
Expand All @@ -11,6 +12,7 @@ type TokenListItemProps = { isDisabled?: boolean } & TokenData;

const TokenListItem = ({ balance, balanceUSD, value, tickers, isDisabled }: TokenListItemProps): JSX.Element => {
const isSelected = useSelectModalContext().selectedItem?.key === value && !isDisabled;
const format = useCurrencyFormatter();

return (
<>
Expand All @@ -21,7 +23,7 @@ const TokenListItem = ({ balance, balanceUSD, value, tickers, isDisabled }: Toke
<Flex alignItems='flex-end' direction='column' flex='0' gap='spacing2'>
<StyledListItemLabel $isSelected={isSelected}>{balance}</StyledListItemLabel>
<Span color='tertiary' size='s'>
{formatUSD(balanceUSD, { compact: true })}
{format(balanceUSD)}
</Span>
</Flex>
</>
Expand Down
18 changes: 0 additions & 18 deletions packages/components/src/utils/format.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
},
"dependencies": {
"@interlay/theme": "workspace:*",
"@internationalized/number": "^3.2.1",
"@react-aria/i18n": "^3.8.1",
"@react-aria/utils": "^3.19.0",
"decimal.js-light": "^2.5.1",
"formik": "^2.4.5",
"react-use": "^17.4.0"
},
Expand Down
1 change: 1 addition & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { useStyleProps } from './use-style-props';
export * from './use-form';
export type { MarginProps, StyleProps, UseStylePropsResult, StyledMarginProps } from './use-style-props';
export { useDOMRef } from './use-dom-ref';
export * from './use-currency-formatter';
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { renderHook } from '@testing-library/react';

import { useCurrencyFormatter } from '../use-currency-formatter';

describe('useCurrencyFormatter', () => {
describe('simple integer', () => {
it('should format 0 to $0.00', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(0)).toBe('$0.00');
});

it('should format 1 to $1.00', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(1)).toBe('$1.00');
});

it('should format 1.0001 to $1.00', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(1.0001)).toBe('$1.00');
});

it('should format 1.0001 to $1.00', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(1.0001)).toBe('$1.00');
});
});

describe('decimals', () => {
it('should format 0.1 to $0.100', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(0.1)).toBe('$0.100');
});

it('should format 0.0001 to $0.0001', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(0.0001)).toBe('$0.0001');
});

it('should format 0.0000000009 to $0.0000000009', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(0.0000000009)).toBe('$0.0000000009');
});

it('should format 0.00000000009 to $0.00000000009', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(0.00000000009)).toBe('<$0.00000001');
});
});

describe('big integer', () => {
it('should format 100000 to $100,000.00', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(100000)).toBe('$100,000.00');
});

it('should format 1000000 to $1M', () => {
const { result } = renderHook(() => useCurrencyFormatter());

expect(result.current(1000000)).toBe('$1M');
});
});

it('should not compact', () => {
const { result } = renderHook(() => useCurrencyFormatter({ compact: false }));

expect(result.current(0)).toBe('$0.00');
expect(result.current(0.001)).toBe('$0.00');
expect(result.current(1)).toBe('$1.00');
expect(result.current(1000000)).toBe('$1,000,000.00');
});

it('should format EUR', () => {
const { result } = renderHook(() => useCurrencyFormatter({ currency: 'EUR' }));

expect(result.current(0)).toBe('€0.00');
});
});
2 changes: 2 additions & 0 deletions packages/hooks/src/use-currency-formatter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useCurrencyFormatter } from './use-currency-formatter';
export type { UseCurrencyFormatterProps } from './use-currency-formatter';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { StoryObj } from '@storybook/react';

import { useCurrencyFormatter } from '../use-currency-formatter';

const Render = () => {
const format = useCurrencyFormatter();

return (
<div style={{ display: 'flex', gap: 32 }}>
<div>
<h1>Digits</h1>
<span>1 digit: {format(1.6801231)}</span>
<br />
<span>2 digit: {format(10.0001231)}</span>
<br />
<span>3 digit: {format(100.0001231)}</span>
<br />
<span>4 digit: {format(1000.0001231)}</span>
<br />
<span>5 digit: {format(10000.0001231)}</span>
<br />
<span>6 digit: {format(100000.0001231)}</span>
<br />
<span>7 digit: {format(1000000.0001231)}</span>
<br />
<span>8 digit: {format(10000000.0001231)}</span>
<br />
<span>9 digit: {format(100000000.0001231)}</span>
<br />
<span>10 digit: {format(1000000000.0001231)}</span>
<br />
<span>11 digit: {format(10000000000.0001231)}</span>
<br />
<span>12 digit: {format(100000000000.0001231)}</span>
<br />
<span>13 digit: {format(1000000000000.0001231)}</span>
</div>
<div>
<h1>Decimals</h1>
<span>1 decimal: {format(0.1)}</span>
<br />
<span>2 decimal: {format(0.01)}</span>
<br />
<span>3 decimal: {format(0.001)}</span>
<br />
<span>4 decimal: {format(0.0001)}</span>
<br />
<span>5 decimal: {format(0.00001)}</span>
<br />
<span>6 decimal: {format(0.000001)}</span>
<br />
<span>7 decimal: {format(0.0000001)}</span>
<br />
<span>8 decimal: {format(0.00000001)}</span>
<br />
<span>9 decimal: {format(0.000000001)}</span>
<br />
<span>10 decimal: {format(0.0000000001)}</span>
<br />
<span>11 decimal: {format(0.00000000001)}</span>
<br />
<span>12 decimal: {format(0.000000000001)}</span>
<br />
<span>13 decimal: {format(0.0000000000001)}</span>
</div>
</div>
);
};

export default {
title: 'Hooks/useCurrencyFormatter',
parameters: {
layout: 'centered'
},
args: {
style: { minWidth: 300 }
},
render: Render
};

export const Default: StoryObj = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useLocale } from '@react-aria/i18n';
import { useMemo } from 'react';
import { NumberFormatter } from '@internationalized/number';
import Decimal from 'decimal.js-light';

const decimalLimit = 0.00000000009;
const overDecimalLimitIndicator = 0.00000001;

type UseCurrencyFormatterProps = {
currency?: string;
compact?: boolean;
};

type UseCurrencyFormatterResult = NumberFormatter['format'];

const useCurrencyFormatter = (options: UseCurrencyFormatterProps = {}): UseCurrencyFormatterResult => {
let { locale } = useLocale();

const { compact: compactProp = true, currency = 'USD' } = options;

const compact = useMemo(
() =>
new NumberFormatter(locale, {
style: 'currency',
currency,
notation: 'compact'
}),
[locale, options]
);

const decimal = useMemo(() => {
const formatter = new NumberFormatter(locale, {
style: 'currency',
currency,
minimumFractionDigits: 3,
maximumFractionDigits: 11
});

return formatter;
}, [locale, options, compact]);

const standard = useMemo(() => {
const formatter = new NumberFormatter(locale, {
style: 'currency',
currency,
minimumFractionDigits: 2
});

return formatter;
}, [locale, options, compact]);

return (value) => {
if (!compactProp || value === 0) {
return standard.format(value);
}

// checks if decimal is lower than the limit
if (new Decimal(value).lte(decimalLimit)) {
return `<${decimal.format(overDecimalLimitIndicator)}`;
}

const length = value.toFixed(0).length;

const isOnlyDecimal = length === 1 && value < 1;

if (isOnlyDecimal) {
return decimal.format(value);
}

return length > 6 ? compact.format(value) : standard.format(value);
};
};

export { useCurrencyFormatter };
export type { UseCurrencyFormatterProps };
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9b66015

Please sign in to comment.