@jobber/components-native 0.21.1 → 0.22.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.
Files changed (40) hide show
  1. package/dist/src/InputText/InputText.js +138 -0
  2. package/dist/src/InputText/InputText.style.js +20 -0
  3. package/dist/src/InputText/context/InputAccessoriesContext.js +17 -0
  4. package/dist/src/InputText/context/InputAccessoriesProvider.js +73 -0
  5. package/dist/src/InputText/context/InputAccessoriesProvider.style.js +21 -0
  6. package/dist/src/InputText/context/InputAccessory.style.js +16 -0
  7. package/dist/src/InputText/context/index.js +2 -0
  8. package/dist/src/InputText/context/types.js +1 -0
  9. package/dist/src/InputText/index.js +2 -0
  10. package/dist/src/hooks/index.js +1 -0
  11. package/dist/src/hooks/useFormController.js +38 -0
  12. package/dist/src/index.js +1 -0
  13. package/dist/tsconfig.tsbuildinfo +1 -1
  14. package/dist/types/src/InputText/InputText.d.ts +165 -0
  15. package/dist/types/src/InputText/InputText.style.d.ts +16 -0
  16. package/dist/types/src/InputText/context/InputAccessoriesContext.d.ts +4 -0
  17. package/dist/types/src/InputText/context/InputAccessoriesProvider.d.ts +4 -0
  18. package/dist/types/src/InputText/context/InputAccessoriesProvider.style.d.ts +17 -0
  19. package/dist/types/src/InputText/context/InputAccessory.style.d.ts +14 -0
  20. package/dist/types/src/InputText/context/index.d.ts +2 -0
  21. package/dist/types/src/InputText/context/types.d.ts +23 -0
  22. package/dist/types/src/InputText/index.d.ts +3 -0
  23. package/dist/types/src/hooks/index.d.ts +1 -0
  24. package/dist/types/src/hooks/useFormController.d.ts +12 -0
  25. package/dist/types/src/index.d.ts +1 -0
  26. package/package.json +4 -2
  27. package/src/InputText/InputText.style.ts +25 -0
  28. package/src/InputText/InputText.test.tsx +534 -0
  29. package/src/InputText/InputText.tsx +483 -0
  30. package/src/InputText/context/InputAccessoriesContext.ts +21 -0
  31. package/src/InputText/context/InputAccessoriesProvider.style.tsx +23 -0
  32. package/src/InputText/context/InputAccessoriesProvider.test.tsx +84 -0
  33. package/src/InputText/context/InputAccessoriesProvider.tsx +121 -0
  34. package/src/InputText/context/InputAccessory.style.ts +17 -0
  35. package/src/InputText/context/index.ts +2 -0
  36. package/src/InputText/context/types.ts +28 -0
  37. package/src/InputText/index.ts +3 -0
  38. package/src/hooks/index.ts +1 -0
  39. package/src/hooks/useFormController.ts +68 -0
  40. package/src/index.ts +1 -0
