@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
|
@@ -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
|
export const defaultValues = {
|
|
5
6
|
// The system time is "p"
|
|
6
7
|
timeFormat: "p",
|
|
@@ -10,6 +11,7 @@ export const defaultValues = {
|
|
|
10
11
|
return;
|
|
11
12
|
},
|
|
12
13
|
floatSeparators: { group: ",", decimal: "." },
|
|
14
|
+
currencySymbol: DEFAULT_CURRENCY_SYMBOL,
|
|
13
15
|
};
|
|
14
16
|
export const AtlantisContext = createContext(defaultValues);
|
|
15
17
|
export function useAtlantisContext() {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { NUMBER_VALIDATION_REGEX, checkLastChar, configureDecimal, convertToNumber, isValidNumber, limitInputWholeDigits, parseGivenInput, } from "./utils";
|
|
5
|
+
import { messages } from "./messages";
|
|
6
|
+
import { useAtlantisContext } from "../AtlantisContext";
|
|
7
|
+
import { InputText } from "../InputText";
|
|
8
|
+
import { useFormController } from "../hooks/useFormController";
|
|
9
|
+
export const getInternalValue = (props, field, formatNumber) => {
|
|
10
|
+
var _a, _b;
|
|
11
|
+
if (!props.value && !field.value)
|
|
12
|
+
return "";
|
|
13
|
+
return ((_b = (_a = props.value) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : formatNumber(field.value, {
|
|
14
|
+
maximumFractionDigits: props.maxDecimalPlaces,
|
|
15
|
+
}));
|
|
16
|
+
};
|
|
17
|
+
const getKeyboard = (props) => {
|
|
18
|
+
var _a;
|
|
19
|
+
if (Platform.OS === "ios") {
|
|
20
|
+
//since we are checking for which keyboard to use here, just implement default keyboard here instead of in params
|
|
21
|
+
return (_a = props.keyboard) !== null && _a !== void 0 ? _a : "numbers-and-punctuation";
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
return "numeric";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export function InputCurrency(props) {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
const { showCurrencySymbol = true, maxDecimalPlaces = 5, decimalPlaces = 2, maxLength = 10, } = props;
|
|
30
|
+
const intl = useIntl();
|
|
31
|
+
const { floatSeparators, currencySymbol } = useAtlantisContext();
|
|
32
|
+
const { field } = useFormController({
|
|
33
|
+
name: props.name,
|
|
34
|
+
value: props.value,
|
|
35
|
+
});
|
|
36
|
+
const internalValue = getInternalValue(props, field, intl.formatNumber);
|
|
37
|
+
const [displayValue, setDisplayValue] = useState(internalValue);
|
|
38
|
+
const setOnChangeAndDisplayValues = (onChangeValue, valueToDisplay) => {
|
|
39
|
+
var _a;
|
|
40
|
+
(_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, onChangeValue);
|
|
41
|
+
setDisplayValue(valueToDisplay);
|
|
42
|
+
};
|
|
43
|
+
const checkDecimalAndI18nOfDisplayValue = (numberedValue, decimalNumbers, decimalCount) => {
|
|
44
|
+
const transformedValue = limitInputWholeDigits(numberedValue, maxLength);
|
|
45
|
+
const stringValue = decimalNumbers !== ""
|
|
46
|
+
? transformedValue.toString() + "." + decimalNumbers.slice(1)
|
|
47
|
+
: transformedValue.toString();
|
|
48
|
+
if (checkLastChar(stringValue)) {
|
|
49
|
+
const roundedDecimal = configureDecimal(decimalCount, maxDecimalPlaces, stringValue, decimalPlaces);
|
|
50
|
+
const internationalizedValueToDisplay = intl.formatNumber(roundedDecimal, {
|
|
51
|
+
maximumFractionDigits: maxDecimalPlaces,
|
|
52
|
+
});
|
|
53
|
+
setOnChangeAndDisplayValues(roundedDecimal, internationalizedValueToDisplay);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const internationalizedValueToDisplay = intl.formatNumber(transformedValue, {
|
|
57
|
+
maximumFractionDigits: maxDecimalPlaces,
|
|
58
|
+
}) + decimalNumbers;
|
|
59
|
+
setOnChangeAndDisplayValues(transformedValue.toString() + "." + decimalNumbers.slice(1), internationalizedValueToDisplay);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const handleChange = (newValue) => {
|
|
63
|
+
const [decimalCount, wholeIntegerValue, decimalNumbers] = parseGivenInput(newValue, floatSeparators.decimal);
|
|
64
|
+
const numberedValue = wholeIntegerValue
|
|
65
|
+
? convertToNumber(wholeIntegerValue)
|
|
66
|
+
: wholeIntegerValue;
|
|
67
|
+
if (isValidNumber(numberedValue) && typeof numberedValue === "number") {
|
|
68
|
+
checkDecimalAndI18nOfDisplayValue(numberedValue, decimalNumbers, decimalCount);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const value = (numberedValue === null || numberedValue === void 0 ? void 0 : numberedValue.toString()) + decimalNumbers;
|
|
72
|
+
setOnChangeAndDisplayValues(value, value);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const { formatMessage } = useIntl();
|
|
76
|
+
return (React.createElement(React.Fragment, null,
|
|
77
|
+
React.createElement(InputText, Object.assign({}, props, { prefix: showCurrencySymbol ? { label: currencySymbol } : undefined, keyboard: getKeyboard(props), value: ((_a = props.value) === null || _a === void 0 ? void 0 : _a.toString()) || displayValue, defaultValue: (_b = props.defaultValue) === null || _b === void 0 ? void 0 : _b.toString(), onChangeText: handleChange, transform: {
|
|
78
|
+
output: val => {
|
|
79
|
+
return val === null || val === void 0 ? void 0 : val.split(floatSeparators.group).join("").replace(floatSeparators.decimal, ".");
|
|
80
|
+
},
|
|
81
|
+
}, validations: Object.assign({ pattern: {
|
|
82
|
+
value: NUMBER_VALIDATION_REGEX,
|
|
83
|
+
message: formatMessage(messages.notANumberError),
|
|
84
|
+
} }, props.validations), onBlur: () => {
|
|
85
|
+
var _a;
|
|
86
|
+
(_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
87
|
+
if (field.value === 0 ||
|
|
88
|
+
field.value === "" ||
|
|
89
|
+
field.value === undefined) {
|
|
90
|
+
setDisplayValue("0");
|
|
91
|
+
}
|
|
92
|
+
} }))));
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_CURRENCY_SYMBOL = "$";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineMessages } from "react-intl";
|
|
2
|
+
export const messages = defineMessages({
|
|
3
|
+
notANumberError: {
|
|
4
|
+
id: "notANumberError",
|
|
5
|
+
defaultMessage: "Enter a number",
|
|
6
|
+
description: "Error message shown when a non-numeric value is typed in number input",
|
|
7
|
+
},
|
|
8
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function countDecimal(value) {
|
|
2
|
+
const convertedValue = value.toString();
|
|
3
|
+
if (convertedValue.includes(".")) {
|
|
4
|
+
return convertedValue.split(".")[1].length;
|
|
5
|
+
}
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
export function limitInputWholeDigits(value, maxInputLength) {
|
|
9
|
+
let convertedValue = value.toString();
|
|
10
|
+
if (convertedValue.length > maxInputLength) {
|
|
11
|
+
convertedValue = convertedValue.slice(0, maxInputLength);
|
|
12
|
+
return parseFloat(convertedValue);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export function configureDecimal(decimalCount, maxDecimalPlaces, transformedValue, decimalPlaces) {
|
|
17
|
+
const targetDecimalPlaces = Math.min(Math.max(decimalCount, decimalPlaces), maxDecimalPlaces);
|
|
18
|
+
const precision = Math.pow(10, targetDecimalPlaces);
|
|
19
|
+
const convertedValue = Math.round(parseFloat(transformedValue) * precision) / precision;
|
|
20
|
+
return convertedValue;
|
|
21
|
+
}
|
|
22
|
+
export function convertToNumber(value) {
|
|
23
|
+
var _a;
|
|
24
|
+
const regexValidation = /^[0-9]*$/;
|
|
25
|
+
if ((_a = value === null || value === void 0 ? void 0 : value.match) === null || _a === void 0 ? void 0 : _a.call(value, regexValidation)) {
|
|
26
|
+
return parseFloat(value);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export const checkLastChar = (stringValue) => {
|
|
31
|
+
const lastChar = stringValue[stringValue.length - 1];
|
|
32
|
+
return Boolean(Number(stringValue)) && lastChar !== "0" && lastChar !== ".";
|
|
33
|
+
};
|
|
34
|
+
export const isValidNumber = (numberedValue) => {
|
|
35
|
+
return (typeof numberedValue === "number" &&
|
|
36
|
+
!isNaN(numberedValue) &&
|
|
37
|
+
numberedValue !== 0);
|
|
38
|
+
};
|
|
39
|
+
export const getDecimalNumbers = (value, decimalSeparator) => {
|
|
40
|
+
const decimalValue = value.split(".")[1];
|
|
41
|
+
if (!decimalValue) {
|
|
42
|
+
return decimalSeparator;
|
|
43
|
+
}
|
|
44
|
+
return `${decimalSeparator}${decimalValue}`;
|
|
45
|
+
};
|
|
46
|
+
export const parseGivenInput = (value, decimalSeparator) => {
|
|
47
|
+
let decimalCount = 0;
|
|
48
|
+
let decimalNumbers = "";
|
|
49
|
+
let wholeIntegerValue = value;
|
|
50
|
+
if (value === null || value === void 0 ? void 0 : value.includes(".")) {
|
|
51
|
+
const splittedValue = value === null || value === void 0 ? void 0 : value.split(".");
|
|
52
|
+
decimalCount = splittedValue[1].length;
|
|
53
|
+
wholeIntegerValue = splittedValue[0];
|
|
54
|
+
decimalNumbers = getDecimalNumbers(value, decimalSeparator);
|
|
55
|
+
}
|
|
56
|
+
return [decimalCount, wholeIntegerValue, decimalNumbers];
|
|
57
|
+
};
|
|
58
|
+
export const NUMBER_VALIDATION_REGEX = /^[-+]?(([0-9]*\.[0-9]+)|([0-9]+)|([0-9]+(\.?[0-9]+)?e[-+]?[0-9]+))$/;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import DateTimePicker from "react-native-modal-datetime-picker";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { useIntl } from "react-intl";
|
|
5
|
+
import { utcToZonedTime } from "date-fns-tz";
|
|
6
|
+
import { format as formatTime } from "date-fns";
|
|
7
|
+
import { styles } from "./InputTime.style";
|
|
8
|
+
import { messages } from "./messages";
|
|
9
|
+
import { getTimeZoneOffsetInMinutes, roundUpToNearestMinutes } from "./utils";
|
|
10
|
+
import { useAtlantisContext } from "../AtlantisContext";
|
|
11
|
+
import { InputPressable } from "../InputPressable";
|
|
12
|
+
import { FormField } from "../FormField";
|
|
13
|
+
const LOCALE_24_HOURS = "en_GB";
|
|
14
|
+
const LOCALE_12_HOURS = "en_US";
|
|
15
|
+
function formatInvalidState(error, invalid) {
|
|
16
|
+
if (invalid)
|
|
17
|
+
return invalid;
|
|
18
|
+
if (error && error.message) {
|
|
19
|
+
return error.message;
|
|
20
|
+
}
|
|
21
|
+
return Boolean(error);
|
|
22
|
+
}
|
|
23
|
+
export function InputTime(props) {
|
|
24
|
+
if (props.name) {
|
|
25
|
+
return (React.createElement(FormField, { name: props.name, validations: props.validations }, (field, error) => (React.createElement(InternalInputTime, Object.assign({}, props, { value: field.value, onChange: (newValue) => {
|
|
26
|
+
var _a;
|
|
27
|
+
field.onChange(newValue);
|
|
28
|
+
field.onBlur();
|
|
29
|
+
(_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, newValue);
|
|
30
|
+
}, invalid: formatInvalidState(error, props.invalid) })))));
|
|
31
|
+
}
|
|
32
|
+
return React.createElement(InternalInputTime, Object.assign({}, props));
|
|
33
|
+
}
|
|
34
|
+
function InternalInputTime({ clearable = "always", disabled, emptyValueLabel, invalid, placeholder, value, name, type = "scheduling", onChange, showIcon = true, }) {
|
|
35
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
36
|
+
const { formatMessage } = useIntl();
|
|
37
|
+
const { timeZone, timeFormat } = useAtlantisContext();
|
|
38
|
+
const is24Hour = timeFormat === "HH:mm";
|
|
39
|
+
const dateTime = useMemo(() => (typeof value === "string" ? new Date(value) : value), [value]);
|
|
40
|
+
const formattedTime = useMemo(() => {
|
|
41
|
+
if (dateTime) {
|
|
42
|
+
const zonedTime = utcToZonedTime(dateTime, timeZone);
|
|
43
|
+
return formatTime(zonedTime, timeFormat);
|
|
44
|
+
}
|
|
45
|
+
return emptyValueLabel;
|
|
46
|
+
}, [dateTime, emptyValueLabel, timeZone, timeFormat]);
|
|
47
|
+
const canClearTime = formattedTime === emptyValueLabel ? "never" : clearable;
|
|
48
|
+
return (React.createElement(View, { style: styles.container },
|
|
49
|
+
React.createElement(InputPressable, { clearable: canClearTime, disabled: disabled, invalid: invalid, placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : formatMessage(messages.time), prefix: showIcon ? { icon: "timer" } : undefined, value: formattedTime, onClear: handleClear, onPress: showDatePicker }),
|
|
50
|
+
React.createElement(DateTimePicker, { testID: "inputTime-Picker", minuteInterval: getMinuteInterval(type), date: getInitialPickerDate(dateTime), timeZoneOffsetInMinutes: getTimeZoneOffsetInMinutes(timeZone, dateTime), isVisible: showPicker, mode: "time", onCancel: handleCancel, onConfirm: handleConfirm, is24Hour: is24Hour, locale: is24Hour ? LOCALE_24_HOURS : LOCALE_12_HOURS })));
|
|
51
|
+
function showDatePicker() {
|
|
52
|
+
setShowPicker(true);
|
|
53
|
+
}
|
|
54
|
+
function handleConfirm(newValue) {
|
|
55
|
+
setShowPicker(false);
|
|
56
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(newValue);
|
|
57
|
+
}
|
|
58
|
+
function handleCancel() {
|
|
59
|
+
setShowPicker(false);
|
|
60
|
+
// Call onChange with the current value to trigger form's validation.
|
|
61
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(dateTime);
|
|
62
|
+
}
|
|
63
|
+
function handleClear() {
|
|
64
|
+
// Returns null only for Form controlled scenarios due to a limitation of
|
|
65
|
+
// react-hook-form that doesn't allow passing undefined to form values.
|
|
66
|
+
if (name) {
|
|
67
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(null);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(undefined);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function getInitialPickerDate(date) {
|
|
75
|
+
if (date)
|
|
76
|
+
return date;
|
|
77
|
+
return roundUpToNearestMinutes(new Date(), 30);
|
|
78
|
+
}
|
|
79
|
+
function getMinuteInterval(type) {
|
|
80
|
+
if (type === "granular")
|
|
81
|
+
return 1;
|
|
82
|
+
return 5;
|
|
83
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getTimezoneOffset } from "date-fns-tz";
|
|
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 function roundUpToNearestMinutes(date, minutes) {
|
|
11
|
+
const ms = 1000 * 60 * minutes;
|
|
12
|
+
return new Date(Math.ceil(date.getTime() / ms) * ms);
|
|
13
|
+
}
|
|
14
|
+
export function getTimeZoneOffsetInMinutes(timeZone, date) {
|
|
15
|
+
return getTimezoneOffset(timeZone, date) / 1000 / 60;
|
|
16
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -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";
|