@jobber/components-native 0.86.1-JOB-136074-a7a2b82.0 → 0.86.2-JOB-89949-9a8f51e.6

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.
Files changed (47) hide show
  1. package/dist/package.json +3 -9
  2. package/dist/src/Form/Form.js +1 -4
  3. package/dist/src/Form/components/FormCache/FormCache.js +1 -0
  4. package/dist/src/InputCurrency/InputCurrency.js +42 -30
  5. package/dist/src/InputEmail/InputEmail.js +12 -4
  6. package/dist/src/InputNumber/InputNumber.js +10 -4
  7. package/dist/src/Typography/Typography.js +13 -2
  8. package/dist/src/hooks/useAtlantisI18n/locales/en.json +1 -0
  9. package/dist/src/hooks/useAtlantisI18n/locales/es.json +1 -0
  10. package/dist/src/hooks/useFormController.js +5 -14
  11. package/dist/tsconfig.build.tsbuildinfo +1 -1
  12. package/dist/types/src/ActionLabel/ActionLabel.d.ts +3 -2
  13. package/dist/types/src/Form/components/FormCache/FormCache.d.ts +2 -2
  14. package/dist/types/src/Form/context/types.d.ts +2 -2
  15. package/dist/types/src/Form/hooks/useInternalForm.d.ts +2 -2
  16. package/dist/types/src/Form/types.d.ts +3 -3
  17. package/dist/types/src/Heading/Heading.d.ts +3 -2
  18. package/dist/types/src/Text/Text.d.ts +3 -2
  19. package/dist/types/src/Typography/Typography.d.ts +2 -2
  20. package/package.json +3 -9
  21. package/src/ActionLabel/ActionLabel.test.tsx +12 -0
  22. package/src/ActionLabel/ActionLabel.tsx +2 -2
  23. package/src/ActionLabel/__snapshots__/ActionLabel.test.tsx.snap +66 -0
  24. package/src/Form/Form.tsx +0 -6
  25. package/src/Form/components/FormCache/FormCache.tsx +4 -3
  26. package/src/Form/context/types.ts +2 -2
  27. package/src/Form/hooks/useInternalForm.ts +2 -1
  28. package/src/Form/types.ts +3 -4
  29. package/src/Heading/Heading.test.tsx +13 -0
  30. package/src/Heading/Heading.tsx +2 -2
  31. package/src/Heading/__snapshots__/Heading.test.tsx.snap +65 -0
  32. package/src/InputCurrency/InputCurrency.tsx +71 -57
  33. package/src/InputEmail/InputEmail.tsx +15 -8
  34. package/src/InputNumber/InputNumber.tsx +11 -7
  35. package/src/Text/Text.test.tsx +21 -0
  36. package/src/Text/Text.tsx +2 -2
  37. package/src/Text/__snapshots__/Text.test.tsx.snap +104 -0
  38. package/src/Typography/Typography.test.tsx +61 -0
  39. package/src/Typography/Typography.tsx +20 -4
  40. package/src/Typography/__snapshots__/Typography.test.tsx.snap +222 -0
  41. package/src/hooks/useAtlantisI18n/locales/en.json +1 -0
  42. package/src/hooks/useAtlantisI18n/locales/es.json +1 -0
  43. package/src/hooks/useFormController.ts +6 -13
  44. package/dist/src/utils/buildConfig/isEdgeToEdgeEnabled.js +0 -17
  45. package/dist/types/src/utils/buildConfig/isEdgeToEdgeEnabled.d.ts +0 -1
  46. package/src/types/buildConfig.d.ts +0 -7
  47. package/src/utils/buildConfig/isEdgeToEdgeEnabled.ts +0 -20
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.86.1-JOB-136074-a7a2b82.0+a7a2b8288",
3
+ "version": "0.86.2-JOB-89949-9a8f51e.6+9a8f51e76",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  "autolinker": "^4.0.0",
44
44
  "deepmerge": "^4.2.2",
45
45
  "lodash": "^4.17.21",
46
- "react-hook-form": "^7.30.0",
46
+ "react-hook-form": "^7.52.0",
47
47
  "react-intl": "^6.4.2",
48
48
  "react-native-keyboard-aware-scroll-view": "^0.9.5",
