@jobber/components-native 0.38.0 → 0.40.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 (184) hide show
  1. package/dist/src/AtlantisContext/AtlantisContext.js +2 -0
  2. package/dist/src/Form/Form.js +187 -0
  3. package/dist/src/Form/Form.style.js +33 -0
  4. package/dist/src/Form/components/FormActionBar/FormActionBar.js +21 -0
  5. package/dist/src/Form/components/FormActionBar/FormActionBar.style.js +5 -0
  6. package/dist/src/Form/components/FormActionBar/index.js +1 -0
  7. package/dist/src/Form/components/FormBody/FormBody.js +20 -0
  8. package/dist/src/Form/components/FormBody/FormBody.style.js +26 -0
  9. package/dist/src/Form/components/FormBody/index.js +1 -0
  10. package/dist/src/Form/components/FormCache/FormCache.js +34 -0
  11. package/dist/src/Form/components/FormErrorBanner/FormErrorBanner.js +21 -0
  12. package/dist/src/Form/components/FormErrorBanner/index.js +1 -0
  13. package/dist/src/Form/components/FormErrorBanner/messages.js +13 -0
  14. package/dist/src/Form/components/FormMask/FormMask.js +11 -0
  15. package/dist/src/Form/components/FormMask/FormMask.style.js +15 -0
  16. package/dist/src/Form/components/FormMask/index.js +1 -0
  17. package/dist/src/Form/components/FormMessage/FormMessage.js +48 -0
  18. package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.js +28 -0
  19. package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.js +17 -0
  20. package/dist/src/Form/components/FormMessage/components/InternalFormMessage/index.js +1 -0
  21. package/dist/src/Form/components/FormMessage/components/InternalFormMessage/messages.js +8 -0
  22. package/dist/src/Form/components/FormMessage/index.js +1 -0
  23. package/dist/src/Form/components/FormMessageBanner/FormMessageBanner.js +15 -0
  24. package/dist/src/Form/components/FormMessageBanner/index.js +1 -0
  25. package/dist/src/Form/components/FormSaveButton/FormSaveButton.js +69 -0
  26. package/dist/src/Form/components/FormSaveButton/index.js +1 -0
  27. package/dist/src/Form/components/FormSaveButton/messages.js +8 -0
  28. package/dist/src/Form/constants.js +2 -0
  29. package/dist/src/Form/context/AtlantisFormContext.js +16 -0
  30. package/dist/src/Form/context/index.js +1 -0
  31. package/dist/src/Form/context/types.js +1 -0
  32. package/dist/src/Form/hooks/useFormViewRefs.js +14 -0
  33. package/dist/src/Form/hooks/useInternalForm.js +37 -0
  34. package/dist/src/Form/hooks/useOfflineHandler.js +24 -0
  35. package/dist/src/Form/hooks/useSaveButtonPosition.js +25 -0
  36. package/dist/src/Form/hooks/useScreenInformation.js +15 -0
  37. package/dist/src/Form/hooks/useScrollToError/index.js +1 -0
  38. package/dist/src/Form/hooks/useScrollToError/useScrollToError.js +63 -0
  39. package/dist/src/Form/index.js +4 -0
  40. package/dist/src/Form/messages.js +28 -0
  41. package/dist/src/Form/types.js +10 -0
  42. package/dist/src/InputDate/InputDate.js +76 -0
  43. package/dist/src/InputDate/index.js +1 -0
  44. package/dist/src/InputDate/messages.js +8 -0
  45. package/dist/src/Menu/Menu.js +67 -0
  46. package/dist/src/Menu/Menu.style.js +6 -0
  47. package/dist/src/Menu/components/MenuOption/MenuOption.js +25 -0
  48. package/dist/src/Menu/components/MenuOption/MenuOption.style.js +10 -0
  49. package/dist/src/Menu/components/MenuOption/index.js +1 -0
  50. package/dist/src/Menu/components/Overlay/Overlay.js +9 -0
  51. package/dist/src/Menu/components/Overlay/Overlay.style.js +6 -0
  52. package/dist/src/Menu/components/Overlay/index.js +1 -0
  53. package/dist/src/Menu/index.js +1 -0
  54. package/dist/src/Menu/messages.js +8 -0
  55. package/dist/src/Menu/types.js +1 -0
  56. package/dist/src/Menu/utils.js +84 -0
  57. package/dist/src/index.js +3 -0
  58. package/dist/tsconfig.tsbuildinfo +1 -1
  59. package/dist/types/src/AtlantisContext/AtlantisContext.d.ts +7 -1
  60. package/dist/types/src/Form/Form.d.ts +4 -0
  61. package/dist/types/src/Form/Form.style.d.ts +31 -0
  62. package/dist/types/src/Form/components/FormActionBar/FormActionBar.d.ts +13 -0
  63. package/dist/types/src/Form/components/FormActionBar/FormActionBar.style.d.ts +15 -0
  64. package/dist/types/src/Form/components/FormActionBar/index.d.ts +2 -0
  65. package/dist/types/src/Form/components/FormBody/FormBody.d.ts +10 -0
  66. package/dist/types/src/Form/components/FormBody/FormBody.style.d.ts +24 -0
  67. package/dist/types/src/Form/components/FormBody/index.d.ts +1 -0
  68. package/dist/types/src/Form/components/FormCache/FormCache.d.ts +10 -0
  69. package/dist/types/src/Form/components/FormErrorBanner/FormErrorBanner.d.ts +3 -0
  70. package/dist/types/src/Form/components/FormErrorBanner/index.d.ts +1 -0
  71. package/dist/types/src/Form/components/FormErrorBanner/messages.d.ts +12 -0
  72. package/dist/types/src/Form/components/FormMask/FormMask.d.ts +2 -0
  73. package/dist/types/src/Form/components/FormMask/FormMask.style.d.ts +13 -0
  74. package/dist/types/src/Form/components/FormMask/index.d.ts +1 -0
  75. package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +19 -0
  76. package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.d.ts +8 -0
  77. package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.d.ts +20 -0
  78. package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/index.d.ts +1 -0
  79. package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/messages.d.ts +7 -0
  80. package/dist/types/src/Form/components/FormMessage/index.d.ts +1 -0
  81. package/dist/types/src/Form/components/FormMessageBanner/FormMessageBanner.d.ts +7 -0
  82. package/dist/types/src/Form/components/FormMessageBanner/index.d.ts +1 -0
  83. package/dist/types/src/Form/components/FormSaveButton/FormSaveButton.d.ts +3 -0
  84. package/dist/types/src/Form/components/FormSaveButton/index.d.ts +1 -0
  85. package/dist/types/src/Form/components/FormSaveButton/messages.d.ts +7 -0
  86. package/dist/types/src/Form/constants.d.ts +2 -0
  87. package/dist/types/src/Form/context/AtlantisFormContext.d.ts +12 -0
  88. package/dist/types/src/Form/context/index.d.ts +2 -0
  89. package/dist/types/src/Form/context/types.d.ts +26 -0
  90. package/dist/types/src/Form/hooks/useFormViewRefs.d.ts +10 -0
  91. package/dist/types/src/Form/hooks/useInternalForm.d.ts +19 -0
  92. package/dist/types/src/Form/hooks/useOfflineHandler.d.ts +1 -0
  93. package/dist/types/src/Form/hooks/useSaveButtonPosition.d.ts +12 -0
  94. package/dist/types/src/Form/hooks/useScreenInformation.d.ts +8 -0
  95. package/dist/types/src/Form/hooks/useScrollToError/index.d.ts +1 -0
  96. package/dist/types/src/Form/hooks/useScrollToError/useScrollToError.d.ts +10 -0
  97. package/dist/types/src/Form/index.d.ts +5 -0
  98. package/dist/types/src/Form/messages.d.ts +27 -0
  99. package/dist/types/src/Form/types.d.ts +199 -0
  100. package/dist/types/src/InputDate/InputDate.d.ts +74 -0
  101. package/dist/types/src/InputDate/index.d.ts +1 -0
  102. package/dist/types/src/InputDate/messages.d.ts +7 -0
  103. package/dist/types/src/InputNumber/InputNumber.d.ts +1 -1
  104. package/dist/types/src/Menu/Menu.d.ts +3 -0
  105. package/dist/types/src/Menu/Menu.style.d.ts +18 -0
  106. package/dist/types/src/Menu/components/MenuOption/MenuOption.d.ts +3 -0
  107. package/dist/types/src/Menu/components/MenuOption/MenuOption.style.d.ts +8 -0
  108. package/dist/types/src/Menu/components/MenuOption/index.d.ts +1 -0
  109. package/dist/types/src/Menu/components/Overlay/Overlay.d.ts +3 -0
  110. package/dist/types/src/Menu/components/Overlay/Overlay.style.d.ts +12 -0
  111. package/dist/types/src/Menu/components/Overlay/index.d.ts +1 -0
  112. package/dist/types/src/Menu/index.d.ts +2 -0
  113. package/dist/types/src/Menu/messages.d.ts +7 -0
  114. package/dist/types/src/Menu/types.d.ts +22 -0
  115. package/dist/types/src/Menu/utils.d.ts +10 -0
  116. package/dist/types/src/index.d.ts +3 -0
  117. package/package.json +3 -2
  118. package/src/AtlantisContext/AtlantisContext.tsx +10 -1
  119. package/src/Form/Form.style.ts +34 -0
  120. package/src/Form/Form.test.tsx +588 -0
  121. package/src/Form/Form.tsx +296 -0
  122. package/src/Form/components/FormActionBar/FormActionBar.style.ts +11 -0
  123. package/src/Form/components/FormActionBar/FormActionBar.tsx +63 -0
  124. package/src/Form/components/FormActionBar/index.ts +2 -0
  125. package/src/Form/components/FormBody/FormBody.style.ts +27 -0
  126. package/src/Form/components/FormBody/FormBody.tsx +62 -0
  127. package/src/Form/components/FormBody/index.ts +1 -0
  128. package/src/Form/components/FormCache/FormCache.tsx +50 -0
  129. package/src/Form/components/FormErrorBanner/FormErrorBanner.test.tsx +124 -0
  130. package/src/Form/components/FormErrorBanner/FormErrorBanner.tsx +34 -0
  131. package/src/Form/components/FormErrorBanner/index.ts +1 -0
  132. package/src/Form/components/FormErrorBanner/messages.ts +14 -0
  133. package/src/Form/components/FormMask/FormMask.style.tsx +16 -0
  134. package/src/Form/components/FormMask/FormMask.tsx +19 -0
  135. package/src/Form/components/FormMask/index.ts +1 -0
  136. package/src/Form/components/FormMessage/FormMessage.test.tsx +72 -0
  137. package/src/Form/components/FormMessage/FormMessage.tsx +63 -0
  138. package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.ts +18 -0
  139. package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.tsx +55 -0
  140. package/src/Form/components/FormMessage/components/InternalFormMessage/index.ts +1 -0
  141. package/src/Form/components/FormMessage/components/InternalFormMessage/messages.ts +10 -0
  142. package/src/Form/components/FormMessage/index.ts +1 -0
  143. package/src/Form/components/FormMessageBanner/FormMessageBanner.test.tsx +27 -0
  144. package/src/Form/components/FormMessageBanner/FormMessageBanner.tsx +33 -0
  145. package/src/Form/components/FormMessageBanner/index.ts +1 -0
  146. package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +159 -0
  147. package/src/Form/components/FormSaveButton/FormSaveButton.tsx +103 -0
  148. package/src/Form/components/FormSaveButton/index.ts +1 -0
  149. package/src/Form/components/FormSaveButton/messages.ts +9 -0
  150. package/src/Form/constants.ts +2 -0
  151. package/src/Form/context/AtlantisFormContext.test.tsx +45 -0
  152. package/src/Form/context/AtlantisFormContext.tsx +21 -0
  153. package/src/Form/context/index.ts +5 -0
  154. package/src/Form/context/types.ts +34 -0
  155. package/src/Form/hooks/useFormViewRefs.ts +23 -0
  156. package/src/Form/hooks/useInternalForm.ts +99 -0
  157. package/src/Form/hooks/useOfflineHandler.ts +36 -0
  158. package/src/Form/hooks/useSaveButtonPosition.ts +52 -0
  159. package/src/Form/hooks/useScreenInformation.ts +25 -0
  160. package/src/Form/hooks/useScrollToError/index.ts +1 -0
  161. package/src/Form/hooks/useScrollToError/useScrollToError.test.tsx +103 -0
  162. package/src/Form/hooks/useScrollToError/useScrollToError.ts +102 -0
  163. package/src/Form/index.ts +13 -0
  164. package/src/Form/messages.ts +33 -0
  165. package/src/Form/types.ts +255 -0
  166. package/src/InputDate/InputDate.test.tsx +295 -0
  167. package/src/InputDate/InputDate.tsx +231 -0
  168. package/src/InputDate/index.ts +1 -0
  169. package/src/InputDate/messages.ts +9 -0
  170. package/src/InputNumber/InputNumber.tsx +1 -1
  171. package/src/Menu/Menu.style.ts +16 -0
  172. package/src/Menu/Menu.test.tsx +201 -0
  173. package/src/Menu/Menu.tsx +116 -0
  174. package/src/Menu/components/MenuOption/MenuOption.style.tsx +11 -0
  175. package/src/Menu/components/MenuOption/MenuOption.tsx +63 -0
  176. package/src/Menu/components/MenuOption/index.ts +1 -0
  177. package/src/Menu/components/Overlay/Overlay.style.ts +13 -0
  178. package/src/Menu/components/Overlay/Overlay.tsx +16 -0
  179. package/src/Menu/components/Overlay/index.ts +1 -0
  180. package/src/Menu/index.ts +6 -0
  181. package/src/Menu/messages.ts +9 -0
  182. package/src/Menu/types.ts +25 -0
  183. package/src/Menu/utils.ts +151 -0
  184. package/src/index.ts +3 -0
