@jobber/components-native 0.33.1 → 0.35.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/dist/src/AtlantisContext/AtlantisContext.js +2 -0
- package/dist/src/InputCurrency/InputCurrency.js +93 -0
- package/dist/src/InputCurrency/constants.js +1 -0
- package/dist/src/InputCurrency/index.js +3 -0
- package/dist/src/InputCurrency/messages.js +8 -0
- package/dist/src/InputCurrency/utils.js +58 -0
- package/dist/src/InputTime/InputTime.js +83 -0
- package/dist/src/InputTime/InputTime.style.js +7 -0
- package/dist/src/InputTime/index.js +2 -0
- package/dist/src/InputTime/messages.js +8 -0
- package/dist/src/InputTime/utils/index.js +16 -0
- package/dist/src/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/AtlantisContext/AtlantisContext.d.ts +4 -0
- package/dist/types/src/InputCurrency/InputCurrency.d.ts +32 -0
- package/dist/types/src/InputCurrency/constants.d.ts +1 -0
- package/dist/types/src/InputCurrency/index.d.ts +3 -0
- package/dist/types/src/InputCurrency/messages.d.ts +7 -0
- package/dist/types/src/InputCurrency/utils.d.ts +9 -0
- package/dist/types/src/InputTime/InputTime.d.ts +61 -0
- package/dist/types/src/InputTime/InputTime.style.d.ts +6 -0
- package/dist/types/src/InputTime/index.d.ts +2 -0
- package/dist/types/src/InputTime/messages.d.ts +7 -0
- package/dist/types/src/InputTime/utils/index.d.ts +11 -0
- package/dist/types/src/index.d.ts +2 -0
- package/package.json +7 -3
- package/src/AtlantisContext/AtlantisContext.test.tsx +1 -0
- package/src/AtlantisContext/AtlantisContext.tsx +7 -0
- package/src/InputCurrency/InputCurrency.test.tsx +158 -0
- package/src/InputCurrency/InputCurrency.tsx +206 -0
- package/src/InputCurrency/constants.ts +1 -0
- package/src/InputCurrency/index.ts +3 -0
- package/src/InputCurrency/messages.ts +10 -0
- package/src/InputCurrency/utils.ts +92 -0
- package/src/InputTime/InputTime.style.ts +8 -0
- package/src/InputTime/InputTime.test.tsx +323 -0
- package/src/InputTime/InputTime.tsx +221 -0
- package/src/InputTime/index.tsx +2 -0
- package/src/InputTime/messages.ts +9 -0
- package/src/InputTime/utils/index.ts +26 -0
- package/src/InputTime/utils/utils.test.ts +47 -0
- package/src/index.ts +2 -0
|
@@ -26,6 +26,10 @@ export interface AtlantisContextProps {
|
|
|
26
26
|
* Grabs the decimal separator and group separator based on locale
|
|
27
27
|
*/
|
|
28
28
|
readonly floatSeparators: Record<"decimal" | "group", string>;
|
|
29
|
+
/**
|
|
30
|
+
* The currency symbol Atlantis components will use
|
|
31
|
+
*/
|
|
32
|
+
readonly currencySymbol: string;
|
|
29
33
|
}
|
|
30
34
|
export declare const defaultValues: AtlantisContextProps;
|
|
31
35
|
export declare const AtlantisContext: import("react").Context<AtlantisContextProps>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { FormatNumberOptions } from "react-intl";
|
|
3
|
+
import { ControllerRenderProps, FieldValues } from "react-hook-form";
|
|
4
|
+
import { InputTextProps } from "../InputText";
|
|
5
|
+
export interface InputCurrencyProps extends Omit<InputTextProps, "keyboard" | "onChangeText" | "value" | "defaultValue"> {
|
|
6
|
+
/**
|
|
7
|
+
* Whether to display the user's currency symbol or not
|
|
8
|
+
* Default value is true
|
|
9
|
+
*/
|
|
10
|
+
readonly showCurrencySymbol?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The minimum decimal places for the currency number
|
|
13
|
+
* Default value is 2
|
|
14
|
+
*/
|
|
15
|
+
readonly decimalPlaces?: number;
|
|
16
|
+
/**
|
|
17
|
+
* The maximum decimal places for the currency number
|
|
18
|
+
* Default value is 5
|
|
19
|
+
*/
|
|
20
|
+
readonly maxDecimalPlaces?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The maximum length of the input
|
|
23
|
+
* Default value is 10
|
|
24
|
+
*/
|
|
25
|
+
readonly maxLength?: number;
|
|
26
|
+
onChange?(newValue?: number | string | undefined): void;
|
|
27
|
+
value?: number;
|
|
28
|
+
defaultValue?: number;
|
|
29
|
+
keyboard?: "decimal-pad" | "numbers-and-punctuation";
|
|
30
|
+
}
|
|
31
|
+
export declare const getInternalValue: (props: InputCurrencyProps, field: ControllerRenderProps<FieldValues, string>, formatNumber: (value: number | bigint, opts?: FormatNumberOptions | undefined) => string) => string;
|
|
32
|
+
export declare function InputCurrency(props: InputCurrencyProps): JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_CURRENCY_SYMBOL = "$";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function countDecimal(value: number): number;
|
|
2
|
+
export declare function limitInputWholeDigits(value: number, maxInputLength: number): number;
|
|
3
|
+
export declare function configureDecimal(decimalCount: number, maxDecimalPlaces: number, transformedValue: string, decimalPlaces: number): number;
|
|
4
|
+
export declare function convertToNumber(value: string): string | number;
|
|
5
|
+
export declare const checkLastChar: (stringValue: string) => boolean;
|
|
6
|
+
export declare const isValidNumber: (numberedValue: string | number | undefined) => boolean;
|
|
7
|
+
export declare const getDecimalNumbers: (value: string, decimalSeparator: string) => string;
|
|
8
|
+
export declare const parseGivenInput: (value: string | undefined, decimalSeparator: string) => [number, string | undefined, string];
|
|
9
|
+
export declare const NUMBER_VALIDATION_REGEX: RegExp;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { UseControllerProps } from "react-hook-form";
|
|
3
|
+
import { XOR } from "ts-xor";
|
|
4
|
+
import { Clearable, InputFieldWrapperProps } from "../InputFieldWrapper";
|
|
5
|
+
interface InputTimeBaseProps extends Pick<InputFieldWrapperProps, "invalid" | "disabled" | "placeholder"> {
|
|
6
|
+
/**
|
|
7
|
+
* Defaulted to "always" so user can clear the time whenever there's a value.
|
|
8
|
+
*/
|
|
9
|
+
readonly clearable?: Extract<Clearable, "always" | "never">;
|
|
10
|
+
/**
|
|
11
|
+
* Add a custom value to display when no time is selected
|
|
12
|
+
* @default undefined
|
|
13
|
+
*/
|
|
14
|
+
readonly emptyValueLabel?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Adjusts the UX of the time picker based on where you'd use it.
|
|
17
|
+
*
|
|
18
|
+
* - `"granular"` - allows the user to pick a very specific time
|
|
19
|
+
* - `"scheduling"` - only allows user to select between 5 minutes interval.
|
|
20
|
+
* If your design is catered towards "scheduling", you should use this type.
|
|
21
|
+
*
|
|
22
|
+
* @default scheduling
|
|
23
|
+
*/
|
|
24
|
+
readonly type?: "granular" | "scheduling";
|
|
25
|
+
/**
|
|
26
|
+
* Hide or show the timer icon.
|
|
27
|
+
*/
|
|
28
|
+
readonly showIcon?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface InputTimeFormControlled extends InputTimeBaseProps {
|
|
31
|
+
/**
|
|
32
|
+
* Adding a `name` would make this component "Form controlled" and must be
|
|
33
|
+
* nested within a `<Form />` component.
|
|
34
|
+
*
|
|
35
|
+
* Cannot be declared if `value` prop is used.
|
|
36
|
+
*/
|
|
37
|
+
readonly name: string;
|
|
38
|
+
/**
|
|
39
|
+
* Shows an error message below the field and highlights it red when the
|
|
40
|
+
* value is invalid. Only applies when nested within a `<Form />` component.
|
|
41
|
+
*/
|
|
42
|
+
readonly validations?: UseControllerProps["rules"];
|
|
43
|
+
/**
|
|
44
|
+
* The callback that fires whenever a time gets selected.
|
|
45
|
+
*/
|
|
46
|
+
readonly onChange?: (value?: Date | null) => void;
|
|
47
|
+
}
|
|
48
|
+
interface InputTimeDevControlled extends InputTimeBaseProps {
|
|
49
|
+
/**
|
|
50
|
+
* The value shown on the field. This gets automatically formatted to the
|
|
51
|
+
* account's time format.
|
|
52
|
+
*/
|
|
53
|
+
readonly value: Date | string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* The callback that fires whenever a time gets selected.
|
|
56
|
+
*/
|
|
57
|
+
readonly onChange: (value?: Date) => void;
|
|
58
|
+
}
|
|
59
|
+
export type InputTimeProps = XOR<InputTimeFormControlled, InputTimeDevControlled>;
|
|
60
|
+
export declare function InputTime(props: InputTimeProps): JSX.Element;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type MinutesIncrement = 15 | 30 | 60;
|
|
2
|
+
/**
|
|
3
|
+
* Rounds up the time by increment.
|
|
4
|
+
* - 15 mins - rounds to the next quarter time of `00:15`, `00:30`, `00:45`,
|
|
5
|
+
* and `01:00`
|
|
6
|
+
* - 30 mins - rounds to the next half hour be it `00:30` or `01:00`
|
|
7
|
+
* - 60 mins - rounds to the next hour. I.e., `02:01` gets rounded up
|
|
8
|
+
* to `03:00`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function roundUpToNearestMinutes(date: Date, minutes: MinutesIncrement): Date;
|
|
11
|
+
export declare function getTimeZoneOffsetInMinutes(timeZone: string, date?: Date): number;
|
|
@@ -18,10 +18,12 @@ export * from "./Heading";
|
|
|
18
18
|
export * from "./Icon";
|
|
19
19
|
export * from "./IconButton";
|
|
20
20
|
export * from "./InputFieldWrapper";
|
|
21
|
+
export * from "./InputCurrency";
|
|
21
22
|
export * from "./InputNumber";
|
|
22
23
|
export * from "./InputPassword";
|
|
23
24
|
export * from "./InputPressable";
|
|
24
25
|
export * from "./InputSearch";
|
|
26
|
+
export * from "./InputTime";
|
|
25
27
|
export * from "./InputText";
|
|
26
28
|
export * from "./TextList";
|
|
27
29
|
export * from "./ProgressBar";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -67,8 +67,12 @@
|
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@babel/core": "^7.4.5",
|
|
70
|
+
"@react-native-community/datetimepicker": ">=6.7.0",
|
|
71
|
+
"date-fns": "^2.0.0",
|
|
72
|
+
"date-fns-tz": "*",
|
|
70
73
|
"react": "^18",
|
|
71
|
-
"react-native": ">=0.69.2"
|
|
74
|
+
"react-native": ">=0.69.2",
|
|
75
|
+
"react-native-modal-datetime-picker": " >=13.0.0"
|
|
72
76
|
},
|
|
73
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "5028c2bd4c97479b567b031432a331e3884afcaa"
|
|
74
78
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
3
|
import RNLocalize from "react-native-localize";
|
|
4
|
+
import { DEFAULT_CURRENCY_SYMBOL } from "../InputCurrency/constants";
|
|
4
5
|
|
|
5
6
|
export interface AtlantisContextProps {
|
|
6
7
|
/**
|
|
@@ -34,6 +35,11 @@ export interface AtlantisContextProps {
|
|
|
34
35
|
* Grabs the decimal separator and group separator based on locale
|
|
35
36
|
*/
|
|
36
37
|
readonly floatSeparators: Record<"decimal" | "group", string>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The currency symbol Atlantis components will use
|
|
41
|
+
*/
|
|
42
|
+
readonly currencySymbol: string;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export const defaultValues: AtlantisContextProps = {
|
|
@@ -45,6 +51,7 @@ export const defaultValues: AtlantisContextProps = {
|
|
|
45
51
|
return;
|
|
46
52
|
},
|
|
47
53
|
floatSeparators: { group: ",", decimal: "." },
|
|
54
|
+
currencySymbol: DEFAULT_CURRENCY_SYMBOL,
|
|
48
55
|
};
|
|
49
56
|
|
|
50
57
|
export const AtlantisContext = createContext(defaultValues);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
|
3
|
+
import RNLocalize from "react-native-localize";
|
|
4
|
+
import { InputCurrency } from "./InputCurrency";
|
|
5
|
+
import { AtlantisContext, AtlantisContextProps } from "../AtlantisContext";
|
|
6
|
+
|
|
7
|
+
const mockCurrencySymbol = "£";
|
|
8
|
+
const atlantisContext: AtlantisContextProps = {
|
|
9
|
+
currencySymbol: mockCurrencySymbol,
|
|
10
|
+
timeFormat: "p",
|
|
11
|
+
timeZone: RNLocalize.getTimeZone(),
|
|
12
|
+
isOnline: true,
|
|
13
|
+
onLogError: err => {
|
|
14
|
+
return err;
|
|
15
|
+
},
|
|
16
|
+
floatSeparators: { group: ",", decimal: "." },
|
|
17
|
+
};
|
|
18
|
+
const placeHolder = "Price";
|
|
19
|
+
describe.each([{ includeATLContext: true }, { includeATLContext: false }])(
|
|
20
|
+
"Has AtlantisContext: $includeATLContext",
|
|
21
|
+
({ includeATLContext }) => {
|
|
22
|
+
function setup({
|
|
23
|
+
showCurrencySymbol,
|
|
24
|
+
name,
|
|
25
|
+
}: {
|
|
26
|
+
showCurrencySymbol?: boolean;
|
|
27
|
+
name: string;
|
|
28
|
+
}) {
|
|
29
|
+
if (includeATLContext) {
|
|
30
|
+
return render(
|
|
31
|
+
<AtlantisContext.Provider value={atlantisContext}>
|
|
32
|
+
<InputCurrency
|
|
33
|
+
placeholder={placeHolder}
|
|
34
|
+
name={name}
|
|
35
|
+
showCurrencySymbol={showCurrencySymbol}
|
|
36
|
+
/>
|
|
37
|
+
</AtlantisContext.Provider>,
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
return render(
|
|
41
|
+
<InputCurrency
|
|
42
|
+
placeholder={placeHolder}
|
|
43
|
+
name={name}
|
|
44
|
+
showCurrencySymbol={showCurrencySymbol}
|
|
45
|
+
/>,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
it("renders with currency symbol when the value is input", () => {
|
|
51
|
+
const { getByText, getByLabelText } = setup({
|
|
52
|
+
name: "sample",
|
|
53
|
+
});
|
|
54
|
+
const value = 123.459119;
|
|
55
|
+
const expectedCurrencySymbol = includeATLContext
|
|
56
|
+
? mockCurrencySymbol
|
|
57
|
+
: "$";
|
|
58
|
+
fireEvent.changeText(getByLabelText(placeHolder), `${value}`);
|
|
59
|
+
|
|
60
|
+
expect(getByText(expectedCurrencySymbol)).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders without currency symbol", () => {
|
|
64
|
+
const { queryByText, getByLabelText } = setup({
|
|
65
|
+
name: "sample",
|
|
66
|
+
showCurrencySymbol: false,
|
|
67
|
+
});
|
|
68
|
+
const value = 123.459119;
|
|
69
|
+
|
|
70
|
+
fireEvent.changeText(getByLabelText(placeHolder), `${value}`);
|
|
71
|
+
expect(queryByText(mockCurrencySymbol)).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("displays a maximum 5 decimal places", async () => {
|
|
75
|
+
const value = 123.459119;
|
|
76
|
+
const { getByLabelText, getByDisplayValue } = setup({
|
|
77
|
+
name: "sample",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
fireEvent.changeText(getByLabelText(placeHolder), `${value}`);
|
|
81
|
+
fireEvent(getByLabelText("Price"), "blur");
|
|
82
|
+
fireEvent(getByLabelText("Price"), "blur");
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(getByDisplayValue("123.45912")).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("displays a minimum of 2 decimal places", async () => {
|
|
90
|
+
const value = 123.11;
|
|
91
|
+
const { getByLabelText, getByDisplayValue } = setup({ name: "sample" });
|
|
92
|
+
|
|
93
|
+
fireEvent.changeText(getByLabelText(placeHolder), `${value}`);
|
|
94
|
+
fireEvent(getByLabelText("Price"), "blur");
|
|
95
|
+
|
|
96
|
+
await waitFor(() => {
|
|
97
|
+
expect(getByDisplayValue("123.11")).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("displays a negative value", async () => {
|
|
102
|
+
const value = -509.543;
|
|
103
|
+
const { getByLabelText, getByDisplayValue } = setup({ name: "sample" });
|
|
104
|
+
|
|
105
|
+
fireEvent.changeText(getByLabelText(placeHolder), `${value}`);
|
|
106
|
+
fireEvent(getByLabelText("Price"), "blur");
|
|
107
|
+
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(getByDisplayValue(`${value}`)).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("internationalizes the display Value in the input", async () => {
|
|
114
|
+
const value = 5098.543;
|
|
115
|
+
const { getByLabelText, getByDisplayValue } = setup({ name: "sample" });
|
|
116
|
+
|
|
117
|
+
fireEvent.changeText(getByLabelText(placeHolder), `${value}`);
|
|
118
|
+
fireEvent(getByLabelText("Price"), "blur");
|
|
119
|
+
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(getByDisplayValue("5,098.543")).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("limits the whole number integer to 10 whole integers by default", async () => {
|
|
126
|
+
const value = 12345678998;
|
|
127
|
+
const { getByLabelText, getByDisplayValue } = setup({ name: "sample" });
|
|
128
|
+
|
|
129
|
+
fireEvent.changeText(getByLabelText(placeHolder), `${value}`);
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(getByDisplayValue("1,234,567,899")).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("rounds the decimal point if there are more decimal numbers than the maxDecimalCount", async () => {
|
|
137
|
+
const value = 123456.789988;
|
|
138
|
+
const { getByLabelText, getByDisplayValue } = setup({ name: "sample" });
|
|
139
|
+
|
|
140
|
+
fireEvent.changeText(getByLabelText(placeHolder), `${value}`);
|
|
141
|
+
fireEvent(getByLabelText("Price"), "blur");
|
|
142
|
+
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(getByDisplayValue("123,456.78999")).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("displays 0 on blur if there is no inputted value or the field.value is undefined", async () => {
|
|
149
|
+
const { getByLabelText, getByDisplayValue } = setup({ name: "sample" });
|
|
150
|
+
|
|
151
|
+
fireEvent(getByLabelText("Price"), "blur");
|
|
152
|
+
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(getByDisplayValue("0")).toBeDefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { FormatNumberOptions, useIntl } from "react-intl";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { ControllerRenderProps, FieldValues } from "react-hook-form";
|
|
5
|
+
import {
|
|
6
|
+
NUMBER_VALIDATION_REGEX,
|
|
7
|
+
checkLastChar,
|
|
8
|
+
configureDecimal,
|
|
9
|
+
convertToNumber,
|
|
10
|
+
isValidNumber,
|
|
11
|
+
limitInputWholeDigits,
|
|
12
|
+
parseGivenInput,
|
|
13
|
+
} from "./utils";
|
|
14
|
+
import { messages } from "./messages";
|
|
15
|
+
import { useAtlantisContext } from "../AtlantisContext";
|
|
16
|
+
import { InputText, InputTextProps } from "../InputText";
|
|
17
|
+
import { useFormController } from "../hooks/useFormController";
|
|
18
|
+
|
|
19
|
+
export interface InputCurrencyProps
|
|
20
|
+
extends Omit<
|
|
21
|
+
InputTextProps,
|
|
22
|
+
"keyboard" | "onChangeText" | "value" | "defaultValue"
|
|
23
|
+
> {
|
|
24
|
+
/**
|
|
25
|
+
* Whether to display the user's currency symbol or not
|
|
26
|
+
* Default value is true
|
|
27
|
+
*/
|
|
28
|
+
readonly showCurrencySymbol?: boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The minimum decimal places for the currency number
|
|
32
|
+
* Default value is 2
|
|
33
|
+
*/
|
|
34
|
+
readonly decimalPlaces?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The maximum decimal places for the currency number
|
|
38
|
+
* Default value is 5
|
|
39
|
+
*/
|
|
40
|
+
readonly maxDecimalPlaces?: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The maximum length of the input
|
|
44
|
+
* Default value is 10
|
|
45
|
+
*/
|
|
46
|
+
readonly maxLength?: number;
|
|
47
|
+
|
|
48
|
+
onChange?(newValue?: number | string | undefined): void;
|
|
49
|
+
value?: number;
|
|
50
|
+
defaultValue?: number;
|
|
51
|
+
keyboard?: "decimal-pad" | "numbers-and-punctuation";
|
|
52
|
+
}
|
|
53
|
+
export const getInternalValue = (
|
|
54
|
+
props: InputCurrencyProps,
|
|
55
|
+
field: ControllerRenderProps<FieldValues, string>,
|
|
56
|
+
formatNumber: (
|
|
57
|
+
value: number | bigint,
|
|
58
|
+
opts?: FormatNumberOptions | undefined,
|
|
59
|
+
) => string,
|
|
60
|
+
): string => {
|
|
61
|
+
if (!props.value && !field.value) return "";
|
|
62
|
+
return (
|
|
63
|
+
props.value?.toString() ??
|
|
64
|
+
formatNumber(field.value, {
|
|
65
|
+
maximumFractionDigits: props.maxDecimalPlaces,
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getKeyboard = (props: InputCurrencyProps) => {
|
|
71
|
+
if (Platform.OS === "ios") {
|
|
72
|
+
//since we are checking for which keyboard to use here, just implement default keyboard here instead of in params
|
|
73
|
+
return props.keyboard ?? "numbers-and-punctuation";
|
|
74
|
+
} else {
|
|
75
|
+
return "numeric";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function InputCurrency(props: InputCurrencyProps): JSX.Element {
|
|
80
|
+
const {
|
|
81
|
+
showCurrencySymbol = true,
|
|
82
|
+
maxDecimalPlaces = 5,
|
|
83
|
+
decimalPlaces = 2,
|
|
84
|
+
maxLength = 10,
|
|
85
|
+
} = props;
|
|
86
|
+
const intl = useIntl();
|
|
87
|
+
const { floatSeparators, currencySymbol } = useAtlantisContext();
|
|
88
|
+
|
|
89
|
+
const { field } = useFormController({
|
|
90
|
+
name: props.name,
|
|
91
|
+
value: props.value,
|
|
92
|
+
});
|
|
93
|
+
const internalValue = getInternalValue(props, field, intl.formatNumber);
|
|
94
|
+
const [displayValue, setDisplayValue] = useState<string | undefined>(
|
|
95
|
+
internalValue,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const setOnChangeAndDisplayValues = (
|
|
99
|
+
onChangeValue: number | string | undefined,
|
|
100
|
+
valueToDisplay: string | undefined,
|
|
101
|
+
) => {
|
|
102
|
+
props.onChange?.(onChangeValue);
|
|
103
|
+
setDisplayValue(valueToDisplay);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const checkDecimalAndI18nOfDisplayValue = (
|
|
107
|
+
numberedValue: number,
|
|
108
|
+
decimalNumbers: string,
|
|
109
|
+
decimalCount: number,
|
|
110
|
+
) => {
|
|
111
|
+
const transformedValue = limitInputWholeDigits(numberedValue, maxLength);
|
|
112
|
+
const stringValue =
|
|
113
|
+
decimalNumbers !== ""
|
|
114
|
+
? transformedValue.toString() + "." + decimalNumbers.slice(1)
|
|
115
|
+
: transformedValue.toString();
|
|
116
|
+
if (checkLastChar(stringValue)) {
|
|
117
|
+
const roundedDecimal = configureDecimal(
|
|
118
|
+
decimalCount,
|
|
119
|
+
maxDecimalPlaces,
|
|
120
|
+
stringValue,
|
|
121
|
+
decimalPlaces,
|
|
122
|
+
);
|
|
123
|
+
const internationalizedValueToDisplay = intl.formatNumber(
|
|
124
|
+
roundedDecimal,
|
|
125
|
+
{
|
|
126
|
+
maximumFractionDigits: maxDecimalPlaces,
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
setOnChangeAndDisplayValues(
|
|
130
|
+
roundedDecimal,
|
|
131
|
+
internationalizedValueToDisplay,
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
const internationalizedValueToDisplay =
|
|
135
|
+
intl.formatNumber(transformedValue, {
|
|
136
|
+
maximumFractionDigits: maxDecimalPlaces,
|
|
137
|
+
}) + decimalNumbers;
|
|
138
|
+
setOnChangeAndDisplayValues(
|
|
139
|
+
transformedValue.toString() + "." + decimalNumbers.slice(1),
|
|
140
|
+
internationalizedValueToDisplay,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleChange = (newValue: string | undefined) => {
|
|
146
|
+
const [decimalCount, wholeIntegerValue, decimalNumbers] = parseGivenInput(
|
|
147
|
+
newValue,
|
|
148
|
+
floatSeparators.decimal,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const numberedValue = wholeIntegerValue
|
|
152
|
+
? convertToNumber(wholeIntegerValue)
|
|
153
|
+
: wholeIntegerValue;
|
|
154
|
+
|
|
155
|
+
if (isValidNumber(numberedValue) && typeof numberedValue === "number") {
|
|
156
|
+
checkDecimalAndI18nOfDisplayValue(
|
|
157
|
+
numberedValue,
|
|
158
|
+
decimalNumbers,
|
|
159
|
+
decimalCount,
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
const value = numberedValue?.toString() + decimalNumbers;
|
|
163
|
+
setOnChangeAndDisplayValues(value, value);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const { formatMessage } = useIntl();
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<>
|
|
171
|
+
<InputText
|
|
172
|
+
{...props}
|
|
173
|
+
prefix={showCurrencySymbol ? { label: currencySymbol } : undefined}
|
|
174
|
+
keyboard={getKeyboard(props)}
|
|
175
|
+
value={props.value?.toString() || displayValue}
|
|
176
|
+
defaultValue={props.defaultValue?.toString()}
|
|
177
|
+
onChangeText={handleChange}
|
|
178
|
+
transform={{
|
|
179
|
+
output: val => {
|
|
180
|
+
return val
|
|
181
|
+
?.split(floatSeparators.group)
|
|
182
|
+
.join("")
|
|
183
|
+
.replace(floatSeparators.decimal, ".");
|
|
184
|
+
},
|
|
185
|
+
}}
|
|
186
|
+
validations={{
|
|
187
|
+
pattern: {
|
|
188
|
+
value: NUMBER_VALIDATION_REGEX,
|
|
189
|
+
message: formatMessage(messages.notANumberError),
|
|
190
|
+
},
|
|
191
|
+
...props.validations,
|
|
192
|
+
}}
|
|
193
|
+
onBlur={() => {
|
|
194
|
+
props.onBlur?.();
|
|
195
|
+
if (
|
|
196
|
+
field.value === 0 ||
|
|
197
|
+
field.value === "" ||
|
|
198
|
+
field.value === undefined
|
|
199
|
+
) {
|
|
200
|
+
setDisplayValue("0");
|
|
201
|
+
}
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
</>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_CURRENCY_SYMBOL = "$";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineMessages } from "react-intl";
|
|
2
|
+
|
|
3
|
+
export const messages = defineMessages({
|
|
4
|
+
notANumberError: {
|
|
5
|
+
id: "notANumberError",
|
|
6
|
+
defaultMessage: "Enter a number",
|
|
7
|
+
description:
|
|
8
|
+
"Error message shown when a non-numeric value is typed in number input",
|
|
9
|
+
},
|
|
10
|
+
});
|