@@ -0,0 +1,121 @@
1
+ import React, { ReactNode, useEffect, useRef, useState } from "react";
2
+ import {
3
+ InputAccessoryView,
4
+ Keyboard,
5
+ Platform,
6
+ // eslint-disable-next-line no-restricted-imports
7
+ Button as RNButton,
8
+ View,
9
+ useColorScheme,
10
+ } from "react-native";
11
+ import { v4 } from "react-native-uuid";
12
+ import { InputAccessoriesContext } from "./InputAccessoriesContext";
13
+ import { styles } from "./InputAccessoriesProvider.style";
14
+
15
+ export function InputAccessoriesProvider({
16
+ children,
17
+ }: {
18
+ children: ReactNode;
19
+ }): JSX.Element {
20
+ const inputAccessoryID = useRef(v4()).current;
21
+ const {
22
+ focusedInput,
23
+ setFocusedInput,
24
+ canFocusNext,
25
+ canFocusPrevious,
26
+ elements,
27
+ setElements,
28
+ previousKey,
29
+ nextKey,
30
+ } = useInputAccessoriesProviderState();
31
+
32
+ const colorScheme = useColorScheme();
33
+
34
+ return (
35
+ <InputAccessoriesContext.Provider
36
+ value={{
37
+ elements,
38
+ focusedInput,
39
+ canFocusNext,
40
+ canFocusPrevious,
41
+ inputAccessoryID,
42
+ register,
43
+ unregister,
44
+ onFocusNext,
45
+ onFocusPrevious,
46
+ setFocusedInput,
47
+ }}
48
+ >
49
+ {children}
50
+ {Platform.OS === "ios" && (
51
+ <InputAccessoryView nativeID={inputAccessoryID}>
52
+ <View
53
+ testID="ATL-InputAccessory"
54
+ style={[
55
+ styles.container,
56
+ colorScheme === "dark" ? styles.darkTheme : styles.lightTheme,
57
+ ]}
58
+ >
59
+ <RNButton
60
+ onPress={Keyboard.dismiss}
61
+ title="Done"
62
+ testID="ATL-InputAccessory-Done"
63
+ color={colorScheme === "dark" ? "white" : undefined}
64
+ />
65
+ </View>
66
+ </InputAccessoryView>
67
+ )}
68
+ </InputAccessoriesContext.Provider>
69
+ );
70
+
71
+ function register(name: string, onFocus: () => void) {
72
+ elements[name] = onFocus;
73
+ setElements(elements);
74
+ }
75
+
76
+ function unregister(name: string) {
77
+ delete elements[name];
78
+ setElements(elements);
79
+ }
80
+
81
+ function onFocusNext() {
82
+ const nextElement = elements[nextKey];
83
+ nextElement?.();
84
+ }
85
+
86
+ function onFocusPrevious() {
87
+ const previousElement = elements[previousKey];
88
+ previousElement?.();
89
+ }
90
+ }
91
+
92
+ function useInputAccessoriesProviderState() {
93
+ const [focusedInput, setFocusedInput] = useState("");
94
+ const [canFocusNext, setCanFocusNext] = useState(false);
95
+ const [canFocusPrevious, setCanFocusPrevious] = useState(false);
96
+ const [elements, setElements] = useState<Record<string, () => void>>({});
97
+
98
+ const keys = Object.keys(elements);
99
+ const selectedIndex = keys.findIndex(key => key === focusedInput);
100
+
101
+ const nextKey = keys[selectedIndex + 1];
102
+ const previousKey = keys[selectedIndex - 1];
103
+
104
+ useEffect(() => {
105
+ setCanFocusNext(Boolean(nextKey));
106
+ setCanFocusPrevious(Boolean(previousKey));
107
+ }, [previousKey, nextKey]);
108
+
109
+ return {
110
+ previousKey,
111
+ nextKey,
112
+ focusedInput,
113
+ setFocusedInput,
114
+ canFocusNext,
115
+ setCanFocusNext,
116
+ canFocusPrevious,
117
+ setCanFocusPrevious,
118
+ elements,
119
+ setElements,
120
+ };
121
+ }
@@ -0,0 +1,17 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ container: {
6
+ flexDirection: "row",
7
+ justifyContent: "space-between",
8
+ alignItems: "center",
9
+ paddingHorizontal: tokens["space-small"],
10
+ backgroundColor: tokens["color-surface--background"],
11
+ borderTopWidth: tokens["space-minuscule"],
12
+ borderTopColor: tokens["color-border"],
13
+ },
14
+ buttonContainer: {
15
+ flexDirection: "row",
16
+ },
17
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./InputAccessoriesContext";
2
+ export * from "./InputAccessoriesProvider";
@@ -0,0 +1,28 @@
1
+ type Name = string;
2
+
3
+ export interface InputAccessoriesContextProps {
4
+ /**
5
+ * Registered elements.
6
+ */
7
+ readonly elements: Record<Name, () => void>;
8
+
9
+ readonly focusedInput: string;
10
+
11
+ readonly canFocusNext: boolean;
12
+ readonly canFocusPrevious: boolean;
13
+ readonly inputAccessoryID?: string;
14
+
15
+ /**
16
+ * Registers the element to the context.
17
+ */
18
+ readonly register: (name: Name, onFocus: () => void) => void;
19
+
20
+ /**
21
+ * Un-registers the element from the context.
22
+ */
23
+ readonly unregister: (name: Name) => void;
24
+
25
+ readonly onFocusNext: () => void;
26
+ readonly onFocusPrevious: () => void;
27
+ readonly setFocusedInput: (name: string) => void;
28
+ }
@@ -0,0 +1,3 @@
1
+ export { InputText } from "./InputText";
2
+ export type { InputTextRef, InputTextProps } from "./InputText";
3
+ export * from "./context";
@@ -0,0 +1 @@
1
+ export * from "./useFormController";
@@ -0,0 +1,68 @@
1
+ import { v1 } from "react-native-uuid";
2
+ import {
3
+ FieldError,
4
+ RegisterOptions,
5
+ UseControllerReturn,
6
+ useController,
7
+ useForm,
8
+ useFormContext,
9
+ } from "react-hook-form";
10
+ import { useState } from "react";
11
+
12
+ interface UseFormControllerProps<T> {
13
+ name?: string;
14
+ value?: T;
15
+ validations?: RegisterOptions;
16
+ }
17
+
18
+ interface UseFormController {
19
+ error?: FieldError;
20
+ field: UseControllerReturn["field"];
21
+ }
22
+
23
+ export function useFormController<T>({
24
+ name,
25
+ value,
26
+ validations,
27
+ }: UseFormControllerProps<T>): UseFormController {
28
+ const fieldName = useControlName(name);
29
+
30
+ const form = useForm<{ fieldName: T }>({
31
+ mode: "onTouched",
32
+ defaultValues: { [fieldName]: value },
33
+ });
34
+ const formContext = useFormContext();
35
+
36
+ const {
37
+ control,
38
+ formState: { errors },
39
+ } = formContext || form;
40
+
41
+ const { field } = useController({
42
+ name: fieldName,
43
+ control,
44
+ rules: validations,
45
+ defaultValue: value || undefined,
46
+ });
47
+
48
+ // The naming convention established by react-hook-form for arrays of fields is, for example, "emails.0.description".
49
+ // This corresponds to the structure of the error object, e.g. { emails: { 0: { description: "foobar" } } }
50
+ // 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").
51
+ // TODO: Add support for two-part identifiers (e.g. "property.province")
52
+ const fieldIdentifiers = fieldName.split(".");
53
+ let error: FieldError | undefined;
54
+ if (fieldIdentifiers.length === 3) {
55
+ const [section, item, identifier] = fieldIdentifiers;
56
+ error = errors[section]?.[item]?.[identifier];
57
+ } else {
58
+ error = errors[fieldName];
59
+ }
60
+
61
+ return { error, field };
62
+ }
63
+
64
+ function useControlName(name?: string): UseControllerReturn["field"]["name"] {
65
+ const [identifier] = useState(v1());
66
+ const prefix = `generatedName--${identifier}`;
67
+ return `${name || prefix}` as const;
68
+ }
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./Icon";
15
15
  export * from "./IconButton";
16
16
  export * from "./InputFieldWrapper";
17
17
  export * from "./InputPressable";
18
+ export * from "./InputText";
18
19
  export * from "./ProgressBar";
19
20
  export * from "./StatusLabel";
20
21
  export * from "./Text";