@toptal/picasso-forms 6.0.4 → 6.0.5

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 (236) hide show
  1. package/CHANGELOG.md +657 -0
  2. package/{Autocomplete → dist-package/Autocomplete}/Autocomplete.d.ts +0 -0
  3. package/{Autocomplete → dist-package/Autocomplete}/Autocomplete.js +0 -0
  4. package/{Autocomplete → dist-package/Autocomplete}/Autocomplete.js.map +0 -0
  5. package/{Autocomplete → dist-package/Autocomplete}/index.d.ts +0 -0
  6. package/{Autocomplete → dist-package/Autocomplete}/index.js +0 -0
  7. package/{Autocomplete → dist-package/Autocomplete}/index.js.map +0 -0
  8. package/{ButtonCheckbox → dist-package/ButtonCheckbox}/ButtonCheckbox.d.ts +0 -0
  9. package/{ButtonCheckbox → dist-package/ButtonCheckbox}/ButtonCheckbox.js +0 -0
  10. package/{ButtonCheckbox → dist-package/ButtonCheckbox}/ButtonCheckbox.js.map +0 -0
  11. package/{ButtonCheckbox → dist-package/ButtonCheckbox}/index.d.ts +0 -0
  12. package/{ButtonCheckbox → dist-package/ButtonCheckbox}/index.js +0 -0
  13. package/{ButtonCheckbox → dist-package/ButtonCheckbox}/index.js.map +0 -0
  14. package/{ButtonRadio → dist-package/ButtonRadio}/ButtonRadio.d.ts +0 -0
  15. package/{ButtonRadio → dist-package/ButtonRadio}/ButtonRadio.js +0 -0
  16. package/{ButtonRadio → dist-package/ButtonRadio}/ButtonRadio.js.map +0 -0
  17. package/{ButtonRadio → dist-package/ButtonRadio}/index.d.ts +0 -0
  18. package/{ButtonRadio → dist-package/ButtonRadio}/index.js +0 -0
  19. package/{ButtonRadio → dist-package/ButtonRadio}/index.js.map +0 -0
  20. package/{Checkbox → dist-package/Checkbox}/Checkbox.d.ts +0 -0
  21. package/{Checkbox → dist-package/Checkbox}/Checkbox.js +0 -0
  22. package/{Checkbox → dist-package/Checkbox}/Checkbox.js.map +0 -0
  23. package/{Checkbox → dist-package/Checkbox}/index.d.ts +0 -0
  24. package/{Checkbox → dist-package/Checkbox}/index.js +0 -0
  25. package/{Checkbox → dist-package/Checkbox}/index.js.map +0 -0
  26. package/{CheckboxGroup → dist-package/CheckboxGroup}/CheckboxGroup.d.ts +0 -0
  27. package/{CheckboxGroup → dist-package/CheckboxGroup}/CheckboxGroup.js +0 -0
  28. package/{CheckboxGroup → dist-package/CheckboxGroup}/CheckboxGroup.js.map +0 -0
  29. package/{CheckboxGroup → dist-package/CheckboxGroup}/CheckboxGroupContext.d.ts +0 -0
  30. package/{CheckboxGroup → dist-package/CheckboxGroup}/CheckboxGroupContext.js +0 -0
  31. package/{CheckboxGroup → dist-package/CheckboxGroup}/CheckboxGroupContext.js.map +0 -0
  32. package/{CheckboxGroup → dist-package/CheckboxGroup}/index.d.ts +0 -0
  33. package/{CheckboxGroup → dist-package/CheckboxGroup}/index.js +0 -0
  34. package/{CheckboxGroup → dist-package/CheckboxGroup}/index.js.map +0 -0
  35. package/{DatePicker → dist-package/DatePicker}/DatePicker.d.ts +0 -0
  36. package/{DatePicker → dist-package/DatePicker}/DatePicker.js +0 -0
  37. package/{DatePicker → dist-package/DatePicker}/DatePicker.js.map +0 -0
  38. package/{DatePicker → dist-package/DatePicker}/index.d.ts +0 -0
  39. package/{DatePicker → dist-package/DatePicker}/index.js +0 -0
  40. package/{DatePicker → dist-package/DatePicker}/index.js.map +0 -0
  41. package/{FieldWrapper → dist-package/FieldWrapper}/FieldWrapper.d.ts +0 -0
  42. package/{FieldWrapper → dist-package/FieldWrapper}/FieldWrapper.js +0 -0
  43. package/{FieldWrapper → dist-package/FieldWrapper}/FieldWrapper.js.map +0 -0
  44. package/{FieldWrapper → dist-package/FieldWrapper}/index.d.ts +0 -0
  45. package/{FieldWrapper → dist-package/FieldWrapper}/index.js +0 -0
  46. package/{FieldWrapper → dist-package/FieldWrapper}/index.js.map +0 -0
  47. package/{FileInput → dist-package/FileInput}/FileInput.d.ts +0 -0
  48. package/{FileInput → dist-package/FileInput}/FileInput.js +0 -0
  49. package/{FileInput → dist-package/FileInput}/FileInput.js.map +0 -0
  50. package/{FileInput → dist-package/FileInput}/index.d.ts +0 -0
  51. package/{FileInput → dist-package/FileInput}/index.js +0 -0
  52. package/{FileInput → dist-package/FileInput}/index.js.map +0 -0
  53. package/{Form → dist-package/Form}/Form.d.ts +0 -0
  54. package/{Form → dist-package/Form}/Form.js +0 -0
  55. package/{Form → dist-package/Form}/Form.js.map +0 -0
  56. package/{Form → dist-package/Form}/FormContext.d.ts +0 -0
  57. package/{Form → dist-package/Form}/FormContext.js +0 -0
  58. package/{Form → dist-package/Form}/FormContext.js.map +0 -0
  59. package/{Form → dist-package/Form}/index.d.ts +0 -0
  60. package/{Form → dist-package/Form}/index.js +0 -0
  61. package/{Form → dist-package/Form}/index.js.map +0 -0
  62. package/{FormConfig → dist-package/FormConfig}/FormConfig.d.ts +0 -0
  63. package/{FormConfig → dist-package/FormConfig}/FormConfig.js +0 -0
  64. package/{FormConfig → dist-package/FormConfig}/FormConfig.js.map +0 -0
  65. package/{FormConfig → dist-package/FormConfig}/index.d.ts +0 -0
  66. package/{FormConfig → dist-package/FormConfig}/index.js +0 -0
  67. package/{FormConfig → dist-package/FormConfig}/index.js.map +0 -0
  68. package/{Input → dist-package/Input}/Input.d.ts +0 -0
  69. package/{Input → dist-package/Input}/Input.js +0 -0
  70. package/{Input → dist-package/Input}/Input.js.map +0 -0
  71. package/{Input → dist-package/Input}/index.d.ts +0 -0
  72. package/{Input → dist-package/Input}/index.js +0 -0
  73. package/{Input → dist-package/Input}/index.js.map +0 -0
  74. package/{Input → dist-package/Input}/utils/get-input-name.d.ts +0 -0
  75. package/{Input → dist-package/Input}/utils/get-input-name.js +0 -0
  76. package/{Input → dist-package/Input}/utils/get-input-name.js.map +0 -0
  77. package/{Input → dist-package/Input}/utils/get-input-name.test.d.ts +0 -0
  78. package/{Input → dist-package/Input}/utils/get-input-name.test.js +0 -0
  79. package/{Input → dist-package/Input}/utils/get-input-name.test.js.map +0 -0
  80. package/{NumberInput → dist-package/NumberInput}/NumberInput.d.ts +0 -0
  81. package/{NumberInput → dist-package/NumberInput}/NumberInput.js +0 -0
  82. package/{NumberInput → dist-package/NumberInput}/NumberInput.js.map +0 -0
  83. package/{NumberInput → dist-package/NumberInput}/index.d.ts +0 -0
  84. package/{NumberInput → dist-package/NumberInput}/index.js +0 -0
  85. package/{NumberInput → dist-package/NumberInput}/index.js.map +0 -0
  86. package/dist-package/README.md +29 -0
  87. package/{Radio → dist-package/Radio}/Radio.d.ts +0 -0
  88. package/{Radio → dist-package/Radio}/Radio.js +0 -0
  89. package/{Radio → dist-package/Radio}/Radio.js.map +0 -0
  90. package/{Radio → dist-package/Radio}/index.d.ts +0 -0
  91. package/{Radio → dist-package/Radio}/index.js +0 -0
  92. package/{Radio → dist-package/Radio}/index.js.map +0 -0
  93. package/{RadioGroup → dist-package/RadioGroup}/RadioGroup.d.ts +0 -0
  94. package/{RadioGroup → dist-package/RadioGroup}/RadioGroup.js +0 -0
  95. package/{RadioGroup → dist-package/RadioGroup}/RadioGroup.js.map +0 -0
  96. package/{RadioGroup → dist-package/RadioGroup}/RadioGroupContext.d.ts +0 -0
  97. package/{RadioGroup → dist-package/RadioGroup}/RadioGroupContext.js +0 -0
  98. package/{RadioGroup → dist-package/RadioGroup}/RadioGroupContext.js.map +0 -0
  99. package/{RadioGroup → dist-package/RadioGroup}/index.d.ts +0 -0
  100. package/{RadioGroup → dist-package/RadioGroup}/index.js +0 -0
  101. package/{RadioGroup → dist-package/RadioGroup}/index.js.map +0 -0
  102. package/{Rating → dist-package/Rating}/Rating.d.ts +0 -0
  103. package/{Rating → dist-package/Rating}/Rating.js +0 -0
  104. package/{Rating → dist-package/Rating}/Rating.js.map +0 -0
  105. package/{Rating → dist-package/Rating}/index.d.ts +0 -0
  106. package/{Rating → dist-package/Rating}/index.js +0 -0
  107. package/{Rating → dist-package/Rating}/index.js.map +0 -0
  108. package/{Select → dist-package/Select}/Select.d.ts +0 -0
  109. package/{Select → dist-package/Select}/Select.js +0 -0
  110. package/{Select → dist-package/Select}/Select.js.map +0 -0
  111. package/{Select → dist-package/Select}/index.d.ts +0 -0
  112. package/{Select → dist-package/Select}/index.js +0 -0
  113. package/{Select → dist-package/Select}/index.js.map +0 -0
  114. package/{SubmitButton → dist-package/SubmitButton}/SubmitButton.d.ts +0 -0
  115. package/{SubmitButton → dist-package/SubmitButton}/SubmitButton.js +0 -0
  116. package/{SubmitButton → dist-package/SubmitButton}/SubmitButton.js.map +0 -0
  117. package/{SubmitButton → dist-package/SubmitButton}/index.d.ts +0 -0
  118. package/{SubmitButton → dist-package/SubmitButton}/index.js +0 -0
  119. package/{SubmitButton → dist-package/SubmitButton}/index.js.map +0 -0
  120. package/{Switch → dist-package/Switch}/Switch.d.ts +0 -0
  121. package/{Switch → dist-package/Switch}/Switch.js +0 -0
  122. package/{Switch → dist-package/Switch}/Switch.js.map +0 -0
  123. package/{Switch → dist-package/Switch}/index.d.ts +0 -0
  124. package/{Switch → dist-package/Switch}/index.js +0 -0
  125. package/{Switch → dist-package/Switch}/index.js.map +0 -0
  126. package/{TagSelector → dist-package/TagSelector}/TagSelector.d.ts +0 -0
  127. package/{TagSelector → dist-package/TagSelector}/TagSelector.js +0 -0
  128. package/{TagSelector → dist-package/TagSelector}/TagSelector.js.map +0 -0
  129. package/{TagSelector → dist-package/TagSelector}/index.d.ts +0 -0
  130. package/{TagSelector → dist-package/TagSelector}/index.js +0 -0
  131. package/{TagSelector → dist-package/TagSelector}/index.js.map +0 -0
  132. package/{TimePicker → dist-package/TimePicker}/TimePicker.d.ts +0 -0
  133. package/{TimePicker → dist-package/TimePicker}/TimePicker.js +0 -0
  134. package/{TimePicker → dist-package/TimePicker}/TimePicker.js.map +0 -0
  135. package/{TimePicker → dist-package/TimePicker}/index.d.ts +0 -0
  136. package/{TimePicker → dist-package/TimePicker}/index.js +0 -0
  137. package/{TimePicker → dist-package/TimePicker}/index.js.map +0 -0
  138. package/{index.d.ts → dist-package/index.d.ts} +2 -1
  139. package/{index.js → dist-package/index.js} +0 -1
  140. package/dist-package/index.js.map +1 -0
  141. package/dist-package/package.json +44 -0
  142. package/{utils → dist-package/utils}/flat-map.d.ts +0 -0
  143. package/{utils → dist-package/utils}/flat-map.js +0 -0
  144. package/{utils → dist-package/utils}/flat-map.js.map +0 -0
  145. package/{utils → dist-package/utils}/index.d.ts +0 -0
  146. package/{utils → dist-package/utils}/index.js +0 -0
  147. package/{utils → dist-package/utils}/index.js.map +0 -0
  148. package/{utils → dist-package/utils}/scroll-to-error-decorator.d.ts +0 -0
  149. package/{utils → dist-package/utils}/scroll-to-error-decorator.js +0 -0
  150. package/{utils → dist-package/utils}/scroll-to-error-decorator.js.map +0 -0
  151. package/{utils → dist-package/utils}/validators.d.ts +0 -0
  152. package/{utils → dist-package/utils}/validators.js +0 -0
  153. package/{utils → dist-package/utils}/validators.js.map +0 -0
  154. package/package.json +4 -5
  155. package/src/Autocomplete/Autocomplete.tsx +21 -0
  156. package/src/Autocomplete/index.ts +1 -0
  157. package/src/ButtonCheckbox/ButtonCheckbox.tsx +57 -0
  158. package/src/ButtonCheckbox/index.ts +1 -0
  159. package/src/ButtonRadio/ButtonRadio.tsx +24 -0
  160. package/src/ButtonRadio/index.ts +1 -0
  161. package/src/Checkbox/Checkbox.tsx +73 -0
  162. package/src/Checkbox/__snapshots__/test.tsx.snap +254 -0
  163. package/src/Checkbox/index.ts +1 -0
  164. package/src/Checkbox/test.tsx +91 -0
  165. package/src/CheckboxGroup/CheckboxGroup.tsx +30 -0
  166. package/src/CheckboxGroup/CheckboxGroupContext.ts +3 -0
  167. package/src/CheckboxGroup/index.ts +3 -0
  168. package/src/CheckboxGroup/test.tsx +35 -0
  169. package/src/DatePicker/DatePicker.tsx +26 -0
  170. package/src/DatePicker/index.ts +1 -0
  171. package/src/FieldWrapper/FieldWrapper.tsx +287 -0
  172. package/src/FieldWrapper/index.ts +2 -0
  173. package/src/FieldWrapper/story/index.jsx +137 -0
  174. package/src/FileInput/FileInput.tsx +66 -0
  175. package/src/FileInput/index.ts +1 -0
  176. package/src/Form/Form.tsx +181 -0
  177. package/src/Form/FormContext.ts +38 -0
  178. package/src/Form/__image_snapshots__/form-default-snap.png +0 -0
  179. package/src/Form/__image_snapshots__/form-form-level-configurations-snap.png +0 -0
  180. package/src/Form/__snapshots__/test.tsx.snap +61 -0
  181. package/src/Form/index.ts +1 -0
  182. package/src/Form/story/BackendCommunication.example.tsx +139 -0
  183. package/src/Form/story/CustomFormLevelConfiguration.example.tsx +26 -0
  184. package/src/Form/story/CustomValidator.example.tsx +45 -0
  185. package/src/Form/story/Default.example.tsx +177 -0
  186. package/src/Form/story/FileInput.example.tsx +42 -0
  187. package/src/Form/story/ParseInput.example.tsx +28 -0
  188. package/src/Form/story/TitleCase.example.tsx +167 -0
  189. package/src/Form/story/ValidateOnSubmit.example.tsx +85 -0
  190. package/src/Form/story/index.jsx +203 -0
  191. package/src/Form/test.tsx +27 -0
  192. package/src/FormConfig/FormConfig.ts +12 -0
  193. package/src/FormConfig/index.ts +1 -0
  194. package/src/FormConfig/test.tsx +44 -0
  195. package/src/Input/Input.tsx +27 -0
  196. package/src/Input/index.ts +1 -0
  197. package/src/Input/test.tsx +34 -0
  198. package/src/Input/utils/get-input-name.test.ts +16 -0
  199. package/src/Input/utils/get-input-name.ts +11 -0
  200. package/src/NumberInput/NumberInput.tsx +45 -0
  201. package/src/NumberInput/index.ts +1 -0
  202. package/src/Radio/Radio.tsx +24 -0
  203. package/src/Radio/__snapshots__/test.tsx.snap +231 -0
  204. package/src/Radio/index.ts +1 -0
  205. package/src/Radio/test.tsx +49 -0
  206. package/src/RadioGroup/RadioGroup.tsx +39 -0
  207. package/src/RadioGroup/RadioGroupContext.ts +3 -0
  208. package/src/RadioGroup/index.ts +3 -0
  209. package/src/RadioGroup/test.tsx +35 -0
  210. package/src/Rating/Rating.tsx +22 -0
  211. package/src/Rating/index.ts +1 -0
  212. package/src/Select/Select.tsx +47 -0
  213. package/src/Select/index.ts +1 -0
  214. package/src/SubmitButton/SubmitButton.tsx +70 -0
  215. package/src/SubmitButton/__image_snapshots__/submitbutton-button-types-snap.png +0 -0
  216. package/src/SubmitButton/__image_snapshots__/submitbutton-default-snap.png +0 -0
  217. package/src/SubmitButton/index.ts +6 -0
  218. package/src/SubmitButton/story/ButtonTypes.example.tsx +46 -0
  219. package/src/SubmitButton/story/Default.example.tsx +15 -0
  220. package/src/SubmitButton/story/index.jsx +32 -0
  221. package/src/Switch/Switch.tsx +23 -0
  222. package/src/Switch/index.ts +1 -0
  223. package/src/TagSelector/TagSelector.tsx +25 -0
  224. package/src/TagSelector/index.ts +1 -0
  225. package/src/TimePicker/TimePicker.tsx +24 -0
  226. package/src/TimePicker/index.ts +1 -0
  227. package/src/index.ts +16 -0
  228. package/src/story/Deserialization.example.tsx +34 -0
  229. package/src/story/FormSpy.example.tsx +42 -0
  230. package/src/story/index.jsx +37 -0
  231. package/src/utils/flat-map.ts +4 -0
  232. package/src/utils/index.ts +3 -0
  233. package/src/utils/scroll-to-error-decorator.ts +78 -0
  234. package/src/utils/validators.ts +18 -0
  235. package/tsconfig.build.json +7 -0
  236. package/index.js.map +0 -1
