envoc-form 4.0.1-9 → 4.2.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 (239) hide show
  1. package/README.md +9443 -12
  2. package/es/AddressInput/AddressInput.d.ts +15 -5
  3. package/es/AddressInput/AddressInput.js +10 -0
  4. package/es/ConfirmBaseForm/ConfirmBaseForm.d.ts +14 -0
  5. package/es/ConfirmBaseForm/ConfirmBaseForm.js +6 -0
  6. package/es/ConfirmDeleteForm/ConfirmDeleteForm.d.ts +13 -0
  7. package/es/ConfirmDeleteForm/ConfirmDeleteForm.js +5 -0
  8. package/es/DatePicker/DatePickerGroup.d.ts +7 -2
  9. package/es/DatePicker/DatePickerGroup.js +18 -7
  10. package/es/DatePicker/StringDateOnlyPickerGroup.d.ts +6 -1
  11. package/es/DatePicker/StringDateOnlyPickerGroup.js +5 -0
  12. package/es/DatePicker/StringDatePickerGroup.d.ts +6 -1
  13. package/es/DatePicker/StringDatePickerGroup.js +5 -0
  14. package/es/DateTimePicker/DateTimePickerGroup.d.ts +13 -0
  15. package/es/DateTimePicker/DateTimePickerGroup.js +87 -0
  16. package/es/DateTimePicker/DateTimePickerHelper.d.ts +3 -0
  17. package/es/DateTimePicker/DateTimePickerHelper.js +1 -0
  18. package/es/DateTimePicker/StringDateTimePickerGroup.d.ts +10 -0
  19. package/es/DateTimePicker/StringDateTimePickerGroup.js +53 -0
  20. package/es/Field/Field.d.ts +9 -3
  21. package/es/Field/Field.js +9 -4
  22. package/es/Field/FieldErrorScrollTarget.d.ts +1 -0
  23. package/es/Field/FieldErrorScrollTarget.js +1 -0
  24. package/es/Field/StandAloneInput.d.ts +4 -0
  25. package/es/Field/StandAloneInput.js +1 -0
  26. package/es/Field/useStandardField.d.ts +6 -0
  27. package/es/Field/useStandardField.js +8 -9
  28. package/es/FieldArray/FieldArray.d.ts +11 -2
  29. package/es/FieldArray/FieldArray.js +7 -2
  30. package/es/File/FileGroup.d.ts +3 -1
  31. package/es/File/FileGroup.js +4 -3
  32. package/es/File/FileList.d.ts +2 -2
  33. package/es/Form/FocusError.d.ts +2 -0
  34. package/es/Form/FocusError.js +1 -0
  35. package/es/Form/Form.d.ts +6 -0
  36. package/es/Form/Form.js +1 -0
  37. package/es/Form/FormBasedPreventNavigation.d.ts +3 -1
  38. package/es/Form/FormBasedPreventNavigation.js +12 -45
  39. package/es/Form/LegacyFormBasedPreventNavigation.d.ts +17 -0
  40. package/es/Form/LegacyFormBasedPreventNavigation.js +69 -0
  41. package/es/Form/NewFormBasedPreventNavigation.d.ts +14 -0
  42. package/es/Form/NewFormBasedPreventNavigation.js +39 -0
  43. package/es/Form/ServerErrorContext.d.ts +1 -0
  44. package/es/Form/ServerErrorContext.js +1 -0
  45. package/es/FormActions.d.ts +6 -0
  46. package/es/FormActions.js +1 -0
  47. package/es/FormDefaults.d.ts +1 -0
  48. package/es/FormDefaults.js +1 -0
  49. package/es/Group.d.ts +7 -4
  50. package/es/Group.js +4 -3
  51. package/es/Input/IconInputGroup.d.ts +4 -1
  52. package/es/Input/IconInputGroup.js +3 -1
  53. package/es/Input/InputGroup.d.ts +4 -4
  54. package/es/Input/InputGroup.js +4 -4
  55. package/es/Input/MoneyInputGroup.d.ts +3 -1
  56. package/es/Input/MoneyInputGroup.js +1 -0
  57. package/es/Input/NumberInputGroup.d.ts +3 -1
  58. package/es/Input/NumberInputGroup.js +1 -0
  59. package/es/Input/PhoneNumberInputGroup.d.ts +10 -0
  60. package/es/Input/PhoneNumberInputGroup.js +47 -0
  61. package/es/Input/StringInputGroup.d.ts +2 -1
  62. package/es/Input/StringInputGroup.js +3 -1
  63. package/es/Normalization/normalizers.d.ts +4 -2
  64. package/es/Normalization/normalizers.js +2 -0
  65. package/es/Select/BooleanSelectGroup.d.ts +2 -1
  66. package/es/Select/BooleanSelectGroup.js +1 -0
  67. package/es/Select/NumberSelectGroup.d.ts +4 -2
  68. package/es/Select/NumberSelectGroup.js +2 -0
  69. package/es/Select/SelectGroup.d.ts +8 -2
  70. package/es/Select/SelectGroup.js +5 -4
  71. package/es/Select/StringSelectGroup.d.ts +2 -0
  72. package/es/Select/StringSelectGroup.js +2 -0
  73. package/es/StandardFormActions.d.ts +5 -0
  74. package/es/StandardFormActions.js +1 -0
  75. package/es/SubmitFormButton.d.ts +4 -1
  76. package/es/SubmitFormButton.js +1 -0
  77. package/es/TextArea/TextAreaGroup.d.ts +2 -1
  78. package/es/TextArea/TextAreaGroup.js +5 -4
  79. package/es/Validation/validators.d.ts +11 -8
  80. package/es/Validation/validators.js +6 -2
  81. package/es/hooks/index.d.ts +2 -0
  82. package/es/hooks/index.js +2 -0
  83. package/es/hooks/useFormValue.d.ts +2 -0
  84. package/es/hooks/useFormValue.js +7 -0
  85. package/es/index.d.ts +10 -4
  86. package/es/index.js +4 -0
  87. package/es/setupTests.d.ts +1 -0
  88. package/es/setupTests.js +1 -0
  89. package/lib/AddressInput/AddressInput.d.ts +15 -5
  90. package/lib/AddressInput/AddressInput.js +10 -0
  91. package/lib/ConfirmBaseForm/ConfirmBaseForm.d.ts +14 -0
  92. package/lib/ConfirmBaseForm/ConfirmBaseForm.js +6 -0
  93. package/lib/ConfirmDeleteForm/ConfirmDeleteForm.d.ts +13 -0
  94. package/lib/ConfirmDeleteForm/ConfirmDeleteForm.js +5 -0
  95. package/lib/DatePicker/DatePickerGroup.d.ts +7 -2
  96. package/lib/DatePicker/DatePickerGroup.js +18 -7
  97. package/lib/DatePicker/StringDateOnlyPickerGroup.d.ts +6 -1
  98. package/lib/DatePicker/StringDateOnlyPickerGroup.js +5 -0
  99. package/lib/DatePicker/StringDatePickerGroup.d.ts +6 -1
  100. package/lib/DatePicker/StringDatePickerGroup.js +5 -0
  101. package/lib/DateTimePicker/DateTimePickerGroup.d.ts +13 -0
  102. package/lib/DateTimePicker/DateTimePickerGroup.js +93 -0
  103. package/lib/DateTimePicker/DateTimePickerHelper.d.ts +3 -0
  104. package/lib/DateTimePicker/DateTimePickerHelper.js +2 -0
  105. package/lib/DateTimePicker/StringDateTimePickerGroup.d.ts +10 -0
  106. package/lib/DateTimePicker/StringDateTimePickerGroup.js +59 -0
  107. package/lib/Field/Field.d.ts +9 -3
  108. package/lib/Field/Field.js +9 -4
  109. package/lib/Field/FieldErrorScrollTarget.d.ts +1 -0
  110. package/lib/Field/FieldErrorScrollTarget.js +1 -0
  111. package/lib/Field/StandAloneInput.d.ts +4 -0
  112. package/lib/Field/StandAloneInput.js +1 -0
  113. package/lib/Field/useStandardField.d.ts +6 -0
  114. package/lib/Field/useStandardField.js +8 -9
  115. package/lib/FieldArray/FieldArray.d.ts +11 -2
  116. package/lib/FieldArray/FieldArray.js +7 -2
  117. package/lib/File/FileGroup.d.ts +3 -1
  118. package/lib/File/FileGroup.js +4 -3
  119. package/lib/File/FileList.d.ts +2 -2
  120. package/lib/Form/FocusError.d.ts +2 -0
  121. package/lib/Form/FocusError.js +1 -0
  122. package/lib/Form/Form.d.ts +6 -0
  123. package/lib/Form/Form.js +1 -0
  124. package/lib/Form/FormBasedPreventNavigation.d.ts +3 -1
  125. package/lib/Form/FormBasedPreventNavigation.js +13 -43
  126. package/lib/Form/LegacyFormBasedPreventNavigation.d.ts +17 -0
  127. package/lib/Form/LegacyFormBasedPreventNavigation.js +72 -0
  128. package/lib/Form/NewFormBasedPreventNavigation.d.ts +14 -0
  129. package/lib/Form/NewFormBasedPreventNavigation.js +42 -0
  130. package/lib/Form/ServerErrorContext.d.ts +1 -0
  131. package/lib/Form/ServerErrorContext.js +1 -0
  132. package/lib/FormActions.d.ts +6 -0
  133. package/lib/FormActions.js +1 -0
  134. package/lib/FormDefaults.d.ts +1 -0
  135. package/lib/FormDefaults.js +1 -0
  136. package/lib/Group.d.ts +7 -4
  137. package/lib/Group.js +4 -3
  138. package/lib/Input/IconInputGroup.d.ts +4 -1
  139. package/lib/Input/IconInputGroup.js +3 -1
  140. package/lib/Input/InputGroup.d.ts +4 -4
  141. package/lib/Input/InputGroup.js +4 -4
  142. package/lib/Input/MoneyInputGroup.d.ts +3 -1
  143. package/lib/Input/MoneyInputGroup.js +1 -0
  144. package/lib/Input/NumberInputGroup.d.ts +3 -1
  145. package/lib/Input/NumberInputGroup.js +1 -0
  146. package/lib/Input/PhoneNumberInputGroup.d.ts +10 -0
  147. package/lib/Input/PhoneNumberInputGroup.js +52 -0
  148. package/lib/Input/StringInputGroup.d.ts +2 -1
  149. package/lib/Input/StringInputGroup.js +3 -1
  150. package/lib/Normalization/normalizers.d.ts +4 -2
  151. package/lib/Normalization/normalizers.js +2 -0
  152. package/lib/Select/BooleanSelectGroup.d.ts +2 -1
  153. package/lib/Select/BooleanSelectGroup.js +1 -0
  154. package/lib/Select/NumberSelectGroup.d.ts +4 -2
  155. package/lib/Select/NumberSelectGroup.js +2 -0
  156. package/lib/Select/SelectGroup.d.ts +8 -2
  157. package/lib/Select/SelectGroup.js +5 -4
  158. package/lib/Select/StringSelectGroup.d.ts +2 -0
  159. package/lib/Select/StringSelectGroup.js +2 -0
  160. package/lib/StandardFormActions.d.ts +5 -0
  161. package/lib/StandardFormActions.js +1 -0
  162. package/lib/SubmitFormButton.d.ts +4 -1
  163. package/lib/SubmitFormButton.js +1 -0
  164. package/lib/TextArea/TextAreaGroup.d.ts +2 -1
  165. package/lib/TextArea/TextAreaGroup.js +5 -4
  166. package/lib/Validation/validators.d.ts +11 -8
  167. package/lib/Validation/validators.js +6 -2
  168. package/lib/hooks/index.d.ts +2 -0
  169. package/lib/{__Tests__ → hooks}/index.js +3 -3
  170. package/lib/hooks/useFormValue.d.ts +2 -0
  171. package/lib/hooks/useFormValue.js +10 -0
  172. package/lib/index.d.ts +10 -4
  173. package/lib/index.js +8 -1
  174. package/lib/setupTests.d.ts +1 -0
  175. package/lib/setupTests.js +3 -0
  176. package/package.json +12 -8
  177. package/src/AddressInput/AddressInput.tsx +15 -5
  178. package/src/AddressInput/__snapshots__/AddressInput.test.tsx.snap +8 -4
  179. package/src/ConfirmBaseForm/ConfirmBaseForm.tsx +14 -0
  180. package/src/ConfirmDeleteForm/ConfirmDeleteForm.tsx +13 -0
  181. package/src/DatePicker/DatePicker.test.tsx +1 -1
  182. package/src/DatePicker/DatePickerGroup.tsx +23 -7
  183. package/src/DatePicker/StringDateOnlyPickerGroup.tsx +7 -2
  184. package/src/DatePicker/StringDatePickerGroup.tsx +7 -1
  185. package/src/DateTimePicker/DateTimePicker.test.tsx +243 -0
  186. package/src/DateTimePicker/DateTimePickerGroup.tsx +116 -0
  187. package/src/DateTimePicker/DateTimePickerHelper.ts +4 -0
  188. package/src/DateTimePicker/StringDateTimePickerGroup.tsx +61 -0
  189. package/src/DateTimePicker/__snapshots__/DateTimePicker.test.tsx.snap +216 -0
  190. package/src/Field/Field.tsx +20 -7
  191. package/src/Field/FieldErrorScrollTarget.tsx +1 -0
  192. package/src/Field/StandAloneInput.tsx +4 -0
  193. package/src/Field/useStandardField.ts +13 -9
  194. package/src/FieldArray/FieldArray.tsx +14 -5
  195. package/src/File/FileGroup.tsx +9 -3
  196. package/src/File/FileList.tsx +2 -2
  197. package/src/Form/FocusError.tsx +3 -0
  198. package/src/Form/Form.tsx +6 -0
  199. package/src/Form/FormBasedPreventNavigation.tsx +28 -46
  200. package/src/Form/LegacyFormBasedPreventNavigation.tsx +77 -0
  201. package/src/Form/NewFormBasedPreventNavigation.tsx +59 -0
  202. package/src/Form/ServerErrorContext.ts +1 -0
  203. package/src/FormActions.tsx +7 -0
  204. package/src/FormDefaults.ts +1 -0
  205. package/src/Group.tsx +12 -4
  206. package/src/Input/IconInputGroup.tsx +5 -2
  207. package/src/Input/InputGroup.tsx +13 -5
  208. package/src/Input/MoneyInputGroup.tsx +3 -1
  209. package/src/Input/NumberInputGroup.tsx +3 -1
  210. package/src/Input/PhoneNumberInputGroup.tsx +49 -0
  211. package/src/Input/StringInputGroup.tsx +3 -2
  212. package/src/Input/__Tests__/PhoneNumberInputGroup.test.tsx +37 -0
  213. package/src/Input/__Tests__/__snapshots__/IconInputGroup.test.tsx.snap +1 -0
  214. package/src/Input/__Tests__/__snapshots__/MoneyInputGroup.test.tsx.snap +1 -0
  215. package/src/Input/__Tests__/__snapshots__/NumberInputGroup.test.tsx.snap +1 -0
  216. package/src/Input/__Tests__/__snapshots__/PhoneNumberInputGroup.test.tsx.snap +33 -0
  217. package/src/Input/__Tests__/__snapshots__/StringInputGroup.test.tsx.snap +1 -0
  218. package/src/Normalization/normalizers.ts +4 -2
  219. package/src/Select/BooleanSelectGroup.tsx +2 -1
  220. package/src/Select/NumberSelectGroup.tsx +4 -2
  221. package/src/Select/SelectGroup.tsx +13 -2
  222. package/src/Select/StringSelectGroup.tsx +2 -0
  223. package/src/StandardFormActions.tsx +5 -0
  224. package/src/SubmitFormButton.tsx +5 -1
  225. package/src/TextArea/TextAreaGroup.tsx +6 -4
  226. package/src/Validation/validators.ts +19 -14
  227. package/src/__Tests__/FormTestBase.tsx +10 -8
  228. package/src/__Tests__/RealisticForm.test.tsx +82 -0
  229. package/src/hooks/index.ts +3 -0
  230. package/src/hooks/useFormValue.ts +16 -0
  231. package/src/index.ts +14 -4
  232. package/src/setupTests.ts +1 -0
  233. package/es/__Tests__/FormTestBase.d.ts +0 -27
  234. package/es/__Tests__/FormTestBase.js +0 -83
  235. package/es/__Tests__/index.d.ts +0 -2
  236. package/es/__Tests__/index.js +0 -2
  237. package/lib/__Tests__/FormTestBase.d.ts +0 -27
  238. package/lib/__Tests__/FormTestBase.js +0 -86
  239. package/lib/__Tests__/index.d.ts +0 -2