49
49
  "react-native-modalize": "^2.0.13",
@@ -86,7 +86,6 @@
86
86
  "react": "^18.2.0",
87
87
  "react-intl": "^6.4.2",
88
88
  "react-native": ">=0.76.0",
89
- "react-native-build-config": "^0.3.2",
90
89
  "react-native-gesture-handler": ">=2.10.0",
91
90
  "react-native-keyboard-aware-scroll-view": "^0.9.5",
92
91
  "react-native-modal-datetime-picker": " >=13.0.0",
@@ -96,10 +95,5 @@
96
95
  "react-native-safe-area-context": "^5.4.0",
97
96
  "react-native-svg": ">=12.0.0"
98
97
  },
99
- "peerDependenciesMeta": {
100
- "react-native-build-config": {
101
- "optional": true
102
- }
103
- },
104
- "gitHead": "a7a2b828816e7a2eae21ead0e0eba40d494e284a"
98
+ "gitHead": "9a8f51e766a4207a1f53367c96c62685d336868d"
105
99
  }
@@ -38,7 +38,6 @@ import { useScrollToError } from "./hooks/useScrollToError";
38
38
  import { FormSaveButton } from "./components/FormSaveButton";
39
39
  import { useSaveButtonPosition } from "./hooks/useSaveButtonPosition";
40
40
  import { FormCache } from "./components/FormCache/FormCache";
41
- import { isEdgeToEdgeEnabled } from "../utils/buildConfig/isEdgeToEdgeEnabled";
42
41
  import { InputAccessoriesProvider } from "../InputText";
43
42
  import { tokens } from "../utils/design";
44
43
  import { ErrorMessageProvider } from "../ErrorMessageWrapper";
