@jobber/components-native 0.33.0 → 0.34.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/Flex/Flex.js +1 -1
- 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/InputSearch/InputSearch.js +1 -1
- package/dist/src/InputText/InputText.js +1 -1
- package/dist/src/index.js +1 -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/index.d.ts +1 -0
- package/package.json +2 -5
- package/src/AtlantisContext/AtlantisContext.test.tsx +1 -0
- package/src/AtlantisContext/AtlantisContext.tsx +7 -0
- package/src/Flex/Flex.tsx +1 -1
- 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/InputSearch/InputSearch.tsx +1 -1
- package/src/InputText/InputText.tsx +1 -1
- package/src/index.ts +1 -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;
|
|
@@ -18,6 +18,7 @@ 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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -38,9 +38,6 @@
|
|
|
38
38
|
"@jobber/design": "^0.42.0",
|
|
39
39
|
"@react-native-picker/picker": "^2.4.10",
|
|
40
40
|
"lodash": "^4.17.21",
|
|
41
|
-
"lodash.chunk": "^4.2.0",
|
|
42
|
-
"lodash.debounce": "^4.0.8",
|
|
43
|
-
"lodash.identity": "^3.0.0",
|
|
44
41
|
"react-hook-form": "^7.30.0",
|
|
45
42
|
"react-intl": "^6.4.2",
|
|
46
43
|
"react-native-gesture-handler": "^2.10.2",
|
|
@@ -73,5 +70,5 @@
|
|
|
73
70
|
"react": "^18",
|
|
74
71
|
"react-native": ">=0.69.2"
|
|
75
72
|
},
|
|
76
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "74d12fc40b92e5b3575256deaba1840a96387764"
|
|
77
74
|
}
|
|
@@ -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);
|
package/src/Flex/Flex.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { Children, PropsWithChildren } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
|
-
import chunk from "lodash
|
|
3
|
+
import chunk from "lodash/chunk";
|
|
4
4
|
import { columnStyles, gapStyles, styles } from "./Flex.styles";
|
|
5
5
|
import { FlexProps } from "./types";
|
|
6
6
|
import { Content } from "../Content";
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export function countDecimal(value: number): number {
|
|
2
|
+
const convertedValue = value.toString();
|
|
3
|
+
if (convertedValue.includes(".")) {
|
|
4
|
+
return convertedValue.split(".")[1].length;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function limitInputWholeDigits(
|
|
11
|
+
value: number,
|
|
12
|
+
maxInputLength: number,
|
|
13
|
+
): number {
|
|
14
|
+
let convertedValue = value.toString();
|
|
15
|
+
if (convertedValue.length > maxInputLength) {
|
|
16
|
+
convertedValue = convertedValue.slice(0, maxInputLength);
|
|
17
|
+
return parseFloat(convertedValue);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function configureDecimal(
|
|
23
|
+
decimalCount: number,
|
|
24
|
+
maxDecimalPlaces: number,
|
|
25
|
+
transformedValue: string,
|
|
26
|
+
decimalPlaces: number,
|
|
27
|
+
): number {
|
|
28
|
+
const targetDecimalPlaces = Math.min(
|
|
29
|
+
Math.max(decimalCount, decimalPlaces),
|
|
30
|
+
maxDecimalPlaces,
|
|
31
|
+
);
|
|
32
|
+
const precision = 10 ** targetDecimalPlaces;
|
|
33
|
+
const convertedValue =
|
|
34
|
+
Math.round(parseFloat(transformedValue) * precision) / precision;
|
|
35
|
+
return convertedValue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function convertToNumber(value: string): string | number {
|
|
39
|
+
const regexValidation = /^[0-9]*$/;
|
|
40
|
+
if (value?.match?.(regexValidation)) {
|
|
41
|
+
return parseFloat(value);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const checkLastChar = (stringValue: string): boolean => {
|
|
47
|
+
const lastChar = stringValue[stringValue.length - 1];
|
|
48
|
+
return Boolean(Number(stringValue)) && lastChar !== "0" && lastChar !== ".";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const isValidNumber = (
|
|
52
|
+
numberedValue: string | number | undefined,
|
|
53
|
+
): boolean => {
|
|
54
|
+
return (
|
|
55
|
+
typeof numberedValue === "number" &&
|
|
56
|
+
!isNaN(numberedValue) &&
|
|
57
|
+
numberedValue !== 0
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const getDecimalNumbers = (
|
|
62
|
+
value: string,
|
|
63
|
+
decimalSeparator: string,
|
|
64
|
+
): string => {
|
|
65
|
+
const decimalValue = value.split(".")[1];
|
|
66
|
+
|
|
67
|
+
if (!decimalValue) {
|
|
68
|
+
return decimalSeparator;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return `${decimalSeparator}${decimalValue}`;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const parseGivenInput = (
|
|
75
|
+
value: string | undefined,
|
|
76
|
+
decimalSeparator: string,
|
|
77
|
+
): [number, string | undefined, string] => {
|
|
78
|
+
let decimalCount = 0;
|
|
79
|
+
let decimalNumbers = "";
|
|
80
|
+
let wholeIntegerValue = value;
|
|
81
|
+
|
|
82
|
+
if (value?.includes(".")) {
|
|
83
|
+
const splittedValue = value?.split(".");
|
|
84
|
+
decimalCount = splittedValue[1].length;
|
|
85
|
+
wholeIntegerValue = splittedValue[0];
|
|
86
|
+
decimalNumbers = getDecimalNumbers(value, decimalSeparator);
|
|
87
|
+
}
|
|
88
|
+
return [decimalCount, wholeIntegerValue, decimalNumbers];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const NUMBER_VALIDATION_REGEX =
|
|
92
|
+
/^[-+]?(([0-9]*\.[0-9]+)|([0-9]+)|([0-9]+(\.?[0-9]+)?e[-+]?[0-9]+))$/;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { Ref, forwardRef, useEffect } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
|
-
import debounce from "lodash
|
|
3
|
+
import debounce from "lodash/debounce";
|
|
4
4
|
import { styles } from "./InputSearch.style";
|
|
5
5
|
import { InputText, InputTextProps, InputTextRef } from "../InputText";
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
RegisterOptions,
|
|
25
25
|
} from "react-hook-form";
|
|
26
26
|
import { IconNames } from "@jobber/design";
|
|
27
|
-
import identity from "lodash
|
|
27
|
+
import identity from "lodash/identity";
|
|
28
28
|
import { styles } from "./InputText.style";
|
|
29
29
|
import { useInputAccessoriesContext } from "./context";
|
|
30
30
|
import { useFormController } from "../hooks";
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ 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";
|