@@ -1,15 +1,15 @@
1
1
  import { useEffect, useState } from 'react';
2
- import DatePicker, { DatePickerProps } from 'react-date-picker/dist/entry.nostyle';
2
+ import DatePicker, {
3
+ DatePickerProps,
4
+ } from 'react-date-picker/dist/entry.nostyle';
3
5
  import classnames from 'classnames';
4
6
  import parseISO from 'date-fns/parseISO';
5
7
  import { InjectedFieldProps } from '../Field/InjectedFieldProps';
6
8
  import { FormDefaults } from '../FormDefaults';
7
9
  import Group, { GroupProps } from '../Group';
8
10
 
9
- // Docs for react-date-picker https://www.npmjs.com/package/react-date-picker
10
-
11
11
  export interface DatePickerGroupProps<T>
12
- extends InjectedFieldProps<T | undefined>,
12
+ extends InjectedFieldProps<T | undefined | null>,
13
13
  Omit<
14
14
  DatePickerProps,
15
15
  keyof InjectedFieldProps<T> | 'name' | 'value' | 'className'
@@ -18,23 +18,35 @@ export interface DatePickerGroupProps<T>
18
18
  convert: (arg: Date) => T;
19
19
  }
20
20
 
21
+ /**
22
+ * Field for inputting dates. Uses `<Group/>` and `<DatePicker/>`.
23
+ *
24
+ * Uses [react-date-picker](https://www.npmjs.com/package/react-date-picker)
25
+ */
21
26
  export default function DatePickerGroup<T>({
22
27
  input,
23
28
  meta,
24
29
  label,
25
30
  helpText,
26
31
  className,
32
+ required,
27
33
  disabled,
28
34
  convert,
35
+ maxDate = new Date(9999, 11, 31),
36
+ minDate = new Date(1000, 0, 1),
29
37
  ...rest
30
38
  }: DatePickerGroupProps<T>) {
31
39
  const [displayDate, setDisplayDate] = useState<Date | null>(null);
32
40
 
33
41
  useEffect(() => {
34
- if (input.value != null) {
35
- setDisplayDate(new Date(`${input.value}T00:00:00.000`));
36
- } else {
42
+ if (!input.value) {
37
43
  setDisplayDate(null);
44
+ } else if (typeof input.value === 'string') {
45
+ if (input.value.indexOf('T') === -1) {
46
+ setDisplayDate(new Date(`${input.value}T00:00:00.000`));
47
+ } else {
48
+ setDisplayDate(new Date(input.value));
49
+ }
38
50
  }
39
51
  }, [setDisplayDate, input.value]);
40
52
 
@@ -48,6 +60,7 @@ export default function DatePickerGroup<T>({
48
60
  className,
49
61
  FormDefaults.cssClassPrefix + 'date-picker'
50
62
  )}
63
+ required={required}
51
64
  disabled={disabled}>
52
65
  <DatePicker
53
66
  {...rest}
@@ -57,6 +70,8 @@ export default function DatePickerGroup<T>({
57
70
  )}
58
71
  value={displayDate}
59
72
  onChange={handleChange}
73
+ maxDate={maxDate}
74
+ minDate={minDate}
60
75
  />
61
76
  </Group>
62
77
  );
@@ -95,6 +110,7 @@ export default function DatePickerGroup<T>({
95
110
  export function convertToTimeZoneInsensitiveISOString(date: Date): string {
96
111
  const year = new Intl.DateTimeFormat('en', { year: 'numeric' })
97
112
  .format(date)
113
+ .substring(0, 4)
98
114
  .padStart(4, '0');
99
115
  const month = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(
100
116
  date
@@ -1,9 +1,14 @@
1
- import { DatePickerHelper } from './DatePickerHelper';
2
1
  import DatePickerGroup from './DatePickerGroup';
2
+ import { DatePickerHelper } from './DatePickerHelper';
3
3
 
4
4
  export interface StringDateOnlyPickerGroupProps
5
- extends DatePickerHelper<string | undefined> {}
5
+ extends DatePickerHelper<string | undefined | null> {}
6
6
 
7
+ /**
8
+ * Date picker input that consumes and outputs as a date only string in ISO format `YYYY-MM-DD`.
9
+ *
10
+ * Default display to the user is in `MM/DD/YYYY` format.
11
+ */
7
12
  export default function StringDatePickerGroup(
8
13
  props: StringDateOnlyPickerGroupProps
9
14
  ) {
@@ -2,7 +2,13 @@ import DatePickerGroup from './DatePickerGroup';
2
2
  import { DatePickerHelper } from './DatePickerHelper';
3
3
 
4
4
  export interface StringDatePickerGroupProps
5
- extends DatePickerHelper<string | undefined> {}
5
+ extends DatePickerHelper<string | undefined | null> {}
6
+
7
+ /**
8
+ * Date picker input that consumes and outputs as a date only string in ISO format `YYYY-MM-DDTHH:mm:ss.sssZ` or `±YYYYYY-MM-DDTHH:mm:ss.sssZ`
9
+ *
10
+ * If you need `YYYY-MM-DD` format use `<StringDateOnlyPickerGroup/>`.
11
+ */
6
12
  export default function StringDatePickerGroup(
7
13
  props: StringDatePickerGroupProps
8
14
  ) {
@@ -0,0 +1,243 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import StringDateTimePickerGroup from './StringDateTimePickerGroup';
4
+ import FormTestBase, { PersonDto } from '../__Tests__/FormTestBase';
5
+
6
+ //hack so the datetimepicker internals don't complain about this not existing in the context of jest
7
+ HTMLCanvasElement.prototype.getContext = () => null;
8
+
9
+ describe('StringDateTimePickerGroup', () => {
10
+ it('renders without crashing', () => {
11
+ render(
12
+ <FormTestBase>
13
+ {({ Field }) => (
14
+ <Field
15
+ name="createdDateTime"
16
+ Component={StringDateTimePickerGroup}
17
+ label="Created Date Time"
18
+ monthPlaceholder="mm"
19
+ dayPlaceholder="dd"
20
+ yearPlaceholder="yyyy"
21
+ disableClock
22
+ maxDate={new Date('9/23/2023')}
23
+ minDate={new Date('6/22/2022')}
24
+ />
25
+ )}
26
+ </FormTestBase>
27
+ );
28
+ });
29
+
30
+ it('has matching snapshot', () => {
31
+ const renderResult = render(
32
+ <FormTestBase>
33
+ {({ Field }) => (
34
+ <Field
35
+ name="createdDateTime"
36
+ Component={StringDateTimePickerGroup}
37
+ label="Created Date Time"
38
+ monthPlaceholder="mm"
39
+ dayPlaceholder="dd"
40
+ yearPlaceholder="yyyy"
41
+ disableClock
42
+ maxDate={new Date('9/23/2023')}
43
+ minDate={new Date('6/22/2022')}
44
+ />
45
+ )}
46
+ </FormTestBase>
47
+ );
48
+ expect(renderResult.asFragment()).toMatchSnapshot();
49
+ });
50
+
51
+ it('does not render with initial date-only string and throws exception', () => {
52
+ const dateOnlyString = '2024-09-04';
53
+ const personDtoWithDateOnlyString: PersonDto = {
54
+ createdDateTime: dateOnlyString,
55
+ };
56
+
57
+ try {
58
+ render(
59
+ <FormTestBase initialValues={personDtoWithDateOnlyString}>
60
+ {({ Field }) => (
61
+ <Field
62
+ name="createdDateTime"
63
+ Component={StringDateTimePickerGroup}
64
+ label="Created Date Time"
65
+ monthPlaceholder="mm"
66
+ dayPlaceholder="dd"
67
+ yearPlaceholder="yyyy"
68
+ disableClock
69
+ />
70
+ )}
71
+ </FormTestBase>
72
+ );
73
+
74
+ fail('Expected an exception to be thrown for date-only string');
75
+ } catch (error: any) {
76
+ // Assert that the error message matches the expected message
77
+ expect(error.message).toEqual(
78
+ `Invalid "date time" value of ${dateOnlyString} provided to DateTimePickerGroup component. Please provide ISO 8601 string that includes the time zone designator. Sample strings: 2022-09-24T:08:54:12+00:00, 2022-09-24T:08:54:12Z, 2022-09-24T:08:54:12.123-05:00`
79
+ );
80
+ }
81
+ });
82
+
83
+ it('renders with ISO UTC date time string taking browser timezone into account', () => {
84
+ const isoUTCDateTimeString = '2024-04-09T21:00:24.670Z';
85
+ const personDtoWithDateTimeString: PersonDto = {
86
+ createdDateTime: isoUTCDateTimeString,
87
+ };
88
+
89
+ render(
90
+ <FormTestBase initialValues={personDtoWithDateTimeString}>
91
+ {({ Field }) => (
92
+ <Field
93
+ name="createdDateTime"
94
+ Component={StringDateTimePickerGroup}
95
+ label="Created Date Time"
96
+ monthPlaceholder="mm"
97
+ dayPlaceholder="dd"
98
+ yearPlaceholder="yyyy"
99
+ disableClock
100
+ />
101
+ )}
102
+ </FormTestBase>
103
+ );
104
+
105
+ const dateTimePickerInputComponent = document.querySelector(
106
+ 'input[name="datetime"][type="datetime-local"]'
107
+ );
108
+ const utcDate = new Date(isoUTCDateTimeString);
109
+ const localDateTimeString =
110
+ convertToDateTimePickerComponentValueString(utcDate);
111
+
112
+ // Assert that no exception is thrown during rendering
113
+ expect(true).toBe(true);
114
+ // Assert the value of the component
115
+ expect(dateTimePickerInputComponent).toHaveValue(localDateTimeString);
116
+ });
117
+
118
+ it('does not render with invalid datetime ISO string and throws exception', () => {
119
+ const invalidISODateTimeString = '2024-04-11T01:00:00.000-5:00';
120
+ const personDtoWithDateOnlyString: PersonDto = {
121
+ createdDateTime: invalidISODateTimeString,
122
+ };
123
+
124
+ try {
125
+ render(
126
+ <FormTestBase initialValues={personDtoWithDateOnlyString}>
127
+ {({ Field }) => (
128
+ <Field
129
+ name="createdDateTime"
130
+ Component={StringDateTimePickerGroup}
131
+ label="Created Date Time"
132
+ monthPlaceholder="mm"
133
+ dayPlaceholder="dd"
134
+ yearPlaceholder="yyyy"
135
+ disableClock
136
+ />
137
+ )}
138
+ </FormTestBase>
139
+ );
140
+
141
+ fail('Expected an exception to be thrown for invalid datetime string');
142
+ } catch (error: any) {
143
+ // Assert that the error message matches the expected message
144
+ expect(error.message).toEqual(
145
+ `Invalid "date time" value of ${invalidISODateTimeString} provided to DateTimePickerGroup component. Please provide ISO 8601 string that includes the time zone designator. Sample strings: 2022-09-24T:08:54:12+00:00, 2022-09-24T:08:54:12Z, 2022-09-24T:08:54:12.123-05:00`
146
+ );
147
+ }
148
+ });
149
+
150
+ it('renders with ISO date time string with different offset than browser taking browser timezone into account', () => {
151
+ const isoDateTimeString = '2024-04-09T21:00:24.670-01:00';
152
+ const personDtoWithDateTimeString: PersonDto = {
153
+ createdDateTime: isoDateTimeString,
154
+ };
155
+
156
+ render(
157
+ <FormTestBase initialValues={personDtoWithDateTimeString}>
158
+ {({ Field }) => (
159
+ <Field
160
+ name="createdDateTime"
161
+ Component={StringDateTimePickerGroup}
162
+ label="Created Date Time"
163
+ monthPlaceholder="mm"
164
+ dayPlaceholder="dd"
165
+ yearPlaceholder="yyyy"
166
+ disableClock
167
+ />
168
+ )}
169
+ </FormTestBase>
170
+ );
171
+
172
+ const dateTimePickerInputComponent = document.querySelector(
173
+ 'input[name="datetime"][type="datetime-local"]'
174
+ );
175
+ const utcDate = new Date(isoDateTimeString);
176
+ const localDateTimeString =
177
+ convertToDateTimePickerComponentValueString(utcDate);
178
+
179
+ // Assert that no exception is thrown during rendering
180
+ expect(true).toBe(true);
181
+ // Assert the value of the component
182
+ expect(dateTimePickerInputComponent).toHaveValue(localDateTimeString);
183
+ });
184
+
185
+ it('renders with ISO date time string with same offset as the browser taking browser timezone into account', () => {
186
+ const isoDateTimeString = '2024-04-09T21:00:24.670-05:00';
187
+ const personDtoWithDateTimeString: PersonDto = {
188
+ createdDateTime: isoDateTimeString,
189
+ };
190
+
191
+ render(
192
+ <FormTestBase initialValues={personDtoWithDateTimeString}>
193
+ {({ Field }) => (
194
+ <Field
195
+ name="createdDateTime"
196
+ Component={StringDateTimePickerGroup}
197
+ label="Created Date Time"
198
+ monthPlaceholder="mm"
199
+ dayPlaceholder="dd"
200
+ yearPlaceholder="yyyy"
201
+ disableClock
202
+ />
203
+ )}
204
+ </FormTestBase>
205
+ );
206
+
207
+ const dateTimePickerInputComponent = document.querySelector(
208
+ 'input[name="datetime"][type="datetime-local"]'
209
+ );
210
+ const utcDate = new Date(isoDateTimeString);
211
+ const localDateTimeString =
212
+ convertToDateTimePickerComponentValueString(utcDate);
213
+
214
+ // Assert that no exception is thrown during rendering
215
+ expect(true).toBe(true);
216
+ // Assert the value of the component
217
+ expect(dateTimePickerInputComponent).toHaveValue(localDateTimeString);
218
+ });
219
+ });
220
+
221
+ function convertToDateTimePickerComponentValueString(date: Date): string {
222
+ // Format the date in the required format using `intl.DateTimeFormat`
223
+ const year = new Intl.DateTimeFormat('en', { year: 'numeric' })
224
+ .format(date)
225
+ .padStart(4, '0');
226
+
227
+ const month = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(
228
+ date
229
+ );
230
+
231
+ const day = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date);
232
+
233
+ const hour = new Intl.DateTimeFormat('en', {
234
+ hour: '2-digit',
235
+ hourCycle: 'h23',
236
+ }).format(date);
237
+
238
+ const minute = new Intl.DateTimeFormat('en', { minute: '2-digit' })
239
+ .format(date)
240
+ .padStart(2, '0');
241
+
242
+ return `${year}-${month}-${day}T${hour}:${minute}`;
243
+ }
@@ -0,0 +1,116 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { DateTimePicker, DateTimePickerProps } from 'react-datetime-picker';
3
+ import classnames from 'classnames';
4
+ import parseISO from 'date-fns/parseISO';
5
+ import { InjectedFieldProps } from '../Field/InjectedFieldProps';
6
+ import { FormDefaults } from '../FormDefaults';
7
+ import Group, { GroupProps } from '../Group';
8
+
9
+ // Represents a single date/date string, or null
10
+ type DateTimePickerValuePiece = Date | string | null;
11
+ // Represents either a single date/date string, a pair of dates/date strings for a range, or null
12
+ type DateTimePickerValue =
13
+ | DateTimePickerValuePiece
14
+ | [DateTimePickerValuePiece, DateTimePickerValuePiece];
15
+
16
+ export interface DateTimePickerGroupProps<T>
17
+ extends InjectedFieldProps<T | undefined | null>,
18
+ Omit<
19
+ DateTimePickerProps,
20
+ keyof InjectedFieldProps<T> | 'name' | 'value' | 'className'
21
+ >,
22
+ Omit<GroupProps, keyof InjectedFieldProps<T> | 'children'> {
23
+ convert: (date: Date) => T;
24
+ }
25
+
26
+ /**
27
+ * Field for inputting date and time. Uses `<Group/>` and `<DateTimePicker/>`.
28
+ *
29
+ * Uses [react-datetime-picker](https://www.npmjs.com/package/react-datetime-picker)
30
+ */
31
+ export default function DateTimePickerGroup<T>({
32
+ input,
33
+ meta,
34
+ label,
35
+ helpText,
36
+ className,
37
+ required,
38
+ disabled,
39
+ convert,
40
+ ...rest
41
+ }: DateTimePickerGroupProps<T>) {
42
+ const [displayDateTime, setDisplayDateTime] = useState<Date | null>(null);
43
+
44
+ useEffect(() => {
45
+ const inputValue = input.value;
46
+ if (!inputValue) {
47
+ setDisplayDateTime(null);
48
+ } else if (typeof inputValue === 'string') {
49
+ const parsedDateTime = convertISODateTimeStringToDate(inputValue);
50
+ setDisplayDateTime(parsedDateTime);
51
+ } else if (inputValue instanceof Date) {
52
+ setDisplayDateTime(inputValue);
53
+ }
54
+ }, [setDisplayDateTime, input.value]);
55
+
56
+ return (
57
+ <Group
58
+ input={input}
59
+ meta={meta}
60
+ label={label}
61
+ helpText={helpText}
62
+ className={classnames(
63
+ className,
64
+ FormDefaults.cssClassPrefix + 'date-time-picker'
65
+ )}
66
+ required={required}
67
+ disabled={disabled}>
68
+ <DateTimePicker
69
+ {...rest}
70
+ className={classnames(
71
+ FormDefaults.cssClassPrefix + 'date-time-picker',
72
+ className
73
+ )}
74
+ value={displayDateTime}
75
+ onChange={handleChange}
76
+ />
77
+ </Group>
78
+ );
79
+
80
+ function handleChange(newDateTime: DateTimePickerValue) {
81
+ const { onChange } = input;
82
+ if (onChange === null) {
83
+ return;
84
+ }
85
+
86
+ if (!newDateTime) {
87
+ onChange(undefined);
88
+ setDisplayDateTime(null);
89
+ return;
90
+ } else if (typeof newDateTime === 'string') {
91
+ const parsedDateTime = parseISO(newDateTime);
92
+ setDisplayDateTime(parsedDateTime);
93
+ onChange(convert(parsedDateTime));
94
+ } else if (newDateTime instanceof Date) {
95
+ setDisplayDateTime(newDateTime);
96
+ onChange(convert(newDateTime));
97
+ }
98
+ }
99
+ }
100
+
101
+ function convertISODateTimeStringToDate(isoDateTimeString: string) {
102
+ const isoDateTimeRegex =
103
+ /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?)([+-]\d{2}:\d{2}|Z)?$/;
104
+ const isValidIsoDateTimeString = isoDateTimeRegex.test(isoDateTimeString);
105
+ if (isValidIsoDateTimeString) {
106
+ return new Date(isoDateTimeString);
107
+ }
108
+
109
+ const errorMessage = `Invalid "date time" value of ${isoDateTimeString} provided to DateTimePickerGroup component. Please provide ISO 8601 string that includes the time zone designator. Sample strings: 2022-09-24T:08:54:12+00:00, 2022-09-24T:08:54:12Z, 2022-09-24T:08:54:12.123-05:00`;
110
+ if (process.env.NODE_ENV !== 'production') {
111
+ throw new Error(errorMessage);
112
+ } else {
113
+ console.error(errorMessage);
114
+ }
115
+ return null;
116
+ }
@@ -0,0 +1,4 @@
1
+ import { DateTimePickerGroupProps } from './DateTimePickerGroup';
2
+
3
+ export interface DateTimePickerHelper<T>
4
+ extends Omit<DateTimePickerGroupProps<T>, 'convert'> {}
@@ -0,0 +1,61 @@
1
+ import DateTimePickerGroup from './DateTimePickerGroup';
2
+ import { DateTimePickerHelper } from './DateTimePickerHelper';
3
+
4
+ export interface StringDateTimePickerGroupProps
5
+ extends DateTimePickerHelper<string | undefined | null> {}
6
+
7
+ /**
8
+ * Date Time picker input that consumes JS Date object and outputs as a date time offset string in ISO format `YYYY-MM-DDTHH:mm:ss.sss±hh:mm`.
9
+ *
10
+ * Default display to the user is in `MM/DD/YYYY HH:mm:ss AM/PM` format.
11
+ */
12
+ export default function StringDateTimePickerGroup(
13
+ props: StringDateTimePickerGroupProps
14
+ ) {
15
+ return <DateTimePickerGroup {...props} convert={convertToISOString} />;
16
+ }
17
+
18
+ function convertToISOString(date: Date): string {
19
+ // Get the offset in minutes
20
+ const offsetMinutes = date.getTimezoneOffset();
21
+
22
+ // Calculate the offset in hours and minutes
23
+ const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
24
+ const offsetMinutesPart = Math.abs(offsetMinutes) % 60;
25
+
26
+ // Format the offset in the required format (+/-hh:mm)
27
+ const offsetString =
28
+ (offsetMinutes >= 0 ? '-' : '+') +
29
+ String(offsetHours).padStart(2, '0') +
30
+ ':' +
31
+ String(offsetMinutesPart).padStart(2, '0');
32
+
33
+ // Format the date in the required format using `intl.DateTimeFormat`
34
+ const year = new Intl.DateTimeFormat('en', { year: 'numeric' })
35
+ .format(date)
36
+ .padStart(4, '0');
37
+
38
+ const month = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(
39
+ date
40
+ );
41
+
42
+ const day = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date);
43
+
44
+ const hour = new Intl.DateTimeFormat('en', {
45
+ hour: '2-digit',
46
+ hourCycle: 'h23',
47
+ }).format(date);
48
+
49
+ const minute = new Intl.DateTimeFormat('en', { minute: '2-digit' })
50
+ .format(date)
51
+ .padStart(2, '0');
52
+
53
+ const second = new Intl.DateTimeFormat('en', {
54
+ second: '2-digit',
55
+ fractionalSecondDigits: 3,
56
+ })
57
+ .format(date)
58
+ .padStart(6, '0');
59
+
60
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}${offsetString}`;
61
+ }