@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,85 @@
1
+ import React, { useCallback } from 'react'
2
+ import { useField } from 'react-final-form'
3
+ import { Container } from '@toptal/picasso'
4
+ import { Form } from '@toptal/picasso-forms'
5
+
6
+ type FormType = {
7
+ hide: boolean
8
+ name: {
9
+ first: string
10
+ last: string
11
+ }
12
+ dob: string
13
+ }
14
+
15
+ const FormContent = () => {
16
+ const {
17
+ input: { value: hide }
18
+ } = useField('hide')
19
+
20
+ return (
21
+ <>
22
+ <Form.Checkbox name='hide' label='Check to hide fields below' />
23
+
24
+ {!hide && (
25
+ <>
26
+ <Form.Input
27
+ enableReset
28
+ required
29
+ name='name.first'
30
+ label='Your first name'
31
+ placeholder='e.g. Bruce'
32
+ />
33
+ <Form.Input
34
+ enableReset
35
+ required
36
+ name='name.last'
37
+ label='Your last name'
38
+ placeholder='e.g. Wayne'
39
+ />
40
+ <Form.DatePicker required name='dob' label='DOB' />
41
+ </>
42
+ )}
43
+ </>
44
+ )
45
+ }
46
+
47
+ const Example = () => {
48
+ const handleSubmit = useCallback((values: FormType) => api.submit(values), [])
49
+
50
+ return (
51
+ <Form.ConfigProvider value={{ validateOnSubmit: true }}>
52
+ <Form<FormType>
53
+ onSubmit={handleSubmit}
54
+ successSubmitMessage='Success!'
55
+ failedSubmitMessage='Failure!'
56
+ >
57
+ <FormContent />
58
+
59
+ <Container top='small'>
60
+ <Form.SubmitButton>Submit</Form.SubmitButton>
61
+ </Container>
62
+ </Form>
63
+ </Form.ConfigProvider>
64
+ )
65
+ }
66
+
67
+ // the emulation of the api call
68
+ const responseWithDelay = async (response: any) =>
69
+ new Promise(resolve => setTimeout(() => resolve(response), 2000))
70
+
71
+ const api = {
72
+ submit: async (values: FormType) => {
73
+ if (values.hide || values.name?.first.toLowerCase() === 'bruce') {
74
+ return responseWithDelay(undefined)
75
+ }
76
+
77
+ return responseWithDelay({
78
+ name: {
79
+ first: 'Unknown first name'
80
+ }
81
+ })
82
+ }
83
+ }
84
+
85
+ export default Example
@@ -0,0 +1,203 @@
1
+ import Form from '../Form'
2
+ import formFieldStory from '../../FieldWrapper/story'
3
+ import PicassoBook from '~/.storybook/components/PicassoBook'
4
+
5
+ const page = PicassoBook.section('Picasso Forms').createPage('Form', 'Form')
6
+
7
+ page
8
+ .createTabChapter('Props')
9
+ .addComponentDocs({
10
+ component: Form,
11
+ name: 'Form',
12
+ additionalDocs: {
13
+ autoComplete: {
14
+ name: 'autoComplete',
15
+ type: {
16
+ name: 'string',
17
+ enums: ['on', 'off']
18
+ },
19
+ description: `HTML Form autocomplete attribute.\n
20
+ The autocomplete attribute specifies whether a form should have autocomplete 'on' or 'off'.
21
+ When autocomplete is 'on', the browser automatically complete values based on values that the user has entered before.\n
22
+ Tip: It is possible to have autocomplete 'on' for the form, and 'off' for specific input fields, or vice versa.`
23
+ },
24
+ debug: {
25
+ name: 'debug',
26
+ type: {
27
+ name: 'function',
28
+ description:
29
+ '(state: FormState, fieldStates: { [string]: FieldState }) => void'
30
+ },
31
+ description:
32
+ 'A callback for debugging that receives the form state and the states of all the fields'
33
+ },
34
+ decorators: {
35
+ name: 'decorators',
36
+ type: 'Decorator[]',
37
+ description: 'An array of decorators to apply to the form'
38
+ },
39
+ initialValues: {
40
+ name: 'initialValues',
41
+ type: 'FormValues | Object',
42
+ description: 'The initial values of the form'
43
+ },
44
+ initialValuesEqual: {
45
+ name: 'initialValuesEqual',
46
+ type: {
47
+ name: 'function',
48
+ description: '(Object | undefined, Object | undefined) => boolean'
49
+ },
50
+ description:
51
+ 'A predicate to determine whether or not the initialValues prop has changed'
52
+ },
53
+ keepDirtyOnReinitialize: {
54
+ name: 'keepDirtyOnReinitialize',
55
+ type: 'boolean',
56
+ description:
57
+ 'If true, only pristine values will be overwritten when initialize(newValues) is called'
58
+ },
59
+ mutators: {
60
+ name: 'mutators',
61
+ type: {
62
+ name: 'object',
63
+ description: '{ [string]: Mutator }'
64
+ },
65
+ description: 'Named mutator functions'
66
+ },
67
+ onSubmit: {
68
+ name: 'onSubmit',
69
+ type: {
70
+ name: 'function',
71
+ description:
72
+ '(values: FormValues, form: FormApi, callback: ?(errors: ?Object) => void) => ?Object | Promise<?Object> | void'
73
+ },
74
+ description: 'Function to call when the form is submitted',
75
+ required: true
76
+ },
77
+ subscription: {
78
+ name: 'subscription',
79
+ type: {
80
+ name: 'object',
81
+ description: '{ [string]: boolean }'
82
+ },
83
+ description:
84
+ 'An object of the parts of FormState (final-form) to subscribe to'
85
+ },
86
+ validate: {
87
+ name: 'validate',
88
+ type: {
89
+ name: 'function',
90
+ description: '(values: FormValues) => Object | Promise<Object>'
91
+ },
92
+ description:
93
+ 'A whole-record validation function that takes all the values of the form and returns any validation errors'
94
+ },
95
+ validateOnBlur: {
96
+ name: 'validateOnBlur',
97
+ type: 'boolean',
98
+ description:
99
+ 'If true, validation will happen on blur. If false, validation will happen on change',
100
+ defaultValue: false
101
+ },
102
+ successSubmitMessage: {
103
+ name: 'successSubmitMessage',
104
+ type: 'ReactNode',
105
+ description:
106
+ 'Message to display in a tooltip when form submitted successfully'
107
+ },
108
+ failedSubmitMessage: {
109
+ name: 'failedSubmitMessage',
110
+ type: 'ReactNode',
111
+ description:
112
+ 'Message to display in a tooltip when form submission failed'
113
+ },
114
+ scrollOffsetTop: {
115
+ name: 'scrollOffsetTop',
116
+ type: 'number',
117
+ description:
118
+ 'Offset from the viewport for inputs to focus on, defaults to the center of the window (deprecated, will not have any effect)'
119
+ }
120
+ }
121
+ })
122
+ .addComponentDocs(formFieldStory.componentDocs)
123
+
124
+ page
125
+ .createChapter()
126
+ .addTextSection(
127
+ `
128
+ Form is a wrapper for 'react-final-form' Form component. It also
129
+ provides inside all the necessary input components types.
130
+ `
131
+ )
132
+ .addExample(
133
+ 'Form/story/Default.example.tsx',
134
+ {
135
+ title: 'Default',
136
+ description: `
137
+ A general look of the form includes the examples of all the input
138
+ types supported by picasso-forms.
139
+ `
140
+ },
141
+ 'picasso-form'
142
+ )
143
+ .addExample(
144
+ 'Form/story/CustomValidator.example.tsx',
145
+ {
146
+ title: 'Custom validator',
147
+ description: `
148
+ We have a 'required' validator included by default to each input type,
149
+ however, you may need custom validators for more complex types of fields.
150
+ `
151
+ },
152
+ 'picasso-form'
153
+ ) // picasso-skip-visuals
154
+ .addExample(
155
+ 'Form/story/ParseInput.example.tsx',
156
+ {
157
+ title: 'Change form input value',
158
+ description: `
159
+ When you use picasso-forms your form input components are no longer
160
+ completely controlled and they are controlled by final-form, which
161
+ gives you the opportunity to rely on it with displaying errors,
162
+ validations, etc.
163
+
164
+ However, sometimes you may need to be able to modify the form input
165
+ value.
166
+ `
167
+ },
168
+ 'picasso-form'
169
+ ) // picasso-skip-visuals
170
+ .addExample(
171
+ 'Form/story/BackendCommunication.example.tsx',
172
+ {
173
+ title: 'Backend communication',
174
+ description: `
175
+ The form usually need to send data to backend, so we need to have
176
+ backend communication and display the process of submission and
177
+ the results. The form-level results are represented by notifications.
178
+ `
179
+ },
180
+ 'picasso-form'
181
+ ) // picasso-skip-visuals
182
+ .addExample(
183
+ 'Form/story/CustomFormLevelConfiguration.example.tsx',
184
+ 'Form Level Configurations'
185
+ )
186
+ .addExample(
187
+ 'Form/story/ValidateOnSubmit.example.tsx',
188
+ {
189
+ title: 'Validate only on submit',
190
+ description: `
191
+ All fields should not show any validation error messages until submission is made.
192
+ `
193
+ },
194
+ 'picasso-form'
195
+ ) // picasso-skip-visuals
196
+ .addExample('Form/story/FileInput.example.tsx', {
197
+ title: 'File input on a Form',
198
+ description: 'Showcase how to upload files on the form submission'
199
+ }) // picasso-skip-visuals
200
+ .addExample('Form/story/TitleCase.example.tsx', {
201
+ title: 'Title case',
202
+ description: "Display the field's label in title case."
203
+ }) // picasso-skip-visuals
@@ -0,0 +1,27 @@
1
+ import React from 'react'
2
+ import { render } from '@toptal/picasso/test-utils'
3
+ import { OmitInternalProps } from '@toptal/picasso-shared'
4
+ import { Button } from '@toptal/picasso'
5
+
6
+ import Form, { Props } from './Form'
7
+
8
+ const renderForm = (props: OmitInternalProps<Props>) => {
9
+ const { onSubmit } = props
10
+
11
+ return render(
12
+ <Form onSubmit={onSubmit}>
13
+ <Form.Input name='test' placeholder='test input' />
14
+ <Button type='submit'>Submit</Button>
15
+ </Form>
16
+ )
17
+ }
18
+
19
+ describe('Form', () => {
20
+ it('renders', () => {
21
+ const { container } = renderForm({
22
+ onSubmit: () => {}
23
+ })
24
+
25
+ expect(container).toMatchSnapshot()
26
+ })
27
+ })
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export type RequiredVariant = 'default' | 'asterisk'
4
+
5
+ export interface FormConfigProps {
6
+ validateOnSubmit?: boolean
7
+ requiredVariant?: RequiredVariant
8
+ }
9
+
10
+ export const FormConfigContext = createContext<FormConfigProps>({})
11
+
12
+ export const useFormConfig = () => useContext(FormConfigContext)
@@ -0,0 +1 @@
1
+ export * from './FormConfig'
@@ -0,0 +1,44 @@
1
+ import React from 'react'
2
+ import { screen, render, fireEvent } from '@toptal/picasso/test-utils'
3
+
4
+ import Form from '../Form'
5
+ import { FormConfigProps } from './FormConfig'
6
+
7
+ const TEXT_INPUT_LABEL = 'Test text field'
8
+
9
+ const renderForm = (configProps: FormConfigProps) => {
10
+ return render(
11
+ <Form.ConfigProvider value={configProps}>
12
+ <Form onSubmit={() => {}}>
13
+ <Form.Input label={TEXT_INPUT_LABEL} required name='test' />
14
+ <Form.SubmitButton>Submit</Form.SubmitButton>
15
+ </Form>
16
+ </Form.ConfigProvider>
17
+ )
18
+ }
19
+
20
+ describe('Form.ConfigProvider', () => {
21
+ it('validate only on submit', async () => {
22
+ renderForm({ validateOnSubmit: true })
23
+
24
+ fireEvent.blur(screen.getByLabelText(TEXT_INPUT_LABEL))
25
+
26
+ expect(
27
+ screen.queryByText('Please complete this field.')
28
+ ).not.toBeInTheDocument()
29
+
30
+ fireEvent.click(screen.getByRole('button', { name: 'Submit' }))
31
+
32
+ expect(
33
+ await screen.findByText('Please complete this field.')
34
+ ).toBeInTheDocument()
35
+ })
36
+
37
+ it('validate normally on blur / change', async () => {
38
+ renderForm({ validateOnSubmit: false })
39
+
40
+ fireEvent.blur(screen.getByLabelText(TEXT_INPUT_LABEL))
41
+
42
+ expect(screen.getByText('Please complete this field.')).toBeInTheDocument()
43
+ })
44
+ })
@@ -0,0 +1,27 @@
1
+ import React from 'react'
2
+ import { Input as PicassoInput, InputProps } from '@toptal/picasso'
3
+
4
+ import FieldWrapper, { FieldProps } from '../FieldWrapper'
5
+ import getInputName from './utils/get-input-name'
6
+
7
+ export type FormInputProps = Omit<InputProps, 'onResetClick'> & {
8
+ /** Callback invoked when reset button was clicked */
9
+ onResetClick?: (set: (value: string) => void) => void
10
+ }
11
+ export type Props = FormInputProps & FieldProps<InputProps['value']>
12
+
13
+ export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => (
14
+ <FieldWrapper<FormInputProps> {...props}>
15
+ {({ name, ...inputProps }: InputProps) => (
16
+ // TODO: remove getInputName completely when Chrome fixes autocomplete issue
17
+ // Link to the issue: https://bugs.chromium.org/p/chromium/issues/detail?id=1255609
18
+ <PicassoInput name={getInputName(name)} {...inputProps} ref={ref} />
19
+ )}
20
+ </FieldWrapper>
21
+ ))
22
+
23
+ Input.defaultProps = {}
24
+
25
+ Input.displayName = 'Input'
26
+
27
+ export default Input
@@ -0,0 +1 @@
1
+ export { default } from './Input'
@@ -0,0 +1,34 @@
1
+ import React from 'react'
2
+ import { render, fireEvent } from '@toptal/picasso/test-utils'
3
+ import { Button } from '@toptal/picasso'
4
+
5
+ import Form, { Props as FormProps } from '../Form/Form'
6
+ import { Props as InputProps } from './Input'
7
+
8
+ type TestFormProps = Pick<FormProps, 'onSubmit'> & Pick<InputProps, 'onFocus'>
9
+
10
+ const renderForm = (props: TestFormProps) => {
11
+ const { onFocus, onSubmit } = props
12
+
13
+ return render(
14
+ <Form onSubmit={onSubmit}>
15
+ <Form.Input onFocus={onFocus} name='test' placeholder='test input' />
16
+ <Button type='submit'>Submit</Button>
17
+ </Form>
18
+ )
19
+ }
20
+
21
+ describe('Input', () => {
22
+ it('fires the onFocus callback after focusing the input', () => {
23
+ const handleFocus = jest.fn()
24
+
25
+ const { getByPlaceholderText } = renderForm({
26
+ onSubmit: () => {},
27
+ onFocus: handleFocus
28
+ })
29
+
30
+ fireEvent.focus(getByPlaceholderText('test input'))
31
+
32
+ expect(handleFocus).toHaveBeenCalled()
33
+ })
34
+ })
@@ -0,0 +1,16 @@
1
+ import getInputName from './get-input-name'
2
+
3
+ describe('#getInputName', () => {
4
+ it.each(['title', 'field', 'input'] as const)(
5
+ 'adds postfix to "%s"',
6
+ name => {
7
+ expect(getInputName(name)).toBe(`${name}-picasso`)
8
+ }
9
+ )
10
+
11
+ it('returns all other names', () => {
12
+ expect(getInputName('name')).toBe('name')
13
+ expect(getInputName('email')).toBe('email')
14
+ expect(getInputName(undefined)).toBeUndefined()
15
+ })
16
+ })
@@ -0,0 +1,11 @@
1
+ const NAMES_NEED_POSTFIX = ['field', 'input', 'title']
2
+
3
+ const getInputName = (name?: string) => {
4
+ if (name && NAMES_NEED_POSTFIX.includes(name)) {
5
+ return `${name}-picasso`
6
+ }
7
+
8
+ return name
9
+ }
10
+
11
+ export default getInputName
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+ import {
3
+ NumberInput as PicassoNumberInput,
4
+ NumberInputProps
5
+ } from '@toptal/picasso'
6
+ import { FieldValidator } from 'final-form'
7
+
8
+ import { validators } from '../utils'
9
+ import FieldWrapper, { FieldProps } from '../FieldWrapper'
10
+
11
+ export type Props = NumberInputProps & FieldProps<NumberInputProps['value']>
12
+
13
+ const MIN = -2147483648
14
+ const MAX = 2147483647
15
+
16
+ const { composeValidators } = validators
17
+
18
+ export const NumberInput = (props: Props) => {
19
+ const { min = MIN, max = MAX, validate } = props
20
+ const validateNumberLimits: FieldValidator<NumberInputProps['value']> = value => {
21
+ if (Number(value) > max) {
22
+ return `Must be less than or equal to ${max}.`
23
+ }
24
+ if (Number(value) < min) {
25
+ return `Must be greater than or equal to ${min}.`
26
+ }
27
+ }
28
+
29
+ return (
30
+ <FieldWrapper<NumberInputProps>
31
+ {...props}
32
+ validate={composeValidators([validateNumberLimits, validate])}
33
+ >
34
+ {(inputProps: NumberInputProps) => {
35
+ return <PicassoNumberInput {...inputProps} />
36
+ }}
37
+ </FieldWrapper>
38
+ )
39
+ }
40
+
41
+ NumberInput.defaultProps = {}
42
+
43
+ NumberInput.displayName = 'NumberInput'
44
+
45
+ export default NumberInput
@@ -0,0 +1 @@
1
+ export { default } from './NumberInput'
@@ -0,0 +1,24 @@
1
+ import React, { useContext } from 'react'
2
+ import { Radio as PicassoRadio, RadioProps } from '@toptal/picasso'
3
+ import { Field } from 'react-final-form'
4
+
5
+ import { RadioGroupContext } from '../RadioGroup'
6
+
7
+ // Intersection with the type { name?: string } is needed here because of
8
+ // TS compiler issue https://github.com/microsoft/TypeScript/issues/34793
9
+ export type Props = RadioProps & {
10
+ name?: string
11
+ }
12
+
13
+ const Radio = ({ name, ...rest }: Props) => {
14
+ const groupName = useContext(RadioGroupContext)
15
+
16
+ return (
17
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
18
+ <Field name={name || groupName!} type='radio' value={rest.value}>
19
+ {({ input }) => <PicassoRadio checked={input.checked} {...rest} />}
20
+ </Field>
21
+ )
22
+ }
23
+
24
+ export default Radio