@@ -103,14 +102,12 @@ function InternalForm({ children, onBeforeSubmit, onSubmit, onSubmitError, onSub
103
102
  setMessageBannerHeight(event.nativeEvent.layout.height);
104
103
  };
105
104
  const styles = useStyles();
106
- // Check if edge-to-edge is enabled using utility function
107
- const edgeToEdgeEnabled = isEdgeToEdgeEnabled();
108
105
  return (React.createElement(FormProvider, Object.assign({}, formMethods),
109
106
  React.createElement(React.Fragment, null,
110
107
  (isSubmitting || isSecondaryActionLoading) && React.createElement(FormMask, null),
111
108
  React.createElement(FormCache, { localCacheKey: localCacheKey, localCacheExclude: localCacheExclude, setLocalCache: setLocalCache }),
112
109
  React.createElement(FormBody, { keyboardHeight: calculateSaveButtonOffset(), submit: handleSubmit(internalSubmit), isFormSubmitting: isSubmitting, saveButtonLabel: saveButtonLabel, shouldRenderActionBar: saveButtonPosition === "sticky", renderStickySection: renderStickySection, secondaryActions: secondaryActions, setSecondaryActionLoading: setIsSecondaryActionLoading, setSaveButtonHeight: setSaveButtonHeight, saveButtonOffset: saveButtonOffset },
113
- React.createElement(KeyboardAwareScrollView, Object.assign({ enableResetScrollToCoords: false, enableAutomaticScroll: true, enableOnAndroid: edgeToEdgeEnabled, keyboardOpeningTime: Platform.OS === "ios" ? tokens["timing-slowest"] : 0, keyboardShouldPersistTaps: "handled", ref: scrollViewRef }, keyboardProps, { extraHeight: headerHeight, extraScrollHeight: edgeToEdgeEnabled ? 20 : 0, contentContainerStyle: !keyboardHeight && styles.scrollContentContainer }),
110
+ React.createElement(KeyboardAwareScrollView, Object.assign({ enableResetScrollToCoords: false, enableAutomaticScroll: true, keyboardOpeningTime: Platform.OS === "ios" ? tokens["timing-slowest"] : 0, keyboardShouldPersistTaps: "handled", ref: scrollViewRef }, keyboardProps, { extraHeight: headerHeight, contentContainerStyle: !keyboardHeight && styles.scrollContentContainer }),
114
111
  React.createElement(View, { onLayout: ({ nativeEvent }) => {
115
112
  setFormContentHeight(nativeEvent.layout.height);
116
113
  } },
@@ -28,5 +28,6 @@ export function FormCache({ localCacheExclude, localCacheKey, setLocalCache, })
28
28
  setLocalCache(formData);
29
29
  }
30
30
  }, [formData, isDirty, localCacheExclude, setLocalCache, shouldExclude]);
31
+ // eslint-disable-next-line react/jsx-no-useless-fragment
31
32
  return React.createElement(React.Fragment, null);
32
33
  }
@@ -24,6 +24,31 @@ const getKeyboard = (props) => {
24
24
  return "numeric";
25
25
  }
26
26
  };
27
+ const computeDisplayFromNumericInput = (numberedValue, decimalNumbers, decimalCount, maxLength, maxDecimalPlaces, decimalPlaces, formatNumber) => {
28
+ const transformedValue = limitInputWholeDigits(numberedValue, maxLength);
29
+ const stringValue = decimalNumbers !== ""
30
+ ? transformedValue.toString() + "." + decimalNumbers.slice(1)
31
+ : transformedValue.toString();
32
+ if (checkLastChar(stringValue)) {
33
+ const roundedDecimal = configureDecimal(decimalCount, maxDecimalPlaces, stringValue, decimalPlaces);
34
+ const internationalizedValueToDisplay = formatNumber(roundedDecimal, {
35
+ maximumFractionDigits: maxDecimalPlaces,
36
+ });
37
+ return {
38
+ onChangeValue: roundedDecimal,
39
+ displayValue: internationalizedValueToDisplay,
40
+ };
41
+ }
42
+ else {
43
+ const internationalizedValueToDisplay = formatNumber(transformedValue, {
44
+ maximumFractionDigits: maxDecimalPlaces,
45
+ }) + decimalNumbers;
46
+ return {
47
+ onChangeValue: transformedValue.toString() + "." + decimalNumbers.slice(1),
48
+ displayValue: internationalizedValueToDisplay,
49
+ };
50
+ }
51
+ };
27
52
  export function InputCurrency(props) {
28
53
  var _a, _b;
29
54
  const { showCurrencySymbol = true, maxDecimalPlaces = 5, decimalPlaces = 2, maxLength = 10, } = props;
@@ -35,52 +60,39 @@ export function InputCurrency(props) {
35
60
  });
36
61
  const internalValue = getInternalValue(props, field, intl.formatNumber);
37
62
  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
63
  const handleChange = (newValue) => {
64
+ var _a, _b;
63
65
  const [decimalCount, wholeIntegerValue, decimalNumbers] = parseGivenInput(newValue, floatSeparators.decimal);
64
66
  const numberedValue = wholeIntegerValue
65
67
  ? convertToNumber(wholeIntegerValue)
66
68
  : wholeIntegerValue;
67
69
  if (isValidNumber(numberedValue) && typeof numberedValue === "number") {
68
- checkDecimalAndI18nOfDisplayValue(numberedValue, decimalNumbers, decimalCount);
70
+ const result = computeDisplayFromNumericInput(numberedValue, decimalNumbers, decimalCount, maxLength, maxDecimalPlaces, decimalPlaces, intl.formatNumber);
71
+ const { onChangeValue, displayValue: valueToDisplay } = result;
72
+ (_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, onChangeValue);
73
+ setDisplayValue(valueToDisplay);
69
74
  }
70
75
  else {
71
76
  const value = (numberedValue === null || numberedValue === void 0 ? void 0 : numberedValue.toString()) + decimalNumbers;
72
- setOnChangeAndDisplayValues(value, value);
77
+ (_b = props.onChange) === null || _b === void 0 ? void 0 : _b.call(props, value);
78
+ setDisplayValue(value);
73
79
  }
74
80
  };
75
81
  const { t } = useAtlantisI18n();
82
+ const defaultValidations = {
83
+ pattern: {
84
+ value: NUMBER_VALIDATION_REGEX,
85
+ message: t("errors.notANumber"),
86
+ },
87
+ };
88
+ const mergedValidations = props.validations
89
+ ? Object.assign({}, defaultValidations, props.validations)
90
+ : defaultValidations;
76
91
  return (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: {
77
92
  output: val => {
78
93
  return val === null || val === void 0 ? void 0 : val.split(floatSeparators.group).join("").replace(floatSeparators.decimal, ".");
79
94
  },
80
- }, validations: Object.assign({ pattern: {
81
- value: NUMBER_VALIDATION_REGEX,
82
- message: t("errors.notANumber"),
83
- } }, props.validations), onBlur: () => {
95
+ }, validations: mergedValidations, onBlur: () => {
84
96
  var _a;
85
97
  (_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props);
86
98
  if (field.value === 0 ||
@@ -1,9 +1,17 @@
1
1
  import React, { forwardRef } from "react";
2
2
  import { InputText } from "../InputText";
3
+ import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
3
4
  export const InputEmail = forwardRef(InputEmailInternal);
4
5
  function InputEmailInternal(props, ref) {
5
- return (React.createElement(InputText, Object.assign({}, props, { ref: ref, autoCapitalize: "none", autoCorrect: false, keyboard: "email-address", validations: Object.assign({ pattern: {
6
- value: /[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?/,
7
- message: "Enter a valid email address (email@example.com)",
8
- } }, props.validations) })));
6
+ const { t } = useAtlantisI18n();
7
+ const defaultValidations = {
8
+ pattern: {
9
+ value: /[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?/,
10
+ message: t("InputEmail.enterEmail"),
11
+ },
12
+ };
13
+ const mergedValidations = props.validations
14
+ ? Object.assign({}, defaultValidations, props.validations)
15
+ : defaultValidations;
16
+ return (React.createElement(InputText, Object.assign({}, props, { ref: ref, autoCapitalize: "none", autoCorrect: false, keyboard: "email-address", validations: mergedValidations })));
9
17
  }
@@ -24,13 +24,19 @@ function InputNumberInternal(props, ref) {
24
24
  (_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, newValue);
25
25
  };
26
26
  const { inputTransform: convertToString, outputTransform: convertToNumber } = useNumberTransform(props.value);
27
+ const defaultValidations = {
28
+ pattern: {
29
+ value: NUMBER_VALIDATION_REGEX,
30
+ message: t("errors.notANumber"),
31
+ },
32
+ };
33
+ const mergedValidations = props.validations
34
+ ? Object.assign({}, defaultValidations, props.validations)
35
+ : defaultValidations;
27
36
  return (React.createElement(InputText, Object.assign({}, props, { keyboard: getKeyboard(), transform: {
28
37
  input: flow(convertToString, ((_a = props.transform) === null || _a === void 0 ? void 0 : _a.input) || identity),
29
38
  output: flow(convertToNumber, ((_b = props.transform) === null || _b === void 0 ? void 0 : _b.output) || identity),
30
- }, ref: ref, value: (_c = props.value) === null || _c === void 0 ? void 0 : _c.toString(), defaultValue: (_d = props.defaultValue) === null || _d === void 0 ? void 0 : _d.toString(), onChangeText: handleChange, validations: Object.assign({ pattern: {
31
- value: NUMBER_VALIDATION_REGEX,
32
- message: t("errors.notANumber"),
33
- } }, props.validations) })));
39
+ }, ref: ref, value: (_c = props.value) === null || _c === void 0 ? void 0 : _c.toString(), defaultValue: (_d = props.defaultValue) === null || _d === void 0 ? void 0 : _d.toString(), onChangeText: handleChange, validations: mergedValidations })));
34
40
  }
35
41
  function hasPeriodAtEnd(value) {
36
42
  // matches patterns like ".", "0.", "12.", "+1.", and "-0."
@@ -38,7 +38,7 @@ function InternalTypography({ fontFamily, fontStyle, fontWeight, transform, colo
38
38
  style.push(UNSAFE_style.textStyle);
39
39
  }
40
40
  const numberOfLinesForNativeText = maxNumberOfLines[maxLines];
41
- const text = getTransformedText(children, transform);
41
+ const content = transformChildren(children, transform);
42
42
  const accessibilityProps = hideFromScreenReader
43
43
  ? {
44
44
  accessibilityRole: "none",
@@ -50,7 +50,7 @@ function InternalTypography({ fontFamily, fontStyle, fontWeight, transform, colo
50
50
  const textComponent = (React.createElement(Text, Object.assign({ allowFontScaling,
51
51
  adjustsFontSizeToFit,
52
52
  style,
53
- numberOfLines: numberOfLinesForNativeText }, accessibilityProps, { maxFontSizeMultiplier: getScaleMultiplier(maxFontScaleSize, sizeAndHeight.fontSize), selectable: selectable, selectionColor: tokens["color-brand--highlight"], onTextLayout: onTextLayout }), text));
53
+ numberOfLines: numberOfLinesForNativeText }, accessibilityProps, { maxFontSizeMultiplier: getScaleMultiplier(maxFontScaleSize, sizeAndHeight.fontSize), selectable: selectable, selectionColor: tokens["color-brand--highlight"], onTextLayout: onTextLayout }), content));
54
54
  // If text is not selectable, there's no need for TypographyGestureDetector
55
55
  // since it only prevents accidental highlighting of selectable text
56
56
  if (!selectable) {
@@ -89,6 +89,17 @@ function getTransformedText(text, transform) {
89
89
  return text;
90
90
  }
91
91
  }
92
+ function transformChildren(children, transform) {
93
+ if (children == null)
94
+ return children;
95
+ return React.Children.map(children, child => {
96
+ if (typeof child === "string") {
97
+ return getTransformedText(child, transform);
98
+ }
99
+ // Keep non-string children (numbers, elements, fragments) unchanged
100
+ return child;
101
+ });
102
+ }
92
103
  function getColorStyle(styles, color, reverseTheme) {
93
104
  if (color === "default" || !color) {
94
105
  return styles.greyBlue;
@@ -18,6 +18,7 @@
18
18
  "goBack": "Go back",
19
19
  "InputFieldWrapper.clear": "Clear input",
20
20
  "InputPassword.enterPassword": "Enter a password",
21
+ "InputEmail.enterEmail": "Enter a valid email address (email@example.com)",
21
22
  "loading": "Loading",
22
23
  "menu": "Menu",
23
24
  "more": "More",
@@ -18,6 +18,7 @@
18
18
  "goBack": "Volver",
19
19
  "InputFieldWrapper.clear": "Borrar",
20
20
  "InputPassword.enterPassword": "Escriba una contraseña",
21
+ "InputEmail.enterEmail": "Introduzca una dirección de correo electrónico válida (email@example.com)",
21
22
  "loading": "Cargando",
22
23
  "menu": "Menú",
23
24
  "more": "Más",
@@ -1,8 +1,7 @@
1
1
  import { v1 } from "react-native-uuid";
2
- import { useController, useForm, useFormContext } from "react-hook-form";
2
+ import { get, useController, useForm, useFormContext } from "react-hook-form";
3
3
  import { useState } from "react";
4
4
  export function useFormController({ name, value, validations, }) {
5
- var _a, _b;
6
5
  const fieldName = useControlName(name);
7
6
  const form = useForm({
8
7
  mode: "onTouched",
@@ -17,18 +16,10 @@ export function useFormController({ name, value, validations, }) {
17
16
  defaultValue: value || undefined,
18
17
  });
19
18
  // The naming convention established by react-hook-form for arrays of fields is, for example, "emails.0.description".
20
- // This corresponds to the structure of the error object, e.g. { emails: { 0: { description: "foobar" } } }
21
- // We assume here that fields will either follow this period-delimited three-part convention, or else that they are simple and indivisible (e.g. "city").
22
- // TODO: Add support for two-part identifiers (e.g. "property.province")
23
- const fieldIdentifiers = fieldName.split(".");
24
- let error;
25
- if (fieldIdentifiers.length === 3) {
26
- const [section, item, identifier] = fieldIdentifiers;
27
- error = (_b = (_a = errors[section]) === null || _a === void 0 ? void 0 : _a[item]) === null || _b === void 0 ? void 0 : _b[identifier];
28
- }
29
- else {
30
- error = errors[fieldName];
31
- }
19
+ // Preserve original behavior: only treat three-part names as nested paths.
20
+ // For anything else, perform a flat lookup to avoid behavioral changes.
21
+ const identifiers = fieldName.split(".");
22
+ const error = identifiers.length === 3 ? get(errors, fieldName) : errors[fieldName];
32
23
  return { error, field };
33
24
  }
34
25
  function useControlName(name) {