@artsy/palette-mobile 13.2.16 → 13.2.18

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.
@@ -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)(propValue ?? defaultValue);
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,10 @@ 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
- setValue(text);
116
- onChangeText?.(text);
117
- }, [onChangeText]);
124
+ const newText = (0, maskValue_1.maskValue)({ currentValue: text, mask: mask, previousValue: value });
125
+ setValue(newText);
126
+ onChangeText?.(newText, (0, maskValue_1.unmaskText)(text));
127
+ }, [onChangeText, value, mask]);
118
128
  const handleClear = (0, react_1.useCallback)(() => {
119
129
  react_native_1.LayoutAnimation.configureNext({ ...react_native_1.LayoutAnimation.Presets.easeInEaseOut, duration: 200 });
120
130
  inputRef.current?.clear();
@@ -0,0 +1,6 @@
1
+ export declare const maskValue: ({ currentValue, mask, previousValue, }: {
2
+ currentValue: string;
3
+ mask: string | string[] | undefined;
4
+ previousValue?: string | undefined;
5
+ }) => string;
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
+ });
@@ -1,3 +1,7 @@
1
1
  /// <reference types="react" />
2
2
  import { FlatListProps } from "react-native";
3
- export declare function ScreenFlatList<T>(props: FlatListProps<T>): JSX.Element;
3
+ import Animated from "react-native-reanimated";
4
+ export type ScreenFlatListProps<T> = FlatListProps<T> & {
5
+ innerRef?: React.Ref<Animated.FlatList<T>>;
6
+ };
7
+ export declare function ScreenFlatList<T>(props: ScreenFlatListProps<T>): JSX.Element;
@@ -17,6 +17,6 @@ function ScreenFlatList(props) {
17
17
  else {
18
18
  handleScroll = scrollHandler;
19
19
  }
20
- return (0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.FlatList, { ...props, onScroll: handleScroll });
20
+ return (0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.FlatList, { ref: props.innerRef, ...props, onScroll: handleScroll });
21
21
  }
22
22
  exports.ScreenFlatList = ScreenFlatList;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artsy/palette-mobile",
3
- "version": "13.2.16",
3
+ "version": "13.2.18",
4
4
  "description": "Artsy's design system for React Native",
5
5
  "scripts": {
6
6
  "android": "RCT_METRO_PORT=8082 react-native run-android --port 8082",