@@ -0,0 +1,255 @@
1
+ import { MutableRefObject, RefObject } from "react";
2
+ import {
3
+ ControllerProps,
4
+ DeepPartial,
5
+ FieldPath,
6
+ FieldValues,
7
+ Mode,
8
+ UnpackNestedValue,
9
+ UseFormReturn,
10
+ } from "react-hook-form";
11
+ import { IconNames } from "@jobber/design";
12
+ import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
13
+
14
+ export type FormValues<T> = UnpackNestedValue<T>;
15
+ export type FormErrors = FormNetworkErrors | FormUserErrors;
16
+ export type FormBannerMessage = FormWarningMessage | FormNoticeMessage;
17
+
18
+ export enum FormSubmitErrorType {
19
+ NetworkError = "NetworkError",
20
+ UserError = "UserErrors",
21
+ }
22
+
23
+ export enum FormBannerMessageType {
24
+ WarningMessage = "WarningMessage",
25
+ NoticeMessage = "NoticeMessage",
26
+ }
27
+
28
+ export interface FormBannerErrors {
29
+ networkError?: string | undefined;
30
+ bannerError?: {
31
+ title: string;
32
+ messages?: string[];
33
+ };
34
+ }
35
+
36
+ interface FormNetworkErrors {
37
+ errorType: FormSubmitErrorType.NetworkError;
38
+ networkErrors: string;
39
+ }
40
+
41
+ export interface FormUserErrors {
42
+ errorType: FormSubmitErrorType.UserError;
43
+ userErrors: Record<string, string>;
44
+ }
45
+
46
+ interface FormWarningMessage {
47
+ messageType: FormBannerMessageType.WarningMessage;
48
+ message: string;
49
+ }
50
+
51
+ interface FormNoticeMessage {
52
+ messageType: FormBannerMessageType.NoticeMessage;
53
+ message: string;
54
+ }
55
+
56
+ export type FormRef<T extends FieldValues = FieldValues> =
57
+ | (UseFormReturn<T> & {
58
+ scrollViewRef?: RefObject<KeyboardAwareScrollView>;
59
+ saveButtonHeight?: number;
60
+ messageBannerHeight?: number;
61
+ })
62
+ | undefined;
63
+
64
+ export interface FormProps<T extends FieldValues, SubmitResponseType> {
65
+ /**
66
+ * Content to be passed into the form
67
+ */
68
+ children: React.ReactNode;
69
+
70
+ /**
71
+ * A callback function that is run before invoking onSubmit. Form submission is canceled if the promise resolves to false.
72
+ */
73
+ onBeforeSubmit?: (data: FormValues<T>) => Promise<boolean>;
74
+
75
+ /**
76
+ * A callback function that handles the submission of form data
77
+ */
78
+ onSubmit: (data: FormValues<T>) => Promise<SubmitResponseType>;
79
+
80
+ /**
81
+ * A callback function that handles any error that occurs during "onSubmit"
82
+ */
83
+ onSubmitError: (error: FormErrors) => void;
84
+
85
+ /**
86
+ * A callback function that handles a successful form submission from "onSubmit"
87
+ */
88
+ onSubmitSuccess: (data: SubmitResponseType) => void;
89
+
90
+ /**
91
+ * Network or user errors to be displayed as a banner at the top of the form
92
+ */
93
+ bannerErrors?: FormBannerErrors;
94
+
95
+ /**
96
+ * Status messages to be displayed as a banner at the top of the form
97
+ */
98
+ bannerMessages?: FormBannerMessage[];
99
+
100
+ /**
101
+ * Loading when the initial form data is being fetched
102
+ */
103
+ initialLoading?: boolean;
104
+
105
+ /**
106
+ * The initial values of the form inputs
107
+ * This should be available as soon as initialLoading is set to false
108
+ */
109
+ initialValues?: FormValues<DeepPartial<T>>;
110
+
111
+ /**
112
+ * When the validation should happen.
113
+ * Possible values are "onBlur", "onChange", "onSubmit", "onTouched", and "all".
114
+ * The default value is "onTouched"
115
+ */
116
+ mode?: Mode;
117
+
118
+ /**
119
+ * When the validation after submission should happen.
120
+ * Possible values are "onBlur", "onChange", and "onSubmit".
121
+ * The default value is "onChange"
122
+ */
123
+ reValidateMode?: Exclude<Mode, "onTouched" | "all">;
124
+
125
+ /**
126
+ * ref object to access react hook form methods and state
127
+ */
128
+ formRef?: MutableRefObject<FormRef<T> | undefined>;
129
+
130
+ /**
131
+ * Label to be displayed for the save button
132
+ */
133
+ saveButtonLabel?: string;
134
+
135
+ /**
136
+ * @deprecated use `secondaryAction` instead.
137
+ * Override default save button in the sticky section of the form with another element.
138
+ */
139
+ renderStickySection?: (
140
+ onSubmit: () => void,
141
+ label: string | undefined,
142
+ isSubmitting: boolean,
143
+ ) => JSX.Element;
144
+
145
+ /**
146
+ * Adding a key will save a local copy of the form data that will be used to
147
+ * recover values when the app is backgrounded or has crashed.
148
+ */
149
+ localCacheKey?: string;
150
+
151
+ /**
152
+ * Forms field names that will not be considered for caching.
153
+ * Useful for omitting sensitive data.
154
+ */
155
+ localCacheExclude?: string[];
156
+
157
+ /**
158
+ * A string or array of strings that can be used to identify the pre-filled
159
+ * data on the form. This can be used to support local caching for forms that
160
+ * prefill data without inadvertently applying the cache at the wrong time.
161
+ *
162
+ * For example this can be used to when an object is based on data from
163
+ * another object (Quote being converted into a Job). This will allow
164
+ * the user to retrieve data from the cache when trying to create the
165
+ * same object (same Quote being converted into a Job) following an app crash.
166
+ *
167
+ * There is still only one copy of data for each `localCacheKey`.
168
+ * If a user opens the same form the data will only be loaded if the `localCacheId` matches
169
+ */
170
+ localCacheId?: string | string[];
171
+
172
+ /**
173
+ * Secondary Action for ButtonGroup
174
+ */
175
+ secondaryActions?: SecondaryActionProp[];
176
+
177
+ /**
178
+ * A number that will pull down the save button when the position is sticky.
179
+ * Useful when there's a footer or content below the form that is pulling
180
+ * the button up.
181
+ */
182
+ saveButtonOffset?: number;
183
+
184
+ /**
185
+ * Forces to render the sticky save button instead of the inline.
186
+ * The sticky save button is default for iOS but not for Android due to
187
+ * limitations. Use this prop with caution on Android.
188
+ */
189
+ showStickySaveButton?: boolean;
190
+
191
+ /**
192
+ * Renders a footer below the save button.
193
+ */
194
+ renderFooter?: React.ReactNode;
195
+ }
196
+
197
+ export type InternalFormProps<T extends FieldValues, SubmitResponseType> = Omit<
198
+ FormProps<T, SubmitResponseType>,
199
+ "initialLoading"
200
+ >;
201
+
202
+ export type ValidationRulesByFieldPath<T extends FieldValues> = {
203
+ [p in FieldPath<T>]: ControllerProps<T, p>["rules"];
204
+ };
205
+
206
+ export interface FormSaveButtonProps {
207
+ /**
208
+ * Press handler
209
+ */
210
+ primaryAction: () => Promise<void> | void;
211
+
212
+ /**
213
+ * Primary Button is loading
214
+ */
215
+ loading: boolean;
216
+
217
+ /**
218
+ * Label for the save button
219
+ */
220
+ label?: string;
221
+ /**
222
+ * Props and information regarding the secondary Action button(s)
223
+ */
224
+ secondaryActions?: SecondaryActionProp[];
225
+
226
+ /**
227
+ * Set whether secondary Button is loading
228
+ */
229
+ setSecondaryActionLoading?: (bool: boolean) => void;
230
+
231
+ /**
232
+ * Callback that is called when the secondary actions bottom sheet is opened.
233
+ */
234
+ onOpenBottomSheet?: () => void;
235
+
236
+ /**
237
+ * Callback that is called when the secondary actions bottom sheet is closed.
238
+ */
239
+ onCloseBottomSheet?: () => void;
240
+ }
241
+
242
+ interface SecondaryActionOnPress {
243
+ onBeforeSubmit?: () => Promise<boolean>;
244
+ onSubmit: (formSubmit: FormSaveButtonProps["primaryAction"]) => Promise<void>;
245
+ onSubmitSuccess?: () => void;
246
+ onSubmitError?: (error: FormErrors) => void;
247
+ resetFormOnSubmit?: boolean;
248
+ }
249
+
250
+ export interface SecondaryActionProp {
251
+ label: string;
252
+ icon?: IconNames | undefined;
253
+ handleAction: SecondaryActionOnPress;
254
+ destructive?: boolean;
255
+ }
@@ -0,0 +1,295 @@
1
+ import React from "react";
2
+ import { fireEvent, render, waitFor } from "@testing-library/react-native";
3
+ import { Host } from "react-native-portalize";
4
+ import { FormProvider, useForm } from "react-hook-form";
5
+ import { InputDate } from "./InputDate";
6
+ import { Button } from "../Button";
7
+ import * as atlantisContext from "../AtlantisContext/AtlantisContext";
8
+
9
+ describe("InputDate", () => {
10
+ describe("Visuals", () => {
11
+ const placeholder = "Start time";
12
+ const expectedDate = "May 30, 2022";
13
+ const value = new Date(2022, 4, 30);
14
+ const handleChange = jest.fn();
15
+
16
+ const setup = () =>
17
+ render(
18
+ <InputDate
19
+ placeholder={placeholder}
20
+ value={value}
21
+ onChange={handleChange}
22
+ />,
23
+ );
24
+
25
+ it("should show a calendar prefix icon", () => {
26
+ const screen = setup();
27
+
28
+ const calendarIcon = screen.getByTestId("calendar");
29
+ expect(calendarIcon).toBeDefined();
30
+ expect(calendarIcon.type).toBe("RNSVGSvgView");
31
+ });
32
+
33
+ it("should show a formatted date", () => {
34
+ const screen = setup();
35
+ expect(
36
+ screen.getByText(expectedDate, { includeHiddenElements: true }),
37
+ ).toBeDefined();
38
+ });
39
+
40
+ it("should be clearable when there's a value", () => {
41
+ const screen = setup();
42
+ const clearAction = screen.getByLabelText("Clear input");
43
+ expect(clearAction).toBeDefined();
44
+
45
+ fireEvent.press(clearAction);
46
+ expect(handleChange).toHaveBeenCalledWith(undefined);
47
+ });
48
+ });
49
+
50
+ describe("String value", () => {
51
+ it("should show a formatted date", () => {
52
+ const expectedDate = "May 31, 2022";
53
+ const value = new Date(2022, 4, 31).toISOString();
54
+ const screen = render(<InputDate value={value} onChange={jest.fn()} />);
55
+
56
+ expect(
57
+ screen.getByText(expectedDate, { includeHiddenElements: true }),
58
+ ).toBeDefined();
59
+ });
60
+ });
61
+
62
+ describe("With emptyValueLabel", () => {
63
+ it("should show the emptyValueLabel when there's no value", () => {
64
+ const label = "Unscheduled";
65
+ const screen = render(<InputDate name="test" emptyValueLabel={label} />);
66
+
67
+ expect(
68
+ screen.getByText(label, { includeHiddenElements: true }),
69
+ ).toBeDefined();
70
+ expect(screen.queryByLabelText("Clear input")).toBeNull();
71
+ });
72
+
73
+ it("should not show the emptyValueLabel when there's a value", () => {
74
+ const label = "Unscheduled";
75
+ const screen = render(
76
+ <InputDate
77
+ emptyValueLabel={label}
78
+ value={new Date()}
79
+ onChange={jest.fn()}
80
+ />,
81
+ );
82
+
83
+ expect(screen.queryByText(label)).toBeNull();
84
+ expect(screen.getByLabelText("Clear input")).toBeDefined();
85
+ });
86
+ });
87
+
88
+ describe("with a defaultValue", () => {
89
+ it("renders the supplied value", () => {
90
+ const { getByText } = render(
91
+ <InputDate name="test" defaultValue={new Date("2022-01-17")} />,
92
+ );
93
+
94
+ expect(
95
+ getByText("Jan 17, 2022", { includeHiddenElements: true }),
96
+ ).toBeDefined();
97
+ });
98
+ });
99
+
100
+ describe("Date picker", () => {
101
+ const placeholder = "Tap me";
102
+ const handleChange = jest.fn();
103
+
104
+ function renderDatePicker() {
105
+ const screen = render(
106
+ <InputDate
107
+ placeholder={placeholder}
108
+ value={undefined}
109
+ onChange={handleChange}
110
+ />,
111
+ );
112
+
113
+ fireEvent.press(screen.getByLabelText(placeholder));
114
+ expect(screen.getByTestId("inputDate-datePicker")).toBeDefined();
115
+
116
+ return screen;
117
+ }
118
+
119
+ it("should not show a date picker", () => {
120
+ const screen = render(<InputDate name="test" />);
121
+ expect(screen.queryByTestId("inputDate-datePicker")).toBeNull();
122
+ });
123
+
124
+ it("should fire the onChange with the current value after canceling a date selection", () => {
125
+ const screen = renderDatePicker();
126
+
127
+ fireEvent.press(screen.getByLabelText("Cancel"));
128
+ expect(handleChange).toHaveBeenCalledWith(undefined);
129
+ });
130
+
131
+ it("should fire the onChange after confirming a date selection", () => {
132
+ const screen = renderDatePicker();
133
+
134
+ fireEvent.press(screen.getByLabelText("Confirm"));
135
+ expect(handleChange).toHaveBeenCalledWith(expect.any(Date));
136
+ });
137
+ });
138
+ const mockOnSubmit = jest.fn();
139
+ const saveButtonText = "Submit";
140
+
141
+ const requiredError = "This is required";
142
+ function SimpleFormWithProvider({ children, defaultValues }) {
143
+ const formMethods = useForm({
144
+ reValidateMode: "onChange",
145
+ defaultValues,
146
+ mode: "onTouched",
147
+ });
148
+
149
+ return (
150
+ <FormProvider {...formMethods}>
151
+ {children}
152
+ <Button
153
+ onPress={formMethods.handleSubmit(values => mockOnSubmit(values))}
154
+ label={saveButtonText}
155
+ accessibilityLabel={saveButtonText}
156
+ />
157
+ </FormProvider>
158
+ );
159
+ }
160
+ describe("Form controlled", () => {
161
+ const pickerName = "datePicker";
162
+ const expectedDate = "May 29, 2022";
163
+ const value = new Date(2022, 4, 29);
164
+ const handleChange = jest.fn();
165
+
166
+ const setup = () =>
167
+ render(
168
+ <SimpleFormWithProvider defaultValues={{ [pickerName]: value }}>
169
+ <Host>
170
+ <InputDate
171
+ name={pickerName}
172
+ onChange={handleChange}
173
+ validations={{ required: requiredError }}
174
+ />
175
+ </Host>
176
+ </SimpleFormWithProvider>,
177
+ );
178
+
179
+ it("should show the initial value", async () => {
180
+ const screen = setup();
181
+ expect(
182
+ screen.getByText(expectedDate, {
183
+ includeHiddenElements: true,
184
+ }),
185
+ ).toBeDefined();
186
+ });
187
+
188
+ it("should update the value", async () => {
189
+ const screen = await setup();
190
+
191
+ fireEvent.press(
192
+ screen.getByText(expectedDate, { includeHiddenElements: true }),
193
+ );
194
+
195
+ const expectedNewDate = "Jun 17, 2022";
196
+ const newSelectedDate = new Date(2022, 5, 17);
197
+ fireEvent(
198
+ screen.getByTestId("inputDate-datePicker"),
199
+ "onConfirm",
200
+ newSelectedDate,
201
+ );
202
+
203
+ expect(
204
+ screen.getByText(expectedNewDate, { includeHiddenElements: true }),
205
+ ).toBeDefined();
206
+ expect(handleChange).toHaveBeenCalledWith(newSelectedDate);
207
+ });
208
+
209
+ it("should show the client side validation error", async () => {
210
+ const screen = setup();
211
+
212
+ const clearAction = screen.getByLabelText("Clear input");
213
+ expect(clearAction).toBeDefined();
214
+
215
+ fireEvent.press(clearAction);
216
+
217
+ await waitFor(() => {
218
+ expect(
219
+ screen.getAllByText(requiredError, { includeHiddenElements: true }),
220
+ ).toHaveLength(1);
221
+ });
222
+ });
223
+
224
+ it("should clear the input with null value when it is in a form", async () => {
225
+ const screen = setup();
226
+ const clearAction = screen.getByLabelText("Clear input");
227
+ expect(clearAction).toBeDefined();
228
+
229
+ fireEvent.press(clearAction);
230
+ expect(handleChange).toHaveBeenCalledWith(null);
231
+ });
232
+ });
233
+
234
+ describe("dateFormat pattern", () => {
235
+ afterEach(() => {
236
+ jest.spyOn(atlantisContext, "useAtlantisContext").mockRestore();
237
+ });
238
+
239
+ it("should display MM/DD/YYYY when dateFormat is 'P'", () => {
240
+ jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
241
+ ...atlantisContext.defaultValues,
242
+ dateFormat: "P",
243
+ });
244
+ const expectedDate = "05/24/2023";
245
+ const value = new Date(2023, 4, 24).toISOString();
246
+ const screen = render(<InputDate value={value} onChange={jest.fn()} />);
247
+
248
+ expect(
249
+ screen.getByText(expectedDate, { includeHiddenElements: true }),
250
+ ).toBeDefined();
251
+ });
252
+
253
+ it("should display mmmm d, yyyy when dateFormat is 'PP'", () => {
254
+ jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
255
+ ...atlantisContext.defaultValues,
256
+ dateFormat: "PP",
257
+ });
258
+ const expectedDate = "Feb 20, 2023";
259
+ const value = new Date(2023, 1, 20).toISOString();
260
+ const screen = render(<InputDate value={value} onChange={jest.fn()} />);
261
+
262
+ expect(
263
+ screen.getByText(expectedDate, { includeHiddenElements: true }),
264
+ ).toBeDefined();
265
+ });
266
+
267
+ it("should display mmmmm d, yyyy when dateFormat is 'PPP'", () => {
268
+ jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
269
+ ...atlantisContext.defaultValues,
270
+ dateFormat: "PPP",
271
+ });
272
+ const expectedDate = "July 7th, 2023";
273
+ const value = new Date(2023, 6, 7).toISOString();
274
+ const screen = render(<InputDate value={value} onChange={jest.fn()} />);
275
+
276
+ expect(
277
+ screen.getByText(expectedDate, { includeHiddenElements: true }),
278
+ ).toBeDefined();
279
+ });
280
+
281
+ it("should display dddd, mmmmm d, yyyy when dateFormat is 'PPPP'", () => {
282
+ jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
283
+ ...atlantisContext.defaultValues,
284
+ dateFormat: "PPPP",
285
+ });
286
+ const expectedDate = "Thursday, June 22nd, 2023";
287
+ const value = new Date(2023, 5, 22).toISOString();
288
+ const screen = render(<InputDate value={value} onChange={jest.fn()} />);
289
+
290
+ expect(
291
+ screen.getByText(expectedDate, { includeHiddenElements: true }),
292
+ ).toBeDefined();
293
+ });
294
+ });
295
+ });