@@ -0,0 +1,287 @@
1
+ import React, { ChangeEvent, FocusEvent, useCallback, useEffect } from 'react'
2
+ import {
3
+ useField,
4
+ FieldProps as FinalFieldProps,
5
+ FieldMetaState,
6
+ FieldRenderProps
7
+ } from 'react-final-form'
8
+ import { Form as PicassoForm, RequiredDecoration } from '@toptal/picasso'
9
+ import { Item } from '@toptal/picasso/Autocomplete'
10
+ import { FileUpload } from '@toptal/picasso/FileInput'
11
+ import { DateOrDateRangeType } from '@toptal/picasso-lab'
12
+ import { TextLabelProps } from '@toptal/picasso-shared'
13
+
14
+ import { useFormContext } from '../Form/FormContext'
15
+ import { useFormConfig, FormConfigProps, RequiredVariant } from '../FormConfig'
16
+ import { validators } from '../utils'
17
+
18
+ const { composeValidators, required: requiredValidator } = validators
19
+
20
+ type ValueType =
21
+ | string
22
+ | string[]
23
+ | number
24
+ | boolean
25
+ | null
26
+ | undefined
27
+ | FileUpload[]
28
+ | DateOrDateRangeType
29
+ | Item
30
+ | Item[]
31
+
32
+ export type FieldProps<TInputValue> = FinalFieldProps<
33
+ TInputValue,
34
+ FieldRenderProps<TInputValue, HTMLInputElement>,
35
+ HTMLInputElement
36
+ > &
37
+ TextLabelProps
38
+
39
+ export type Props<
40
+ TInputValue,
41
+ TWrappedComponentProps
42
+ > = TWrappedComponentProps &
43
+ FieldProps<TInputValue> &
44
+ TextLabelProps & {
45
+ name: string
46
+ type?: string
47
+ hideFieldLabel?: boolean
48
+ hideLabelRequiredDecoration?: boolean
49
+ fieldType?: string
50
+ children: (props: any) => React.ReactNode
51
+ }
52
+
53
+ type FieldMeta<T> = FieldMetaState<T> & {
54
+ dirtyAfterBlur?: boolean
55
+ }
56
+
57
+ const getInputError = <T extends ValueType>(
58
+ meta: FieldMeta<T>,
59
+ formConfig: FormConfigProps
60
+ ) => {
61
+ if (formConfig.validateOnSubmit && meta.modifiedSinceLastSubmit) {
62
+ return null
63
+ }
64
+
65
+ if (!meta.error && !meta.submitError) {
66
+ return null
67
+ }
68
+
69
+ if (!meta.touched) {
70
+ return null
71
+ }
72
+
73
+ if (meta.error) {
74
+ return meta.error
75
+ }
76
+
77
+ if (meta.dirtySinceLastSubmit) {
78
+ return null
79
+ }
80
+
81
+ return meta.submitError
82
+ }
83
+
84
+ const getValidators = (required: boolean, validate?: any) => {
85
+ if (required && validate) {
86
+ return composeValidators([requiredValidator, validate])
87
+ }
88
+
89
+ if (required && !validate) {
90
+ return requiredValidator
91
+ }
92
+
93
+ return validate
94
+ }
95
+
96
+ const getProps = ({
97
+ hideFieldLabel,
98
+ error,
99
+ label
100
+ }: {
101
+ hideFieldLabel?: boolean
102
+ error: string
103
+ label: string
104
+ }) => {
105
+ if (hideFieldLabel) {
106
+ return {
107
+ label
108
+ }
109
+ }
110
+
111
+ return {
112
+ error: Boolean(error)
113
+ }
114
+ }
115
+
116
+ const getRequiredDecoration = (
117
+ hideLabelRequiredDecoration?: boolean,
118
+ required?: boolean,
119
+ requiredVariant?: RequiredVariant
120
+ ): RequiredDecoration | undefined => {
121
+ if (hideLabelRequiredDecoration) {
122
+ return
123
+ }
124
+
125
+ const showAsterisk = required && requiredVariant === 'asterisk'
126
+
127
+ if (showAsterisk) {
128
+ return 'asterisk'
129
+ }
130
+
131
+ const showOptional =
132
+ !required && (!requiredVariant || requiredVariant === 'default')
133
+
134
+ if (showOptional) {
135
+ return 'optional'
136
+ }
137
+ }
138
+
139
+ const FieldWrapper = <
140
+ TWrappedComponentProps extends { value?: ValueType },
141
+ TInputValue extends ValueType = TWrappedComponentProps['value']
142
+ >(
143
+ props: Props<TInputValue, TWrappedComponentProps>
144
+ ) => {
145
+ const {
146
+ type,
147
+ hideFieldLabel,
148
+ hideLabelRequiredDecoration,
149
+ hint,
150
+ label,
151
+ required,
152
+ enableReset,
153
+ onResetClick,
154
+ 'data-testid': dataTestId,
155
+ // FieldProps - https://final-form.org/docs/react-final-form/types/FieldProps
156
+ afterSubmit,
157
+ allowNull,
158
+ beforeSubmit,
159
+ children,
160
+ data,
161
+ defaultValue,
162
+ format,
163
+ formatOnBlur,
164
+ initialValue,
165
+ isEqual,
166
+ name,
167
+ id = name,
168
+ parse,
169
+ subscription,
170
+ validate,
171
+ validateFields,
172
+ value,
173
+ titleCase,
174
+ //
175
+ ...rest
176
+ } = props
177
+
178
+ const formConfig = useFormConfig()
179
+ const { setValidators, clearValidators } = useFormContext()
180
+ const validators = getValidators(required, validate)
181
+
182
+ if (formConfig.validateOnSubmit) {
183
+ setValidators(name, validators)
184
+ }
185
+
186
+ useEffect(() => {
187
+ return () => {
188
+ if (formConfig.validateOnSubmit) {
189
+ clearValidators(name)
190
+ }
191
+ }
192
+ }, [clearValidators, formConfig.validateOnSubmit, name])
193
+
194
+ const { meta, input } = useField<TInputValue>(name, {
195
+ validate: formConfig.validateOnSubmit ? undefined : validators,
196
+ type,
197
+ afterSubmit,
198
+ allowNull,
199
+ beforeSubmit,
200
+ data,
201
+ defaultValue,
202
+ format,
203
+ formatOnBlur,
204
+ initialValue,
205
+ isEqual,
206
+ parse,
207
+ subscription,
208
+ validateFields,
209
+ value
210
+ })
211
+
212
+ const defaultResetClickHandler = useCallback(() => {
213
+ input.onChange('')
214
+ }, [input])
215
+
216
+ const resetClickHandler = useCallback(() => {
217
+ onResetClick((resetValue: string) => {
218
+ input.onChange(resetValue)
219
+ })
220
+ }, [input, onResetClick])
221
+
222
+ const error = getInputError<TInputValue>(meta, formConfig)
223
+
224
+ const childProps: Record<string, unknown> = {
225
+ id,
226
+ ...rest,
227
+ ...input,
228
+ ...getProps({ hideFieldLabel, error, label }),
229
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
230
+ onChange: (event: ChangeEvent<HTMLElement> | any) => {
231
+ input.onChange(event)
232
+
233
+ if (rest.onChange) {
234
+ rest.onChange(event)
235
+ }
236
+ },
237
+ onBlur: (event: FocusEvent<HTMLElement>) => {
238
+ input.onBlur(event)
239
+
240
+ if (rest.onBlur) {
241
+ rest.onBlur(event)
242
+ }
243
+ },
244
+ onFocus: (event: FocusEvent<HTMLElement>) => {
245
+ input.onFocus(event)
246
+
247
+ if (rest.onFocus) {
248
+ rest.onFocus(event)
249
+ }
250
+ }
251
+ }
252
+
253
+ if (enableReset) {
254
+ childProps.onResetClick = onResetClick
255
+ ? resetClickHandler
256
+ : defaultResetClickHandler
257
+ childProps.enableReset = enableReset
258
+ }
259
+
260
+ const requiredDecoration = getRequiredDecoration(
261
+ hideLabelRequiredDecoration,
262
+ required,
263
+ formConfig.requiredVariant
264
+ )
265
+
266
+ return (
267
+ <PicassoForm.Field error={error} hint={hint} data-testid={dataTestId}>
268
+ {!hideFieldLabel && label && (
269
+ <PicassoForm.Label
270
+ requiredDecoration={requiredDecoration}
271
+ htmlFor={id}
272
+ disabled={rest.disabled}
273
+ titleCase={titleCase}
274
+ >
275
+ {label}
276
+ </PicassoForm.Label>
277
+ )}
278
+ {children(childProps)}
279
+ </PicassoForm.Field>
280
+ )
281
+ }
282
+
283
+ FieldWrapper.defaultProps = {}
284
+
285
+ FieldWrapper.displayName = 'FieldWrapper'
286
+
287
+ export default FieldWrapper
@@ -0,0 +1,2 @@
1
+ export { default } from './FieldWrapper'
2
+ export * from './FieldWrapper'
@@ -0,0 +1,137 @@
1
+ import FieldWrapper from '../FieldWrapper'
2
+ import PicassoBook from '~/.storybook/components/PicassoBook'
3
+
4
+ const componentDocs = PicassoBook.createComponentDocs(
5
+ FieldWrapper,
6
+ 'Form.Field',
7
+ undefined,
8
+ {
9
+ name: {
10
+ name: 'name',
11
+ type: 'string',
12
+ description: 'The field name',
13
+ required: true
14
+ },
15
+ label: {
16
+ name: 'label',
17
+ type: 'string',
18
+ description: 'The field label text'
19
+ },
20
+ hint: {
21
+ name: 'hint',
22
+ type: 'string',
23
+ description: 'The hint of the field with some additional information'
24
+ },
25
+ required: {
26
+ name: 'required',
27
+ type: 'boolean',
28
+ description: 'Makes field to be required in the form'
29
+ },
30
+ // FieldProps - https://final-form.org/docs/react-final-form/types/FieldProps
31
+ afterSubmit: {
32
+ name: 'afterSubmit',
33
+ type: {
34
+ name: 'function',
35
+ description: '() => void'
36
+ },
37
+ description:
38
+ 'A callback to notify fields after submission has completed successfully'
39
+ },
40
+ allowNull: {
41
+ name: 'allowNull',
42
+ type: 'boolean',
43
+ description: 'By default null value is converted to empty string',
44
+ defaultValue: false
45
+ },
46
+ beforeSubmit: {
47
+ name: 'beforeSubmit',
48
+ type: {
49
+ name: 'function',
50
+ description: '() => void | false'
51
+ },
52
+ description: 'A function to call just before calling onSubmit'
53
+ },
54
+ data: {
55
+ name: 'data',
56
+ type: 'object',
57
+ description: 'Initial state for arbitrary values to be placed by mutators'
58
+ },
59
+ defaultValue: {
60
+ name: 'defaultValue',
61
+ type: 'any',
62
+ description: 'Default value of the field upon creation'
63
+ },
64
+ format: {
65
+ name: 'format',
66
+ type: {
67
+ name: 'function',
68
+ description: '(value: any, name: string) => any'
69
+ },
70
+ description:
71
+ 'A function that takes the value from the form values and the name of the field and formats the value to give to the input'
72
+ },
73
+ formatOnBlur: {
74
+ name: 'formatOnBlur',
75
+ type: 'boolean',
76
+ description:
77
+ 'If true, the format function will only be called when the field is blurred. If false, format will be called on every render'
78
+ },
79
+ initialValue: {
80
+ name: 'initialValue',
81
+ type: 'any',
82
+ description: 'The initial value for the field'
83
+ },
84
+ isEqual: {
85
+ name: 'isEqual',
86
+ type: {
87
+ name: 'function',
88
+ description: '(a: any, b: any) => boolean'
89
+ },
90
+ description: 'A function to determine if two values are equal'
91
+ },
92
+ parse: {
93
+ name: 'parse',
94
+ type: {
95
+ name: 'function',
96
+ description: '(value: any, name: string) => any'
97
+ },
98
+ description:
99
+ "A function that takes the value from the input and name of the field and converts the value into the value you want stored as this field's value in the form"
100
+ },
101
+ subscription: {
102
+ name: 'subscription',
103
+ type: {
104
+ name: 'object',
105
+ description: '{ [string]: boolean }'
106
+ },
107
+ description:
108
+ 'An object of the parts of FieldState (final-form) to subscribe to'
109
+ },
110
+ validate: {
111
+ name: 'validate',
112
+ type: {
113
+ name: 'function',
114
+ description:
115
+ '(value: ?any, allValues: Object, meta: ?FieldState) => ?any'
116
+ },
117
+ description:
118
+ 'A function that takes the field value, all the values of the form and the meta data about the field and returns an error if the value is invalid, or undefined if the value is valid'
119
+ },
120
+ validateFields: {
121
+ name: 'validateFields',
122
+ type: 'string[]',
123
+ description: 'An array of field names to validate when this field changes'
124
+ },
125
+ // //
126
+ allFieldProps: {
127
+ name: '<all field props>',
128
+ type: 'any',
129
+ description:
130
+ 'This component also accepts all the native props from the corresponding form component, ex. Form.Input accepts all the Picasso Input props'
131
+ }
132
+ }
133
+ )
134
+
135
+ export default {
136
+ componentDocs
137
+ }
@@ -0,0 +1,66 @@
1
+ import React from 'react'
2
+ import { FileInput as PicassoFileInput, FileInputProps } from '@toptal/picasso'
3
+ import { FieldInputProps as FinalFieldInputProps } from 'react-final-form'
4
+ import { FileUpload } from '@toptal/picasso/FileInput'
5
+
6
+ import FieldWrapper, { FieldProps } from '../FieldWrapper'
7
+
8
+ type FinalFormOnChangeType = FinalFieldInputProps<
9
+ FileInputProps['value']
10
+ >['onChange']
11
+
12
+ export type Props = FileInputProps & FieldProps<FileInputProps['value']>
13
+
14
+ export const FileInput = (props: Props) => {
15
+ const handleChange = (
16
+ event: React.ChangeEvent<HTMLInputElement>,
17
+ value: FileUpload[] | undefined = [],
18
+ finalFormOnChange: FinalFormOnChangeType
19
+ ) => {
20
+ if (!event.target || !event.target.files || !event.target.files.length) {
21
+ return null
22
+ }
23
+
24
+ const newFiles = Array.from(event.target.files).map(file => ({
25
+ file,
26
+ uploading: false
27
+ }))
28
+
29
+ finalFormOnChange([...value, ...newFiles])
30
+
31
+ // reset input
32
+ event.target.value = ''
33
+ }
34
+
35
+ const handleRemove = (
36
+ fileIndex: number,
37
+ value: FileUpload[] | undefined = [],
38
+ finalFormOnChange: FinalFormOnChangeType
39
+ ) => {
40
+ const updatedFiles = value.filter((_, index) => index !== fileIndex)
41
+
42
+ finalFormOnChange(updatedFiles)
43
+ }
44
+
45
+ return (
46
+ <FieldWrapper<FileInputProps, FileUpload[] | undefined> {...props}>
47
+ {inputProps => (
48
+ <PicassoFileInput
49
+ {...inputProps}
50
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
51
+ handleChange(event, inputProps.value, inputProps.onChange)
52
+ }}
53
+ onRemove={(fileName: string, index: number) => {
54
+ handleRemove(index, inputProps.value, inputProps.onChange)
55
+ }}
56
+ />
57
+ )}
58
+ </FieldWrapper>
59
+ )
60
+ }
61
+
62
+ FileInput.defaultProps = {}
63
+
64
+ FileInput.displayName = 'FileInput'
65
+
66
+ export default FileInput
@@ -0,0 +1 @@
1
+ export { default } from './FileInput'
@@ -0,0 +1,181 @@
1
+ import React, { useMemo, ReactNode, useRef } from 'react'
2
+ import {
3
+ Form as FinalForm,
4
+ FormProps as FinalFormProps
5
+ } from 'react-final-form'
6
+ import { FormApi, SubmissionErrors, getIn, setIn, AnyObject } from 'final-form'
7
+ import { Form as PicassoForm, Container } from '@toptal/picasso'
8
+ import { useNotifications } from '@toptal/picasso/utils'
9
+
10
+ import Autocomplete from '../Autocomplete'
11
+ import Input from '../Input'
12
+ import Select from '../Select'
13
+ import Radio from '../Radio'
14
+ import ButtonRadio from '../ButtonRadio'
15
+ import RadioGroup from '../RadioGroup'
16
+ import Checkbox from '../Checkbox'
17
+ import ButtonCheckbox from '../ButtonCheckbox'
18
+ import CheckboxGroup from '../CheckboxGroup'
19
+ import NumberInput from '../NumberInput'
20
+ import FileInput from '../FileInput'
21
+ import DatePicker from '../DatePicker'
22
+ import TimePicker from '../TimePicker'
23
+ import TagSelector from '../TagSelector'
24
+ import SubmitButton from '../SubmitButton'
25
+ import Switch from '../Switch'
26
+ import Rating from '../Rating'
27
+ import { FormConfigContext } from '../FormConfig'
28
+ import { createScrollToErrorDecorator } from '../utils'
29
+ import {
30
+ FormContext,
31
+ Validators,
32
+ FormContextProps,
33
+ createFormContext
34
+ } from './FormContext'
35
+
36
+ export type Props<T = AnyObject> = FinalFormProps<T> & {
37
+ autoComplete?: HTMLFormElement['autocomplete']
38
+ successSubmitMessage?: ReactNode
39
+ failedSubmitMessage?: ReactNode
40
+ scrollOffsetTop?: number
41
+ 'data-testid'?: string
42
+ }
43
+
44
+ const getValidationErrors = (
45
+ validators: Validators,
46
+ formValues: any,
47
+ form: FormApi<any>
48
+ ): SubmissionErrors | void => {
49
+ let errors: SubmissionErrors
50
+
51
+ Object.entries(validators).forEach(([key, validator]) => {
52
+ const fieldValue = getIn(formValues, key)
53
+ const fieldMetaState = form.getFieldState(key)
54
+
55
+ if (!validator) {
56
+ return
57
+ }
58
+
59
+ const error = validator(fieldValue, formValues, fieldMetaState)
60
+
61
+ if (error) {
62
+ errors = setIn(errors || {}, key, error)
63
+ }
64
+ })
65
+
66
+ return errors
67
+ }
68
+
69
+ export const Form = <T extends any = AnyObject>(props: Props<T>) => {
70
+ const {
71
+ children,
72
+ autoComplete,
73
+ onSubmit,
74
+ successSubmitMessage,
75
+ failedSubmitMessage,
76
+ decorators = [],
77
+ 'data-testid': dataTestId,
78
+ ...rest
79
+ } = props
80
+ const { showSuccess, showError } = useNotifications()
81
+ const scrollToErrorDecorator = useMemo(
82
+ () => createScrollToErrorDecorator(),
83
+ []
84
+ )
85
+
86
+ const validationObject = useRef<FormContextProps>(createFormContext())
87
+
88
+ const showSuccessNotification = () => {
89
+ if (!successSubmitMessage) {
90
+ return
91
+ }
92
+
93
+ showSuccess(successSubmitMessage)
94
+ }
95
+
96
+ const showErrorNotification = (errors: SubmissionErrors) => {
97
+ if (typeof errors === 'string') {
98
+ showError(errors, undefined, { persist: true })
99
+
100
+ return
101
+ }
102
+
103
+ if (!failedSubmitMessage) {
104
+ return
105
+ }
106
+
107
+ showError(failedSubmitMessage, undefined, { persist: true })
108
+ }
109
+
110
+ const handleSubmit = async (
111
+ values: T,
112
+ form: FormApi<T>,
113
+ callback?: (errors?: SubmissionErrors) => void
114
+ ) => {
115
+ const validationErrors = getValidationErrors(
116
+ validationObject.current.getValidators(),
117
+ values,
118
+ form
119
+ )
120
+
121
+ if (validationErrors) {
122
+ return validationErrors
123
+ }
124
+
125
+ const submissionErrors = await onSubmit(values, form, callback)
126
+
127
+ if (!submissionErrors) {
128
+ showSuccessNotification()
129
+ } else {
130
+ showErrorNotification(submissionErrors)
131
+ }
132
+
133
+ return submissionErrors
134
+ }
135
+
136
+ return (
137
+ <FormContext.Provider value={validationObject}>
138
+ <FinalForm
139
+ render={({ handleSubmit }) => (
140
+ <Container>
141
+ <PicassoForm
142
+ autoComplete={autoComplete}
143
+ onSubmit={handleSubmit}
144
+ data-testid={dataTestId}
145
+ >
146
+ {children}
147
+ </PicassoForm>
148
+ </Container>
149
+ )}
150
+ onSubmit={handleSubmit}
151
+ decorators={[...decorators, scrollToErrorDecorator]}
152
+ {...rest}
153
+ />
154
+ </FormContext.Provider>
155
+ )
156
+ }
157
+
158
+ Form.defaultProps = {}
159
+
160
+ Form.displayName = 'Form'
161
+
162
+ Form.Autocomplete = Autocomplete
163
+ Form.Input = Input
164
+ Form.Select = Select
165
+ Form.Radio = Radio
166
+ Form.ButtonRadio = ButtonRadio
167
+ Form.RadioGroup = RadioGroup
168
+ Form.Checkbox = Checkbox
169
+ Form.ButtonCheckbox = ButtonCheckbox
170
+ Form.CheckboxGroup = CheckboxGroup
171
+ Form.NumberInput = NumberInput
172
+ Form.FileInput = FileInput
173
+ Form.DatePicker = DatePicker
174
+ Form.TimePicker = TimePicker
175
+ Form.TagSelector = TagSelector
176
+ Form.SubmitButton = SubmitButton
177
+ Form.ConfigProvider = FormConfigContext.Provider
178
+ Form.Switch = Switch
179
+ Form.Rating = Rating
180
+
181
+ export default Form
@@ -0,0 +1,38 @@
1
+ import { createContext, useContext, MutableRefObject } from 'react'
2
+ import { FieldValidator } from 'final-form'
3
+
4
+ export type Validators = Record<string, FieldValidator<unknown>>
5
+
6
+ export type FormContextProps = {
7
+ getValidators: () => Validators
8
+ setValidators: (fieldName: string, validator: FieldValidator<unknown>) => void
9
+ clearValidators: (fieldName: string) => void
10
+ }
11
+
12
+ export const createFormContext = (): FormContextProps => {
13
+ const validators: Validators = {}
14
+
15
+ return {
16
+ getValidators: () => validators,
17
+ setValidators: (fieldName, validator) => {
18
+ validators[fieldName] = validator
19
+ },
20
+ clearValidators: fieldName => {
21
+ delete validators[fieldName]
22
+ }
23
+ }
24
+ }
25
+
26
+ export const FormContext = createContext<MutableRefObject<
27
+ FormContextProps
28
+ > | null>(null)
29
+
30
+ export const useFormContext = () => {
31
+ const context = useContext(FormContext)
32
+
33
+ if (!context) {
34
+ throw new Error('Form Field cannot be rendered outside Form component')
35
+ }
36
+
37
+ return context.current
38
+ }