-
Notifications
You must be signed in to change notification settings - Fork 148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(Amount): Negative Amount support #2320
Changes from 15 commits
a1d6a2a
97f6583
7095038
64fa503
7df6750
5805174
846b830
9b8097c
5179a54
57728d8
bd8c420
44358d3
397b172
25b8c38
e2bdb49
d9e314d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import type { ReactElement } from 'react'; | ||
import React from 'react'; | ||
import type { CurrencyCodeType } from '@razorpay/i18nify-js/currency'; | ||
import { formatNumber, formatNumberByParts } from '@razorpay/i18nify-js/currency'; | ||
import { formatNumberByParts } from '@razorpay/i18nify-js/currency'; | ||
import type { AmountTypeProps } from './amountTokens'; | ||
import { normalAmountSizes, subtleFontSizes, amountLineHeights } from './amountTokens'; | ||
import type { BaseTextProps } from '~components/Typography/BaseText/types'; | ||
|
@@ -19,6 +19,43 @@ import { Text } from '~components/Typography'; | |
import { opacity } from '~tokens/global'; | ||
import type { FontFamily } from '~tokens/global'; | ||
|
||
/** | ||
* Pollyfill function to get around the node 18 error | ||
* | ||
* This function is maintained by i18nify team. Reach out to them for any change regarding this. | ||
*/ | ||
const stripTrailingZerosFromParts = ( | ||
parts: ReturnType<typeof formatNumberByParts>, | ||
): ReturnType<typeof formatNumberByParts> => { | ||
const decimalPart = parts.rawParts | ||
.filter(({ type }) => type === 'fraction') | ||
.map(({ value }) => value) | ||
.join(''); | ||
|
||
const hasFraction = parts.rawParts.some(({ type }) => type === 'fraction'); | ||
|
||
if (hasFraction && /^0+$/.test(decimalPart)) { | ||
delete parts.decimal; | ||
delete parts.fraction; | ||
parts.rawParts = parts.rawParts.filter(({ type }) => type !== 'decimal' && type !== 'fraction'); | ||
} | ||
|
||
return parts; | ||
}; | ||
|
||
/** | ||
* Wrapper that uses pollyfill of i18nify team | ||
*/ | ||
const pollyfilledFormatNumberByParts: typeof formatNumberByParts = (value, options) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also since this is a polyfill are we conditionally calling it or are we always calling it regardless. eg: checking for formatToParts existence There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've put conditions inside this function itself. So we can always call this instead of |
||
const parts = formatNumberByParts(value, options); | ||
|
||
if (options?.intlOptions?.trailingZeroDisplay === 'stripIfInteger') { | ||
return stripTrailingZerosFromParts(parts); | ||
} | ||
|
||
return parts; | ||
}; | ||
|
||
type AmountCommonProps = { | ||
/** | ||
* The value to be rendered within the component. | ||
|
@@ -82,7 +119,7 @@ const getTextColorProps = ({ color }: { color: AmountProps['color'] }): ColorPro | |
return props; | ||
}; | ||
|
||
type AmountType = Partial<ReturnType<typeof formatNumberByParts>> & { formatted: string }; | ||
type AmountType = Partial<ReturnType<typeof formatNumberByParts>>; | ||
|
||
interface AmountValue extends Omit<AmountProps, 'value'> { | ||
amountValueColor: BaseTextProps['color']; | ||
|
@@ -141,35 +178,38 @@ const AmountValue = ({ | |
color={amountValueColor} | ||
lineHeight={amountLineHeights[type][size]} | ||
> | ||
{amount.formatted} | ||
{amount.integer} | ||
{amount.decimal} | ||
{amount.fraction} | ||
{amount.compact} | ||
</BaseText> | ||
); | ||
}; | ||
|
||
type FormatAmountWithSuffixType = { | ||
suffix: AmountProps['suffix']; | ||
value: number; | ||
currency: AmountProps['currency']; | ||
}; | ||
|
||
/** | ||
* Returns a parsed object based on the suffix passed in parameters | ||
* === Logic === | ||
* value = 12500.45 | ||
* if suffix === 'decimals' => { | ||
"formatted": "12,500.45", | ||
"integer": "12,500", | ||
"decimal": ".", | ||
"fraction": "45", | ||
"compact": "K", | ||
"isPrefixSymbol": false, | ||
"rawParts": [{"type": "integer","value": "12"},{"type": "group","value": ","},{"type": "integer","value": "500"},{"type": "decimal","value": "."},{"type": "fraction","value": "45"}] | ||
} | ||
* else if suffix === 'humanize' => { formatted: "1.2T" } | ||
* else => { formatted: "1,23,456" } | ||
* @returns {AmountType} | ||
*/ | ||
export const formatAmountWithSuffix = ({ | ||
export const getAmountByParts = ({ | ||
suffix, | ||
value, | ||
currency, | ||
}: FormatAmountWithSuffixType): AmountType => { | ||
try { | ||
switch (suffix) { | ||
|
@@ -179,40 +219,37 @@ export const formatAmountWithSuffix = ({ | |
maximumFractionDigits: 2, | ||
minimumFractionDigits: 2, | ||
}, | ||
}; | ||
return { | ||
...formatNumberByParts(value, options), | ||
formatted: formatNumber(value, options), | ||
}; | ||
currency, | ||
} as const; | ||
return pollyfilledFormatNumberByParts(value, options); | ||
} | ||
case 'humanize': { | ||
const formatted = formatNumber(value, { | ||
const options = { | ||
intlOptions: { | ||
notation: 'compact', | ||
maximumFractionDigits: 2, | ||
trailingZeroDisplay: 'stripIfInteger', | ||
}, | ||
}); | ||
return { | ||
formatted, | ||
}; | ||
currency, | ||
} as const; | ||
return pollyfilledFormatNumberByParts(value, options); | ||
} | ||
|
||
default: { | ||
const formatted = formatNumber(value, { | ||
const options = { | ||
intlOptions: { | ||
maximumFractionDigits: 0, | ||
roundingMode: 'floor', | ||
}, | ||
}); | ||
return { | ||
formatted, | ||
}; | ||
currency, | ||
} as const; | ||
return pollyfilledFormatNumberByParts(value, options); | ||
} | ||
} | ||
} catch (err: unknown) { | ||
return { | ||
formatted: `${value}`, | ||
integer: `${value}`, | ||
currency, | ||
}; | ||
} | ||
}; | ||
|
@@ -275,20 +312,11 @@ const _Amount = ({ | |
color, | ||
}); | ||
|
||
let isPrefixSymbol, currencySymbol; | ||
try { | ||
const byParts = formatNumberByParts(value, { | ||
currency, | ||
}); | ||
isPrefixSymbol = byParts.isPrefixSymbol; | ||
currencySymbol = byParts.currency; | ||
} catch (err: unknown) { | ||
isPrefixSymbol = true; | ||
currencySymbol = currency; | ||
} | ||
const renderedValue = getAmountByParts({ suffix, value, currency }); | ||
const isPrefixSymbol = renderedValue.isPrefixSymbol ?? true; | ||
const currencySymbol = renderedValue.currency ?? currency; | ||
|
||
const currencyPosition = isPrefixSymbol ? 'left' : 'right'; | ||
const renderedValue = formatAmountWithSuffix({ suffix, value }); | ||
const currencySymbolOrCode = currencyIndicator === 'currency-symbol' ? currencySymbol : currency; | ||
|
||
const currencyFontSize = isAffixSubtle | ||
|
@@ -309,6 +337,18 @@ const _Amount = ({ | |
flexDirection="row" | ||
position="relative" | ||
> | ||
{renderedValue.minusSign ? ( | ||
<BaseText | ||
fontSize={normalAmountSizes[type][size]} | ||
fontWeight={weight} | ||
lineHeight={amountLineHeights[type][size]} | ||
color={amountValueColor} | ||
as={isReactNative ? undefined : 'span'} | ||
marginX="spacing.2" | ||
> | ||
{renderedValue.minusSign} | ||
</BaseText> | ||
) : null} | ||
{currencyPosition === 'left' && ( | ||
<BaseText | ||
marginRight="spacing.1" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this come from i18nify library itself then instead of being in blade repo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did suggest them this but they are saying they'll just document it on their end in documentation and consumers will have to add it. Can discuss it later separately as well. Wanted to get it resolved earlier for negative amount thing. But yup overall I too feel this should be in their repo