@artsy/palette-mobile 13.2.17 → 13.2.19
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/elements/Input/Input.d.ts +12 -1
- package/dist/elements/Input/Input.js +23 -7
- package/dist/elements/Input/maskValue.d.ts +6 -0
- package/dist/elements/Input/maskValue.js +49 -0
- package/dist/elements/Input/maskValue.tests.d.ts +1 -0
- package/dist/elements/Input/maskValue.tests.js +58 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { EventEmitter } from "events";
|
|
|
4
4
|
import { TextInput, TextInputProps } from "react-native";
|
|
5
5
|
export declare const inputEvents: EventEmitter;
|
|
6
6
|
export declare const emitInputClearEvent: () => void;
|
|
7
|
-
export interface InputProps extends Omit<TextInputProps, "placeholder"> {
|
|
7
|
+
export interface InputProps extends Omit<TextInputProps, "placeholder" | "onChangeText"> {
|
|
8
8
|
addClearListener?: boolean;
|
|
9
9
|
/**
|
|
10
10
|
* We are applying some optimisations to make sure the UX is smooth
|
|
@@ -22,6 +22,7 @@ export interface InputProps extends Omit<TextInputProps, "placeholder"> {
|
|
|
22
22
|
onHintPress?: () => void;
|
|
23
23
|
onSelectTap?: () => void;
|
|
24
24
|
optional?: boolean;
|
|
25
|
+
onChangeText?: (text: string, unmaskedText?: string) => void;
|
|
25
26
|
/**
|
|
26
27
|
* The placeholder can be an array of string, specifically for android, because of a bug.
|
|
27
28
|
* On ios, the longest string will always be picked, as ios can add ellipsis.
|
|
@@ -49,6 +50,16 @@ export interface InputProps extends Omit<TextInputProps, "placeholder"> {
|
|
|
49
50
|
showLimit?: boolean;
|
|
50
51
|
title?: string;
|
|
51
52
|
unit?: string | undefined | null;
|
|
53
|
+
/**
|
|
54
|
+
* A mask to apply to the input value.
|
|
55
|
+
* Make sure to use mask values using only the digit 9 and non-digit characters.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* <Input mask="999-99-9999" />
|
|
59
|
+
* <Input mask="(999)-99-9999 999-99-9999" />
|
|
60
|
+
* <Input mask="999-99-9999 999-99-9999 999-99-9999" />
|
|
61
|
+
*/
|
|
62
|
+
mask?: string | string[] | undefined;
|
|
52
63
|
}
|
|
53
64
|
export declare const HORIZONTAL_PADDING = 15;
|
|
54
65
|
export declare const INPUT_BORDER_RADIUS = 4;
|
|
@@ -38,6 +38,7 @@ const react_native_1 = require("react-native");
|
|
|
38
38
|
const react_native_reanimated_1 = __importStar(require("react-native-reanimated"));
|
|
39
39
|
const styled_components_1 = __importDefault(require("styled-components"));
|
|
40
40
|
const helpers_1 = require("./helpers");
|
|
41
|
+
const maskValue_1 = require("./maskValue");
|
|
41
42
|
const svgs_1 = require("../../svgs");
|
|
42
43
|
const hooks_1 = require("../../utils/hooks");
|
|
43
44
|
const useMeasure_1 = require("../../utils/hooks/useMeasure");
|
|
@@ -58,11 +59,14 @@ exports.MULTILINE_INPUT_MAX_HEIGHT = 300;
|
|
|
58
59
|
exports.LABEL_HEIGHT = 25;
|
|
59
60
|
exports.LEFT_COMPONENT_WIDTH = 40;
|
|
60
61
|
exports.SELECT_COMPONENT_WIDTH = 120;
|
|
61
|
-
exports.Input = (0, react_1.forwardRef)(({ addClearListener = false, defaultValue, disabled = false, enableClearButton = false, fixedRightPlaceholder, hintText = "What's this?", icon, leftComponentWidth = exports.LEFT_COMPONENT_WIDTH, selectComponentWidth = exports.SELECT_COMPONENT_WIDTH, loading = false, onBlur, onChangeText, onClear, onFocus, onSelectTap, placeholder, secureTextEntry = false, style: styleProp = {}, unit, value: propValue, selectDisplayLabel, ...props }, ref) => {
|
|
62
|
+
exports.Input = (0, react_1.forwardRef)(({ addClearListener = false, defaultValue, disabled = false, enableClearButton = false, fixedRightPlaceholder, hintText = "What's this?", icon, leftComponentWidth = exports.LEFT_COMPONENT_WIDTH, mask, selectComponentWidth = exports.SELECT_COMPONENT_WIDTH, loading = false, onBlur, onChangeText, onClear, onFocus, onSelectTap, placeholder, secureTextEntry = false, style: styleProp = {}, unit, value: propValue, selectDisplayLabel, ...props }, ref) => {
|
|
62
63
|
const { color, theme, space } = (0, hooks_1.useTheme)();
|
|
63
64
|
const [focused, setIsFocused] = (0, react_1.useState)(false);
|
|
64
65
|
const [delayedFocused, setDelayedFocused] = (0, react_1.useState)(false);
|
|
65
|
-
const [value, setValue] = (0, react_1.useState)(
|
|
66
|
+
const [value, setValue] = (0, react_1.useState)((0, maskValue_1.maskValue)({
|
|
67
|
+
currentValue: propValue ?? defaultValue,
|
|
68
|
+
mask: mask,
|
|
69
|
+
}));
|
|
66
70
|
const [showPassword, setShowPassword] = (0, react_1.useState)(!secureTextEntry);
|
|
67
71
|
const [inputWidth, setInputWidth] = (0, react_1.useState)(0);
|
|
68
72
|
const placeholderWidths = (0, react_1.useRef)([]);
|
|
@@ -82,9 +86,14 @@ exports.Input = (0, react_1.forwardRef)(({ addClearListener = false, defaultValu
|
|
|
82
86
|
// If the prop value changes, update the local state
|
|
83
87
|
// This optimisation is not needed if no propValue has been specified
|
|
84
88
|
if (propValue !== undefined && propValue !== value) {
|
|
85
|
-
setValue(propValue);
|
|
89
|
+
setValue((0, maskValue_1.maskValue)({ currentValue: propValue || "", mask }));
|
|
86
90
|
}
|
|
87
|
-
}, [propValue, value]);
|
|
91
|
+
}, [propValue, value, mask]);
|
|
92
|
+
(0, react_1.useEffect)(() => {
|
|
93
|
+
// If the mask value changes, update the value state to be formatted again
|
|
94
|
+
setValue((0, maskValue_1.maskValue)({ currentValue: value, mask }));
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [mask]);
|
|
88
97
|
const fontFamily = theme.fonts.sans.regular;
|
|
89
98
|
(0, react_1.useEffect)(() => {
|
|
90
99
|
/* to make the font work for secure text inputs,
|
|
@@ -112,9 +121,16 @@ exports.Input = (0, react_1.forwardRef)(({ addClearListener = false, defaultValu
|
|
|
112
121
|
};
|
|
113
122
|
}, [focused, delayedFocused]);
|
|
114
123
|
const handleChangeText = (0, react_1.useCallback)((text) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
if (mask) {
|
|
125
|
+
const newText = (0, maskValue_1.maskValue)({ currentValue: text, mask: mask, previousValue: value }) || "";
|
|
126
|
+
setValue(newText);
|
|
127
|
+
onChangeText?.(newText, (0, maskValue_1.unmaskText)(text));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
setValue(text);
|
|
131
|
+
onChangeText?.(text);
|
|
132
|
+
}
|
|
133
|
+
}, [onChangeText, value, mask]);
|
|
118
134
|
const handleClear = (0, react_1.useCallback)(() => {
|
|
119
135
|
react_native_1.LayoutAnimation.configureNext({ ...react_native_1.LayoutAnimation.Presets.easeInEaseOut, duration: 200 });
|
|
120
136
|
inputRef.current?.clear();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const maskValue: ({ currentValue, mask, previousValue, }: {
|
|
2
|
+
currentValue: string | undefined;
|
|
3
|
+
mask: string | string[] | undefined;
|
|
4
|
+
previousValue?: string | undefined;
|
|
5
|
+
}) => string | undefined;
|
|
6
|
+
export declare const unmaskText: (mask: string) => string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.unmaskText = exports.maskValue = void 0;
|
|
4
|
+
const maskString = (value, mask) => {
|
|
5
|
+
let result = mask.replace(/9/g, "_");
|
|
6
|
+
for (const digit of value.replace(/\D/g, "")) {
|
|
7
|
+
if (result.includes("_")) {
|
|
8
|
+
result = result.replace("_", digit);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
result = result + digit;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (result.includes("_")) {
|
|
15
|
+
result = result.slice(0, result.indexOf("_"));
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
const maskValue = ({ currentValue, mask, previousValue = "", }) => {
|
|
20
|
+
if (!currentValue || !mask || mask.length === 0) {
|
|
21
|
+
return currentValue;
|
|
22
|
+
}
|
|
23
|
+
const value = (0, exports.unmaskText)(currentValue);
|
|
24
|
+
if (previousValue && previousValue.length > currentValue.length) {
|
|
25
|
+
// user is deleting, don't mess with the format
|
|
26
|
+
return currentValue;
|
|
27
|
+
}
|
|
28
|
+
if (typeof mask === "string") {
|
|
29
|
+
return maskString(currentValue, mask);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
if (value.length <= (0, exports.unmaskText)(mask[0]).length) {
|
|
33
|
+
return maskString(value, mask[0]);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const nearestMask = [...mask].reverse().find((m) => {
|
|
37
|
+
if (value.length >= (0, exports.unmaskText)(m).length) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}) || mask[0];
|
|
42
|
+
return maskString(value, nearestMask);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
exports.maskValue = maskValue;
|
|
47
|
+
// Helper method to clean the mask and remove all non-digits and spaces
|
|
48
|
+
const unmaskText = (mask) => mask.replace(/\W/g, "");
|
|
49
|
+
exports.unmaskText = unmaskText;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const maskValue_1 = require("./maskValue");
|
|
4
|
+
const GERMANY_PHONE_MASKS = [
|
|
5
|
+
"999 999",
|
|
6
|
+
"999 9999",
|
|
7
|
+
"999 99999",
|
|
8
|
+
"999 999999",
|
|
9
|
+
"999 9999999",
|
|
10
|
+
"999 99999999",
|
|
11
|
+
"9999 99999999",
|
|
12
|
+
];
|
|
13
|
+
const TUNISIA_PHONE_MASK = "99 999 999";
|
|
14
|
+
const UNITED_STATES_PHONE_MASK = "999 999 9999";
|
|
15
|
+
describe(maskValue_1.maskValue, () => {
|
|
16
|
+
it("leaves the value value alone if the user is deleting characters", () => {
|
|
17
|
+
expect((0, maskValue_1.maskValue)({
|
|
18
|
+
currentValue: "017672458053",
|
|
19
|
+
mask: GERMANY_PHONE_MASKS,
|
|
20
|
+
})).toBe("0176 72458053");
|
|
21
|
+
expect((0, maskValue_1.maskValue)({
|
|
22
|
+
currentValue: "17672458053",
|
|
23
|
+
mask: GERMANY_PHONE_MASKS,
|
|
24
|
+
})).toBe("176 72458053");
|
|
25
|
+
expect((0, maskValue_1.maskValue)({
|
|
26
|
+
currentValue: "030901820",
|
|
27
|
+
mask: GERMANY_PHONE_MASKS,
|
|
28
|
+
})).toBe("030 901820");
|
|
29
|
+
expect((0, maskValue_1.maskValue)({
|
|
30
|
+
currentValue: "901820",
|
|
31
|
+
mask: GERMANY_PHONE_MASKS,
|
|
32
|
+
})).toBe("901 820");
|
|
33
|
+
expect((0, maskValue_1.maskValue)({
|
|
34
|
+
currentValue: "20335902",
|
|
35
|
+
mask: TUNISIA_PHONE_MASK,
|
|
36
|
+
})).toBe("20 335 902");
|
|
37
|
+
});
|
|
38
|
+
it("works with empty strings", () => {
|
|
39
|
+
expect((0, maskValue_1.maskValue)({
|
|
40
|
+
currentValue: "",
|
|
41
|
+
mask: GERMANY_PHONE_MASKS,
|
|
42
|
+
})).toBe("");
|
|
43
|
+
expect((0, maskValue_1.maskValue)({
|
|
44
|
+
currentValue: "",
|
|
45
|
+
mask: "",
|
|
46
|
+
})).toBe("");
|
|
47
|
+
});
|
|
48
|
+
it("formats a given phone number to the given country's default format", () => {
|
|
49
|
+
expect((0, maskValue_1.maskValue)({
|
|
50
|
+
currentValue: "7825577664",
|
|
51
|
+
mask: UNITED_STATES_PHONE_MASK,
|
|
52
|
+
})).toBe("782 557 7664");
|
|
53
|
+
expect((0, maskValue_1.maskValue)({
|
|
54
|
+
currentValue: "782557766",
|
|
55
|
+
mask: UNITED_STATES_PHONE_MASK,
|
|
56
|
+
})).toBe("782 557 766");
|
|
57
|
+
});
|
|
58
|
+
});
|