@sproutsocial/seeds-react-numeral 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +12 -0
- package/dist/esm/index.js +154 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +192 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +13 -0
- package/package.json +46 -0
- package/src/Numeral.stories.tsx +169 -0
- package/src/Numeral.tsx +208 -0
- package/src/NumeralTypes.ts +27 -0
- package/src/__tests__/Numeral.typetest.tsx +32 -0
- package/src/__tests__/features/A11y.test.tsx +11 -0
- package/src/__tests__/features/abbreviate.test.ts +215 -0
- package/src/__tests__/features/currency.test.ts +122 -0
- package/src/__tests__/features/defaults.test.ts +231 -0
- package/src/__tests__/features/invalid.test.ts +47 -0
- package/src/__tests__/features/locale.test.ts +417 -0
- package/src/__tests__/features/precision.test.ts +452 -0
- package/src/__tests__/features/testNumeral.tsx +82 -0
- package/src/__tests__/features/zero.test.ts +51 -0
- package/src/constants.ts +16 -0
- package/src/index.ts +6 -0
- package/src/styled.d.ts +7 -0
- package/src/styles.ts +12 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Numeral from "./Numeral";
|
|
3
|
+
|
|
4
|
+
const localeOptions = [
|
|
5
|
+
"United States (en-US)",
|
|
6
|
+
"English (en)",
|
|
7
|
+
"Arabic (ar-EG)",
|
|
8
|
+
"Brazil (pt-BR)",
|
|
9
|
+
"India (en-IN)",
|
|
10
|
+
"French (fr-FR)",
|
|
11
|
+
"Spain (es-ES)",
|
|
12
|
+
"Mexico (es-MX)",
|
|
13
|
+
"Germany (de-DE)",
|
|
14
|
+
"German (de)",
|
|
15
|
+
"Japan (ja-JP)",
|
|
16
|
+
"Japanese (ja)",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const localeMapping = {
|
|
20
|
+
"United States (en-US)": "en-US",
|
|
21
|
+
"English (en)": "en",
|
|
22
|
+
"Arabic (ar-EG)": "ar-EG",
|
|
23
|
+
"Brazil (pt-BR)": "pt-BR",
|
|
24
|
+
"India (en-IN)": "en-IN",
|
|
25
|
+
"French (fr-FR)": "fr-FR",
|
|
26
|
+
"Spain (es-ES)": "es-ES",
|
|
27
|
+
"Mexico (es-MX)": "es-MX",
|
|
28
|
+
"Germany (de-DE)": "de-DE",
|
|
29
|
+
"German (de)": "de",
|
|
30
|
+
"Japan (ja-JP)": "ja-JP",
|
|
31
|
+
"Japanese (ja)": "ja",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const currencyOptions = [
|
|
35
|
+
"Egyptian £",
|
|
36
|
+
"European €",
|
|
37
|
+
"Indian ₹",
|
|
38
|
+
"Japanese ¥",
|
|
39
|
+
"USA $",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const currencyMapping = {
|
|
43
|
+
"Egyptian £": "EGP",
|
|
44
|
+
"European €": "EUR",
|
|
45
|
+
"Indian ₹": "INR",
|
|
46
|
+
"Japanese ¥": "JPY",
|
|
47
|
+
"USA $": "USD",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const formatOptions = ["decimal", "currency", "percent"];
|
|
51
|
+
|
|
52
|
+
const formatMapping = {
|
|
53
|
+
decimal: "decimal",
|
|
54
|
+
currency: "currency",
|
|
55
|
+
percent: "percent",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const abbreviateOptions = [
|
|
59
|
+
"true",
|
|
60
|
+
"false",
|
|
61
|
+
" 500",
|
|
62
|
+
"1_000",
|
|
63
|
+
"10_000",
|
|
64
|
+
"100_000",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const abbreviateMapping = {
|
|
68
|
+
true: true,
|
|
69
|
+
false: false,
|
|
70
|
+
" 500": 500,
|
|
71
|
+
"1_000": 1000,
|
|
72
|
+
"10_000": 10000,
|
|
73
|
+
"100_000": 100000,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const precisionOptions = ["0", "1", "2", "3", "6", "none"];
|
|
77
|
+
|
|
78
|
+
const precisionMapping = {
|
|
79
|
+
"0": 0,
|
|
80
|
+
"1": 1,
|
|
81
|
+
"2": 2,
|
|
82
|
+
"3": 3,
|
|
83
|
+
"6": 6,
|
|
84
|
+
none: "none",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const meta: Meta<typeof Numeral> = {
|
|
88
|
+
title: "Components/Numeral",
|
|
89
|
+
component: Numeral,
|
|
90
|
+
argTypes: {
|
|
91
|
+
locale: {
|
|
92
|
+
control: "select",
|
|
93
|
+
options: localeOptions,
|
|
94
|
+
mapping: localeMapping,
|
|
95
|
+
},
|
|
96
|
+
format: {
|
|
97
|
+
control: "select",
|
|
98
|
+
options: formatOptions,
|
|
99
|
+
mapping: formatMapping,
|
|
100
|
+
},
|
|
101
|
+
currency: {
|
|
102
|
+
control: "select",
|
|
103
|
+
options: currencyOptions,
|
|
104
|
+
mapping: currencyMapping,
|
|
105
|
+
},
|
|
106
|
+
abbreviate: {
|
|
107
|
+
control: "select",
|
|
108
|
+
options: abbreviateOptions,
|
|
109
|
+
mapping: abbreviateMapping,
|
|
110
|
+
},
|
|
111
|
+
precision: {
|
|
112
|
+
control: "select",
|
|
113
|
+
options: precisionOptions,
|
|
114
|
+
mapping: precisionMapping,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
args: {
|
|
118
|
+
color: "text.headline",
|
|
119
|
+
number: 12.89,
|
|
120
|
+
invalidNumberLabel: "Not available",
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
export default meta;
|
|
124
|
+
|
|
125
|
+
type Story = StoryObj<typeof Numeral>;
|
|
126
|
+
|
|
127
|
+
export const Default: Story = {};
|
|
128
|
+
|
|
129
|
+
export const Total: Story = {
|
|
130
|
+
args: {
|
|
131
|
+
number: 100,
|
|
132
|
+
fontWeight: "semibold",
|
|
133
|
+
fontSize: 500,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const Trend: Story = {
|
|
138
|
+
args: {
|
|
139
|
+
number: 100,
|
|
140
|
+
color: "teal.500",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const NoPrecision: Story = {
|
|
145
|
+
args: {
|
|
146
|
+
number: 123.45678,
|
|
147
|
+
precision: "none",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const CurrencyPrecision: Story = {
|
|
152
|
+
args: {
|
|
153
|
+
number: 123.4,
|
|
154
|
+
format: "currency",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const Invalid: Story = {
|
|
159
|
+
args: {
|
|
160
|
+
number: null,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const AbbreviatedNegative: Story = {
|
|
165
|
+
args: {
|
|
166
|
+
number: -123456.789,
|
|
167
|
+
abbreviate: true,
|
|
168
|
+
},
|
|
169
|
+
};
|
package/src/Numeral.tsx
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
// @ts-expect-error lru-memoize is not typed
|
|
3
|
+
import memoize from "lru-memoize";
|
|
4
|
+
import { EM_DASH } from "./constants";
|
|
5
|
+
import Tooltip from "@sproutsocial/seeds-react-tooltip";
|
|
6
|
+
import type { TypeTextProps } from "@sproutsocial/seeds-react-text";
|
|
7
|
+
import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_THRESHOLD,
|
|
11
|
+
MEMO_CACHE_SIZE,
|
|
12
|
+
COMPARE_OBJECTS,
|
|
13
|
+
MAX_PRECISION,
|
|
14
|
+
ABBREV_PRECISION,
|
|
15
|
+
DefaultPrecisions,
|
|
16
|
+
} from "./constants";
|
|
17
|
+
import { AbbrContainer, Container } from "./styles";
|
|
18
|
+
import type { EnumNumeralFormat, TypeNumeralProps } from "./NumeralTypes";
|
|
19
|
+
|
|
20
|
+
interface TypeFormatOptions {
|
|
21
|
+
locale: string;
|
|
22
|
+
format: EnumNumeralFormat;
|
|
23
|
+
currency: string;
|
|
24
|
+
min: number;
|
|
25
|
+
max: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface TypeFormatters {
|
|
29
|
+
standard: Intl.NumberFormat;
|
|
30
|
+
abbreviated: Intl.NumberFormat;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface TypeArgs {
|
|
34
|
+
value: number;
|
|
35
|
+
canAbbreviate: boolean;
|
|
36
|
+
invalidNumberLabel?: string;
|
|
37
|
+
ariaLabel?: string;
|
|
38
|
+
options: TypeFormatOptions;
|
|
39
|
+
qa: object | null | undefined;
|
|
40
|
+
rest: Omit<TypeTextProps, "children">;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const _getNumberFormatters = (options: TypeFormatOptions): TypeFormatters => {
|
|
44
|
+
const { locale, format, currency, min, max } = options;
|
|
45
|
+
const compactPrecision = min === max ? min : ABBREV_PRECISION;
|
|
46
|
+
|
|
47
|
+
const _currency = format === "currency" ? currency : undefined;
|
|
48
|
+
|
|
49
|
+
const standard: Intl.NumberFormatOptions = {
|
|
50
|
+
style: format,
|
|
51
|
+
minimumFractionDigits: min,
|
|
52
|
+
maximumFractionDigits: max,
|
|
53
|
+
currency: _currency,
|
|
54
|
+
};
|
|
55
|
+
const compact: Intl.NumberFormatOptions = {
|
|
56
|
+
style: format,
|
|
57
|
+
minimumFractionDigits: compactPrecision,
|
|
58
|
+
maximumFractionDigits: compactPrecision,
|
|
59
|
+
currency: _currency,
|
|
60
|
+
notation: "compact",
|
|
61
|
+
};
|
|
62
|
+
// Safari 14.1 is currently throwing errors when trying to use the compact
|
|
63
|
+
// options of NumberFormat
|
|
64
|
+
// https://community.atlassian.com/t5/Trello-questions/Trello-stuck-at-loading-after-Safari-14-1-update-on-macOS-Mojave/qaq-p/1675577#M45687
|
|
65
|
+
let abbreviated;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
abbreviated = new Intl.NumberFormat(locale, compact);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
abbreviated = new Intl.NumberFormat(locale, standard);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
standard: new Intl.NumberFormat(locale, standard),
|
|
75
|
+
abbreviated,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Memoize to reduce the energy of creating new instances of Intl.NumberFormat
|
|
80
|
+
const memoizer = memoize(MEMO_CACHE_SIZE, COMPARE_OBJECTS);
|
|
81
|
+
const getNumberFormatters = memoizer(_getNumberFormatters);
|
|
82
|
+
|
|
83
|
+
const getThreshold = (abbreviate: boolean | number): number => {
|
|
84
|
+
if (typeof abbreviate === "number")
|
|
85
|
+
return Math.max(1000, Math.abs(abbreviate));
|
|
86
|
+
if (abbreviate) return DEFAULT_THRESHOLD;
|
|
87
|
+
return Infinity;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const getMinMaxPrecision = (
|
|
91
|
+
precision: TypeNumeralProps["precision"],
|
|
92
|
+
format: EnumNumeralFormat
|
|
93
|
+
): [number, number] => {
|
|
94
|
+
if (typeof precision === "number") return [precision, precision];
|
|
95
|
+
if (precision === "none") return [0, MAX_PRECISION];
|
|
96
|
+
return DefaultPrecisions[format];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isValidNumber = (value: unknown): boolean => {
|
|
100
|
+
return typeof value === "number" && isFinite(value);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const normalizeArgs = (props: TypeNumeralProps): TypeArgs => {
|
|
104
|
+
const {
|
|
105
|
+
number,
|
|
106
|
+
locale = "us-EN",
|
|
107
|
+
format = props.currency ? "currency" : "decimal",
|
|
108
|
+
currency = "USD",
|
|
109
|
+
abbreviate = true,
|
|
110
|
+
invalidNumberLabel,
|
|
111
|
+
precision,
|
|
112
|
+
qa,
|
|
113
|
+
...rest
|
|
114
|
+
} = props;
|
|
115
|
+
const threshold = getThreshold(abbreviate);
|
|
116
|
+
const [min, max] = getMinMaxPrecision(precision, format);
|
|
117
|
+
|
|
118
|
+
const _number = number || 0;
|
|
119
|
+
|
|
120
|
+
const value = _number * (format === "percent" ? 0.01 : 1);
|
|
121
|
+
const canAbbreviate = Math.abs(_number) >= threshold;
|
|
122
|
+
const options = {
|
|
123
|
+
locale,
|
|
124
|
+
format,
|
|
125
|
+
currency,
|
|
126
|
+
min,
|
|
127
|
+
max,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
value,
|
|
132
|
+
canAbbreviate,
|
|
133
|
+
invalidNumberLabel,
|
|
134
|
+
options,
|
|
135
|
+
qa,
|
|
136
|
+
rest,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const getNumeral = ({
|
|
141
|
+
returnType,
|
|
142
|
+
props,
|
|
143
|
+
}: {
|
|
144
|
+
returnType: "string" | "component";
|
|
145
|
+
props: TypeNumeralProps;
|
|
146
|
+
}): string | React.ReactNode => {
|
|
147
|
+
const isReturnTypeString = returnType === "string";
|
|
148
|
+
const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } =
|
|
149
|
+
normalizeArgs(props);
|
|
150
|
+
|
|
151
|
+
if (!isValidNumber(props.number)) {
|
|
152
|
+
return isReturnTypeString ? (
|
|
153
|
+
EM_DASH
|
|
154
|
+
) : (
|
|
155
|
+
<>
|
|
156
|
+
{invalidNumberLabel && (
|
|
157
|
+
// Give screen readers something useful to read off + hide the em dash
|
|
158
|
+
<VisuallyHidden>{invalidNumberLabel}</VisuallyHidden>
|
|
159
|
+
)}
|
|
160
|
+
<Container aria-hidden {...qa}>
|
|
161
|
+
{EM_DASH}
|
|
162
|
+
</Container>
|
|
163
|
+
</>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const formatters = getNumberFormatters(options);
|
|
168
|
+
const fullText = formatters.standard.format(value);
|
|
169
|
+
|
|
170
|
+
if (canAbbreviate) {
|
|
171
|
+
const abbreviatedText = formatters.abbreviated.format(value);
|
|
172
|
+
|
|
173
|
+
// The following are used to debug the skipped tests which are misbehaving!!!
|
|
174
|
+
// console.log({ fullText, abbreviatedText });
|
|
175
|
+
// console.log({ abbreviated: formatters.abbreviated.resolvedOptions() });
|
|
176
|
+
// The following check is necessary because each locale may have differing thresholds
|
|
177
|
+
// for which abbreviation begins.
|
|
178
|
+
if (abbreviatedText !== fullText) {
|
|
179
|
+
return isReturnTypeString ? (
|
|
180
|
+
abbreviatedText
|
|
181
|
+
) : (
|
|
182
|
+
<Tooltip content={fullText}>
|
|
183
|
+
<AbbrContainer {...qa} {...rest}>
|
|
184
|
+
{abbreviatedText}
|
|
185
|
+
</AbbrContainer>
|
|
186
|
+
</Tooltip>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return isReturnTypeString ? (
|
|
192
|
+
fullText
|
|
193
|
+
) : (
|
|
194
|
+
<Container {...qa} {...rest}>
|
|
195
|
+
{fullText}
|
|
196
|
+
</Container>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const formatNumeral = (props: TypeNumeralProps): string => {
|
|
201
|
+
return getNumeral({ returnType: "string", props }) as string;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const Numeral = (props: TypeNumeralProps) => {
|
|
205
|
+
return getNumeral({ returnType: "component", props });
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export default Numeral;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TypeTextProps } from "@sproutsocial/seeds-react-text";
|
|
2
|
+
|
|
3
|
+
export type EnumNumeralFormat = "decimal" | "currency" | "percent";
|
|
4
|
+
|
|
5
|
+
export interface TypeNumeralProps extends Omit<TypeTextProps, "children"> {
|
|
6
|
+
/** The number to be formatted */
|
|
7
|
+
number?: number | null | undefined;
|
|
8
|
+
|
|
9
|
+
/** Locale to format. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locales_argument */
|
|
10
|
+
locale?: string;
|
|
11
|
+
|
|
12
|
+
/* The number formatting style to use */
|
|
13
|
+
format?: EnumNumeralFormat;
|
|
14
|
+
|
|
15
|
+
/** The currency format to use when formatting currency */
|
|
16
|
+
currency?: string;
|
|
17
|
+
|
|
18
|
+
/** A boolean determining whether or not the number should be abbreviated, or a number representing the abbreviation threshold */
|
|
19
|
+
abbreviate?: boolean | number;
|
|
20
|
+
|
|
21
|
+
/** Text to be read off by screen readers for invalid values (i.e., any value rendered as '—' (em dash)) */
|
|
22
|
+
invalidNumberLabel?: string;
|
|
23
|
+
|
|
24
|
+
/** Override the default decimal precision (2 for decimals/currency, 1 for percentages), or "none" allowing unrestricted precision. */
|
|
25
|
+
precision?: number | "none";
|
|
26
|
+
qa?: object;
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Numeral from "../Numeral";
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
+
function NumeralTypes() {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
color: "text.headline",
|
|
8
|
+
number: 12.89,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<Numeral {...defaultProps} />
|
|
14
|
+
<Numeral
|
|
15
|
+
{...defaultProps}
|
|
16
|
+
number={100}
|
|
17
|
+
fontWeight="semibold"
|
|
18
|
+
fontSize={500}
|
|
19
|
+
/>
|
|
20
|
+
<Numeral {...defaultProps} number={100} color="teal.500" />
|
|
21
|
+
<Numeral {...defaultProps} number={100} precision="none" />
|
|
22
|
+
<Numeral {...defaultProps} number={123.4} format="currency" />
|
|
23
|
+
<Numeral
|
|
24
|
+
{...defaultProps}
|
|
25
|
+
number={null}
|
|
26
|
+
invalidNumberLabel="Not available"
|
|
27
|
+
/>
|
|
28
|
+
{/* @ts-expect-error - test that invalid precision is rejected */}
|
|
29
|
+
<Numeral {...defaultProps} precision="invalid" />
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import Numeral from "../../Numeral";
|
|
4
|
+
|
|
5
|
+
describe("When rendering...", () => {
|
|
6
|
+
it("should handle A11y", async () => {
|
|
7
|
+
const { container, runA11yCheck } = render(<Numeral />);
|
|
8
|
+
expect(container).toBeTruthy();
|
|
9
|
+
await runA11yCheck();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import testNumeral from "./testNumeral";
|
|
2
|
+
|
|
3
|
+
describe("When setting abbreviate false...", () => {
|
|
4
|
+
describe("... it never abbreviates the text", () => {
|
|
5
|
+
const options = {
|
|
6
|
+
abbreviate: false,
|
|
7
|
+
};
|
|
8
|
+
// positive values
|
|
9
|
+
testNumeral(1, options, {
|
|
10
|
+
text: "1",
|
|
11
|
+
});
|
|
12
|
+
testNumeral(12, options, {
|
|
13
|
+
text: "12",
|
|
14
|
+
});
|
|
15
|
+
testNumeral(0.23, options, {
|
|
16
|
+
text: "0.23",
|
|
17
|
+
});
|
|
18
|
+
testNumeral(1234, options, {
|
|
19
|
+
text: "1,234",
|
|
20
|
+
});
|
|
21
|
+
testNumeral(12345, options, {
|
|
22
|
+
text: "12,345",
|
|
23
|
+
});
|
|
24
|
+
testNumeral(123456.789, options, {
|
|
25
|
+
text: "123,456.79",
|
|
26
|
+
});
|
|
27
|
+
testNumeral(1225000000000, options, {
|
|
28
|
+
text: "1,225,000,000,000",
|
|
29
|
+
});
|
|
30
|
+
// negative values
|
|
31
|
+
testNumeral(-1, options, {
|
|
32
|
+
text: "-1",
|
|
33
|
+
});
|
|
34
|
+
testNumeral(-12, options, {
|
|
35
|
+
text: "-12",
|
|
36
|
+
});
|
|
37
|
+
testNumeral(-0.23, options, {
|
|
38
|
+
text: "-0.23",
|
|
39
|
+
});
|
|
40
|
+
testNumeral(-1234, options, {
|
|
41
|
+
text: "-1,234",
|
|
42
|
+
});
|
|
43
|
+
testNumeral(-12345, options, {
|
|
44
|
+
text: "-12,345",
|
|
45
|
+
});
|
|
46
|
+
testNumeral(-123456.789, options, {
|
|
47
|
+
text: "-123,456.79",
|
|
48
|
+
});
|
|
49
|
+
testNumeral(-1225000000000, options, {
|
|
50
|
+
text: "-1,225,000,000,000",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("When abbreviate throws an error", () => {
|
|
56
|
+
describe("... it never abbreviates the text", () => {
|
|
57
|
+
const OriginalNumberFormat = Intl.NumberFormat;
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
class CustomNumberFormat {
|
|
60
|
+
constructor(locale: string, standard?: Intl.NumberFormatOptions) {
|
|
61
|
+
if (standard?.notation === "compact") {
|
|
62
|
+
throw new Error("safari bug can not use compact");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new OriginalNumberFormat(locale, standard);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// @ts-expect-error - types mismatch
|
|
69
|
+
Intl["NumberFormat"] = CustomNumberFormat;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
Intl["NumberFormat"] = OriginalNumberFormat;
|
|
74
|
+
});
|
|
75
|
+
const options = {
|
|
76
|
+
abbreviate: true,
|
|
77
|
+
};
|
|
78
|
+
// positive values
|
|
79
|
+
testNumeral(1, options, {
|
|
80
|
+
text: "1",
|
|
81
|
+
});
|
|
82
|
+
testNumeral(12, options, {
|
|
83
|
+
text: "12",
|
|
84
|
+
});
|
|
85
|
+
testNumeral(0.23, options, {
|
|
86
|
+
text: "0.23",
|
|
87
|
+
});
|
|
88
|
+
testNumeral(1234, options, {
|
|
89
|
+
text: "1,234",
|
|
90
|
+
});
|
|
91
|
+
testNumeral(12345, options, {
|
|
92
|
+
text: "12,345",
|
|
93
|
+
});
|
|
94
|
+
testNumeral(123456.789, options, {
|
|
95
|
+
text: "123,456.79",
|
|
96
|
+
});
|
|
97
|
+
testNumeral(1225000000000, options, {
|
|
98
|
+
text: "1,225,000,000,000",
|
|
99
|
+
});
|
|
100
|
+
// negative values
|
|
101
|
+
testNumeral(-1, options, {
|
|
102
|
+
text: "-1",
|
|
103
|
+
});
|
|
104
|
+
testNumeral(-12, options, {
|
|
105
|
+
text: "-12",
|
|
106
|
+
});
|
|
107
|
+
testNumeral(-0.23, options, {
|
|
108
|
+
text: "-0.23",
|
|
109
|
+
});
|
|
110
|
+
testNumeral(-1234, options, {
|
|
111
|
+
text: "-1,234",
|
|
112
|
+
});
|
|
113
|
+
testNumeral(-12345, options, {
|
|
114
|
+
text: "-12,345",
|
|
115
|
+
});
|
|
116
|
+
testNumeral(-123456.789, options, {
|
|
117
|
+
text: "-123,456.79",
|
|
118
|
+
});
|
|
119
|
+
testNumeral(-1225000000000, options, {
|
|
120
|
+
text: "-1,225,000,000,000",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("When setting abbreviate to a threshold...", () => {
|
|
126
|
+
describe("... it doesn't abbreviate if under the threshold", () => {
|
|
127
|
+
// positive values
|
|
128
|
+
const positiveOptions = {
|
|
129
|
+
abbreviate: 2000,
|
|
130
|
+
};
|
|
131
|
+
testNumeral(1999, positiveOptions, {
|
|
132
|
+
text: "1,999",
|
|
133
|
+
});
|
|
134
|
+
testNumeral(2000, positiveOptions, {
|
|
135
|
+
text: "2.00K",
|
|
136
|
+
tip: "2,000",
|
|
137
|
+
});
|
|
138
|
+
testNumeral(2011, positiveOptions, {
|
|
139
|
+
text: "2.01K",
|
|
140
|
+
tip: "2,011",
|
|
141
|
+
});
|
|
142
|
+
// negative values
|
|
143
|
+
const negativeOptions = {
|
|
144
|
+
abbreviate: -2000,
|
|
145
|
+
};
|
|
146
|
+
testNumeral(-1999, negativeOptions, {
|
|
147
|
+
text: "-1,999",
|
|
148
|
+
});
|
|
149
|
+
testNumeral(-2000, negativeOptions, {
|
|
150
|
+
text: "-2.00K",
|
|
151
|
+
tip: "-2,000",
|
|
152
|
+
});
|
|
153
|
+
testNumeral(-2011, negativeOptions, {
|
|
154
|
+
text: "-2.01K",
|
|
155
|
+
tip: "-2,011",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("... or if too small to abbreviate", () => {
|
|
160
|
+
// positive values
|
|
161
|
+
const positiveOptions = {
|
|
162
|
+
abbreviate: 500,
|
|
163
|
+
};
|
|
164
|
+
testNumeral(999, positiveOptions, {
|
|
165
|
+
text: "999",
|
|
166
|
+
});
|
|
167
|
+
testNumeral(1000, positiveOptions, {
|
|
168
|
+
text: "1.00K",
|
|
169
|
+
tip: "1,000",
|
|
170
|
+
});
|
|
171
|
+
// negative values
|
|
172
|
+
const negativeOptions = {
|
|
173
|
+
abbreviate: -500,
|
|
174
|
+
};
|
|
175
|
+
testNumeral(-999, negativeOptions, {
|
|
176
|
+
text: "-999",
|
|
177
|
+
});
|
|
178
|
+
testNumeral(-1000, negativeOptions, {
|
|
179
|
+
text: "-1.00K",
|
|
180
|
+
tip: "-1,000",
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("... it abbreviates if greater or equal to the threshold", () => {
|
|
185
|
+
const options = {
|
|
186
|
+
abbreviate: true,
|
|
187
|
+
};
|
|
188
|
+
// positive values
|
|
189
|
+
testNumeral(12345, options, {
|
|
190
|
+
text: "12.35K",
|
|
191
|
+
tip: "12,345",
|
|
192
|
+
});
|
|
193
|
+
testNumeral(123456.789, options, {
|
|
194
|
+
text: "123.46K",
|
|
195
|
+
tip: "123,456.79",
|
|
196
|
+
});
|
|
197
|
+
testNumeral(1225000000000, options, {
|
|
198
|
+
text: "1.23T",
|
|
199
|
+
tip: "1,225,000,000,000",
|
|
200
|
+
});
|
|
201
|
+
// negative values
|
|
202
|
+
testNumeral(-12345, options, {
|
|
203
|
+
text: "-12.35K",
|
|
204
|
+
tip: "-12,345",
|
|
205
|
+
});
|
|
206
|
+
testNumeral(-123456.789, options, {
|
|
207
|
+
text: "-123.46K",
|
|
208
|
+
tip: "-123,456.79",
|
|
209
|
+
});
|
|
210
|
+
testNumeral(-1225000000000, options, {
|
|
211
|
+
text: "-1.23T",
|
|
212
|
+
tip: "-1,225,000,000,000",
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|