playbook_ui 18.2.0 → 18.3.0.pre.rc.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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_form/docs/_description_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_default_values.jsx +898 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_default_values_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_defaults.jsx +879 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_defaults_react.md +29 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_loading.jsx +75 -0
- data/app/pb_kits/playbook/pb_form/docs/{_form_loading.md → _form_loading_rails.md} +1 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_loading_react.md +1 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_model_values.jsx +147 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_model_values_react.md +16 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_multi_column.jsx +164 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_multi_column_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_validation.jsx +1057 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_validation_react.md +26 -0
- data/app/pb_kits/playbook/pb_form/docs/example.yml +7 -0
- data/app/pb_kits/playbook/pb_legend/_legend.tsx +3 -1
- data/app/pb_kits/playbook/pb_legend/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_progress_step/_progress_step_item.tsx +3 -1
- data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -1
- data/app/pb_kits/playbook/utilities/test/globalProps/onClick.test.js +90 -0
- data/dist/ai/all-schemas.json +1 -0
- data/dist/ai/forms.json +1 -1
- data/dist/ai/index.json +2 -2
- data/dist/ai/kits/legend.schema.json +1 -0
- data/dist/chunks/vendor.js +1 -1
- data/lib/playbook/version.rb +2 -2
- metadata +26 -10
- /data/app/pb_kits/playbook/pb_form/docs/{_description.md → _description_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_default_values.md → _form_default_values_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_defaults.md → _form_defaults_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_model_values.md → _form_model_values_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_multi_column.md → _form_multi_column_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_validation.md → _form_validation_rails.md} +0 -0
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react"
|
|
2
|
+
import { Controller, useForm } from "react-hook-form"
|
|
3
|
+
|
|
4
|
+
import Body from "../../pb_body/_body"
|
|
5
|
+
import Button from "../../pb_button/_button"
|
|
6
|
+
import Caption from "../../pb_caption/_caption"
|
|
7
|
+
import Card from "../../pb_card/_card"
|
|
8
|
+
import Checkbox from "../../pb_checkbox/_checkbox"
|
|
9
|
+
import DatePicker from "../../pb_date_picker/_date_picker"
|
|
10
|
+
import Dropdown from "../../pb_dropdown/_dropdown"
|
|
11
|
+
import Flex from "../../pb_flex/_flex"
|
|
12
|
+
import MultiLevelSelect from "../../pb_multi_level_select/_multi_level_select"
|
|
13
|
+
import PhoneNumberInput from "../../pb_phone_number_input/_phone_number_input"
|
|
14
|
+
import Pill from "../../pb_pill/_pill"
|
|
15
|
+
import Select from "../../pb_select/_select"
|
|
16
|
+
import StarRating from "../../pb_star_rating/_star_rating"
|
|
17
|
+
import Textarea from "../../pb_textarea/_textarea"
|
|
18
|
+
import TextInput from "../../pb_text_input/_text_input"
|
|
19
|
+
import TimePicker from "../../pb_time_picker/_time_picker"
|
|
20
|
+
import Typeahead from "../../pb_typeahead/_typeahead"
|
|
21
|
+
|
|
22
|
+
const INPUT_TYPES = [
|
|
23
|
+
{ key: "checkbox", label: "Checkbox" },
|
|
24
|
+
{ key: "date_picker", label: "Date Picker" },
|
|
25
|
+
{ key: "dropdown", label: "Dropdown: Default" },
|
|
26
|
+
{ key: "multi_select", label: "Dropdown: Multi Select" },
|
|
27
|
+
{ key: "multi_level_select", label: "Multi Level Select" },
|
|
28
|
+
{ key: "phone_number", label: "Phone Number" },
|
|
29
|
+
{ key: "select", label: "Select: Default" },
|
|
30
|
+
{ key: "star_rating", label: "Star Rating" },
|
|
31
|
+
{ key: "text_field", label: "Text Input: Default" },
|
|
32
|
+
{ key: "email_field", label: "Text Input: Email" },
|
|
33
|
+
{ key: "number_field", label: "Text Input: Number" },
|
|
34
|
+
{ key: "password_field", label: "Text Input: Password" },
|
|
35
|
+
{ key: "search_field", label: "Text Input: Search" },
|
|
36
|
+
{ key: "url_field", label: "Text Input: URL" },
|
|
37
|
+
{ key: "textarea", label: "Textarea" },
|
|
38
|
+
{ key: "time_picker", label: "Time Picker" },
|
|
39
|
+
{ key: "typeahead_default", label: "Typeahead: Default" },
|
|
40
|
+
{ key: "typeahead", label: "Typeahead: Multi" },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const DEFAULT_SELECTED_KEY = "text_field"
|
|
44
|
+
|
|
45
|
+
const COLOR_SELECT_OPTIONS = [
|
|
46
|
+
{ text: "Red", value: "red" },
|
|
47
|
+
{ text: "Blue", value: "blue" },
|
|
48
|
+
{ text: "Green", value: "green" },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
const COUNTRY_OPTIONS = [
|
|
52
|
+
{ id: "us-dv", label: "United States", value: "United States" },
|
|
53
|
+
{ id: "ca-dv", label: "Canada", value: "Canada" },
|
|
54
|
+
{ id: "pk-dv", label: "Pakistan", value: "Pakistan" },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const CANADA = COUNTRY_OPTIONS[1]
|
|
58
|
+
|
|
59
|
+
const TYPEAHEAD_COLOR_OPTIONS = [
|
|
60
|
+
{ label: "Orange", value: "#FFA500" },
|
|
61
|
+
{ label: "Red", value: "#FF0000" },
|
|
62
|
+
{ label: "Green", value: "#00FF00" },
|
|
63
|
+
{ label: "Blue", value: "#0000FF" },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const BLUE = TYPEAHEAD_COLOR_OPTIONS[3]
|
|
67
|
+
|
|
68
|
+
const DEPARTMENT_TREE_DATA = [
|
|
69
|
+
{
|
|
70
|
+
children: [
|
|
71
|
+
{
|
|
72
|
+
children: [
|
|
73
|
+
{ id: "default-values-102", label: "Talent Acquisition", value: "talentAcquisition" },
|
|
74
|
+
{ id: "default-values-103", label: "People Experience", value: "peopleExperience" },
|
|
75
|
+
],
|
|
76
|
+
id: "default-values-101",
|
|
77
|
+
label: "People",
|
|
78
|
+
value: "people",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
children: [
|
|
82
|
+
{ id: "default-values-105", label: "Customer Service", value: "customerService" },
|
|
83
|
+
{ id: "default-values-106", label: "Energy", value: "energy" },
|
|
84
|
+
],
|
|
85
|
+
id: "default-values-104",
|
|
86
|
+
label: "Contact Center",
|
|
87
|
+
value: "contactCenter",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
expanded: true,
|
|
91
|
+
id: "default-values-100",
|
|
92
|
+
label: "Power Home Remodeling",
|
|
93
|
+
value: "powerHomeRemodeling",
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
// Recursively locate a node by its `id` so onSubmit can translate stored ids
|
|
98
|
+
// back into the human-readable `value`s (matches FormDefaults behavior).
|
|
99
|
+
const findNodeById = (nodes, id) => {
|
|
100
|
+
for (const node of nodes) {
|
|
101
|
+
if (node.id === id) return node
|
|
102
|
+
if (node.children) {
|
|
103
|
+
const found = findNodeById(node.children, id)
|
|
104
|
+
if (found) return found
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// NOTE: `rating` is intentionally omitted. StarRating does not forward `rating`
|
|
111
|
+
// into StarRatingInteractive (which keeps its own useState(0)), so a prefilled
|
|
112
|
+
// rating would submit a value the control never displays. Re-add once the kit
|
|
113
|
+
// forwards the initial rating.
|
|
114
|
+
const DEFAULT_VALUES = {
|
|
115
|
+
bio: "Loves building with Playbook.",
|
|
116
|
+
countries: [CANADA],
|
|
117
|
+
country: CANADA,
|
|
118
|
+
department: ["default-values-102"],
|
|
119
|
+
email: "jane@example.com",
|
|
120
|
+
favoriteColor: "blue",
|
|
121
|
+
favoriteColorSearch: BLUE,
|
|
122
|
+
favoriteColors: [BLUE],
|
|
123
|
+
fullName: "John Doe",
|
|
124
|
+
newsletter: true,
|
|
125
|
+
password: "secret",
|
|
126
|
+
phone: "5551234567",
|
|
127
|
+
projectNumber: "33-12345",
|
|
128
|
+
quantity: 3,
|
|
129
|
+
startDate: "06/15/2026",
|
|
130
|
+
startTime: "09:30",
|
|
131
|
+
website: "https://example.com",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Map each field name to its toggle key so onSubmit can filter out fields
|
|
135
|
+
// whose pill has been deselected.
|
|
136
|
+
const FIELD_TO_INPUT_KEY = {
|
|
137
|
+
newsletter: "checkbox",
|
|
138
|
+
startDate: "date_picker",
|
|
139
|
+
country: "dropdown",
|
|
140
|
+
countries: "multi_select",
|
|
141
|
+
department: "multi_level_select",
|
|
142
|
+
phone: "phone_number",
|
|
143
|
+
favoriteColor: "select",
|
|
144
|
+
rating: "star_rating",
|
|
145
|
+
fullName: "text_field",
|
|
146
|
+
email: "email_field",
|
|
147
|
+
quantity: "number_field",
|
|
148
|
+
password: "password_field",
|
|
149
|
+
projectNumber: "search_field",
|
|
150
|
+
website: "url_field",
|
|
151
|
+
bio: "textarea",
|
|
152
|
+
startTime: "time_picker",
|
|
153
|
+
favoriteColorSearch: "typeahead_default",
|
|
154
|
+
favoriteColors: "typeahead",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Only interactive StarRating needs the reset: it keeps its own useState and
|
|
158
|
+
// ignores `rating`, so a stored rating would submit while the control looks
|
|
159
|
+
// empty after remount. Other toggle-remounted kits (TimePicker, DatePicker,
|
|
160
|
+
// Dropdown, Typeahead) rehydrate from their `defaultX` / `value` props.
|
|
161
|
+
const RESET_ON_DESELECT = {
|
|
162
|
+
star_rating: "rating",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const CODE_SNIPPETS = {
|
|
166
|
+
checkbox: `<Controller
|
|
167
|
+
control={control}
|
|
168
|
+
name="newsletter"
|
|
169
|
+
render={({ field }) => (
|
|
170
|
+
<Checkbox
|
|
171
|
+
checked={!!field.value}
|
|
172
|
+
onChange={(event) => field.onChange(event.target.checked)}
|
|
173
|
+
text="Subscribe to newsletter"
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
176
|
+
/>`,
|
|
177
|
+
date_picker: `<Controller
|
|
178
|
+
control={control}
|
|
179
|
+
name="startDate"
|
|
180
|
+
render={({ field }) => (
|
|
181
|
+
<DatePicker
|
|
182
|
+
allowInput
|
|
183
|
+
defaultDate={field.value}
|
|
184
|
+
initializeOnce
|
|
185
|
+
key={resetKey}
|
|
186
|
+
label="Start Date"
|
|
187
|
+
onChange={(dateStr) => field.onChange(dateStr)}
|
|
188
|
+
pickerId="form-default-values-start-date"
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
/>`,
|
|
192
|
+
dropdown: `<Controller
|
|
193
|
+
control={control}
|
|
194
|
+
name="country"
|
|
195
|
+
render={({ field }) => (
|
|
196
|
+
<Dropdown
|
|
197
|
+
key={resetKey}
|
|
198
|
+
label="Country"
|
|
199
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
200
|
+
options={countryOptions}
|
|
201
|
+
value={field.value}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
/>`,
|
|
205
|
+
email_field: `<Controller
|
|
206
|
+
control={control}
|
|
207
|
+
name="email"
|
|
208
|
+
render={({ field }) => (
|
|
209
|
+
<TextInput
|
|
210
|
+
label="Email"
|
|
211
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
212
|
+
type="email"
|
|
213
|
+
value={field.value}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
/>`,
|
|
217
|
+
multi_level_select: `<Controller
|
|
218
|
+
control={control}
|
|
219
|
+
name="department"
|
|
220
|
+
render={({ field }) => (
|
|
221
|
+
<MultiLevelSelect
|
|
222
|
+
id="mls-default-values"
|
|
223
|
+
label="Department"
|
|
224
|
+
onChange={(event) => field.onChange(event.target.value.map((item) => item.id))}
|
|
225
|
+
selectedIds={field.value}
|
|
226
|
+
treeData={departmentTreeData}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
/>`,
|
|
230
|
+
multi_select: `<Controller
|
|
231
|
+
control={control}
|
|
232
|
+
name="countries"
|
|
233
|
+
render={({ field }) => (
|
|
234
|
+
<Dropdown
|
|
235
|
+
key={resetKey}
|
|
236
|
+
label="Countries"
|
|
237
|
+
multiSelect
|
|
238
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
239
|
+
options={countryOptions}
|
|
240
|
+
value={field.value}
|
|
241
|
+
/>
|
|
242
|
+
)}
|
|
243
|
+
/>`,
|
|
244
|
+
number_field: `<Controller
|
|
245
|
+
control={control}
|
|
246
|
+
name="quantity"
|
|
247
|
+
render={({ field }) => (
|
|
248
|
+
<TextInput
|
|
249
|
+
label="Quantity"
|
|
250
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
251
|
+
type="number"
|
|
252
|
+
value={field.value}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
/>`,
|
|
256
|
+
password_field: `<Controller
|
|
257
|
+
control={control}
|
|
258
|
+
name="password"
|
|
259
|
+
render={({ field }) => (
|
|
260
|
+
<TextInput
|
|
261
|
+
label="Password"
|
|
262
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
263
|
+
type="password"
|
|
264
|
+
value={field.value}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
/>`,
|
|
268
|
+
phone_number: `<Controller
|
|
269
|
+
control={control}
|
|
270
|
+
name="phone"
|
|
271
|
+
render={({ field }) => (
|
|
272
|
+
<PhoneNumberInput
|
|
273
|
+
initialCountry="us"
|
|
274
|
+
key={resetKey}
|
|
275
|
+
label="Phone Number"
|
|
276
|
+
onChange={(data) => field.onChange(data?.number || "")}
|
|
277
|
+
value={field.value}
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
280
|
+
/>`,
|
|
281
|
+
search_field: `<Controller
|
|
282
|
+
control={control}
|
|
283
|
+
name="projectNumber"
|
|
284
|
+
render={({ field }) => (
|
|
285
|
+
<TextInput
|
|
286
|
+
label="Project Number"
|
|
287
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
288
|
+
type="search"
|
|
289
|
+
value={field.value}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
/>`,
|
|
293
|
+
select: `<Controller
|
|
294
|
+
control={control}
|
|
295
|
+
name="favoriteColor"
|
|
296
|
+
render={({ field }) => (
|
|
297
|
+
<Select
|
|
298
|
+
blankSelection="Select one..."
|
|
299
|
+
key={resetKey}
|
|
300
|
+
label="Favorite Color"
|
|
301
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
302
|
+
options={colorSelectOptions}
|
|
303
|
+
value={field.value}
|
|
304
|
+
/>
|
|
305
|
+
)}
|
|
306
|
+
/>`,
|
|
307
|
+
star_rating: `// NOTE: resetField("rating") runs from an effect when this pill is
|
|
308
|
+
// deselected, because interactive StarRating keeps its own state and ignores
|
|
309
|
+
// the rating prop on remount.
|
|
310
|
+
<Controller
|
|
311
|
+
control={control}
|
|
312
|
+
name="rating"
|
|
313
|
+
render={({ field }) => (
|
|
314
|
+
<StarRating
|
|
315
|
+
key={resetKey}
|
|
316
|
+
label="Rating"
|
|
317
|
+
onClick={(value) => field.onChange(value)}
|
|
318
|
+
rating={field.value || 0}
|
|
319
|
+
variant="interactive"
|
|
320
|
+
/>
|
|
321
|
+
)}
|
|
322
|
+
/>`,
|
|
323
|
+
text_field: `<Controller
|
|
324
|
+
control={control}
|
|
325
|
+
name="fullName"
|
|
326
|
+
render={({ field }) => (
|
|
327
|
+
<TextInput
|
|
328
|
+
label="Full Name"
|
|
329
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
330
|
+
value={field.value}
|
|
331
|
+
/>
|
|
332
|
+
)}
|
|
333
|
+
/>`,
|
|
334
|
+
textarea: `<Controller
|
|
335
|
+
control={control}
|
|
336
|
+
name="bio"
|
|
337
|
+
render={({ field }) => (
|
|
338
|
+
<Textarea
|
|
339
|
+
label="Bio"
|
|
340
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
341
|
+
value={field.value}
|
|
342
|
+
/>
|
|
343
|
+
)}
|
|
344
|
+
/>`,
|
|
345
|
+
time_picker: `// NOTE: TimePicker rehydrates from defaultTime={field.value} on remount, so
|
|
346
|
+
// no reset is needed when the pill is turned off and back on.
|
|
347
|
+
<Controller
|
|
348
|
+
control={control}
|
|
349
|
+
name="startTime"
|
|
350
|
+
render={({ field }) => (
|
|
351
|
+
<TimePicker
|
|
352
|
+
defaultTime={field.value}
|
|
353
|
+
key={resetKey}
|
|
354
|
+
label="Start Time"
|
|
355
|
+
onChange={(time) => field.onChange(time)}
|
|
356
|
+
/>
|
|
357
|
+
)}
|
|
358
|
+
/>`,
|
|
359
|
+
typeahead: `<Controller
|
|
360
|
+
control={control}
|
|
361
|
+
name="favoriteColors"
|
|
362
|
+
render={({ field: { onChange: handleChange, value } }) => (
|
|
363
|
+
<Typeahead
|
|
364
|
+
isMulti
|
|
365
|
+
key={resetKey}
|
|
366
|
+
label="Favorite Color"
|
|
367
|
+
onChange={handleChange}
|
|
368
|
+
options={typeaheadColorOptions}
|
|
369
|
+
pills
|
|
370
|
+
placeholder="Search for a color"
|
|
371
|
+
value={value}
|
|
372
|
+
/>
|
|
373
|
+
)}
|
|
374
|
+
/>`,
|
|
375
|
+
typeahead_default: `<Controller
|
|
376
|
+
control={control}
|
|
377
|
+
name="favoriteColorSearch"
|
|
378
|
+
render={({ field: { onChange: handleChange, value } }) => (
|
|
379
|
+
<Typeahead
|
|
380
|
+
key={resetKey}
|
|
381
|
+
label="Favorite Color"
|
|
382
|
+
onChange={handleChange}
|
|
383
|
+
options={typeaheadColorOptions}
|
|
384
|
+
placeholder="Search for a color"
|
|
385
|
+
value={value}
|
|
386
|
+
/>
|
|
387
|
+
)}
|
|
388
|
+
/>`,
|
|
389
|
+
url_field: `<Controller
|
|
390
|
+
control={control}
|
|
391
|
+
name="website"
|
|
392
|
+
render={({ field }) => (
|
|
393
|
+
<TextInput
|
|
394
|
+
label="Website"
|
|
395
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
396
|
+
type="url"
|
|
397
|
+
value={field.value}
|
|
398
|
+
/>
|
|
399
|
+
)}
|
|
400
|
+
/>`,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const FORM_OPEN = `const { control, handleSubmit } = useForm({
|
|
404
|
+
defaultValues: {
|
|
405
|
+
fullName: "John Doe",
|
|
406
|
+
email: "jane@example.com",
|
|
407
|
+
quantity: 3,
|
|
408
|
+
newsletter: true,
|
|
409
|
+
startDate: "06/15/2026",
|
|
410
|
+
},
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
<form onSubmit={handleSubmit(onSubmit)}>`
|
|
414
|
+
const FORM_CLOSE = ` <Button htmlType="submit" text="Submit" />
|
|
415
|
+
<Button htmlType="button" onClick={onReset} text="Cancel" variant="secondary" />
|
|
416
|
+
</form>`
|
|
417
|
+
|
|
418
|
+
const FormDefaultValues = () => {
|
|
419
|
+
const [selectedKeys, setSelectedKeys] = useState([DEFAULT_SELECTED_KEY])
|
|
420
|
+
const [submittedData, setSubmittedData] = useState(null)
|
|
421
|
+
const [resetKey, setResetKey] = useState(0)
|
|
422
|
+
const { control, handleSubmit, reset, resetField, getValues } = useForm({
|
|
423
|
+
defaultValues: DEFAULT_VALUES,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const isSelected = (key) => selectedKeys.includes(key)
|
|
427
|
+
|
|
428
|
+
// When a pill is turned off for a control that can't rehydrate on remount,
|
|
429
|
+
// clear its stored value so the next mount and the submitted payload agree.
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
Object.entries(RESET_ON_DESELECT).forEach(([inputKey, fieldName]) => {
|
|
432
|
+
const stillSelected = selectedKeys.includes(inputKey)
|
|
433
|
+
const hasValue = getValues(fieldName) !== undefined && getValues(fieldName) !== null
|
|
434
|
+
if (!stillSelected && hasValue) resetField(fieldName)
|
|
435
|
+
})
|
|
436
|
+
}, [selectedKeys, getValues, resetField])
|
|
437
|
+
|
|
438
|
+
const toggleKey = (key) => {
|
|
439
|
+
setSelectedKeys((current) => {
|
|
440
|
+
if (!current.includes(key)) return [...current, key]
|
|
441
|
+
return current.length > 1 ? current.filter((selected) => selected !== key) : current
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const handlePillKeyDown = (event, key) => {
|
|
446
|
+
if (event.key !== "Enter" && event.key !== " ") return
|
|
447
|
+
event.preventDefault()
|
|
448
|
+
toggleKey(key)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const onSubmit = (data) => {
|
|
452
|
+
// Dropdown stores the selected option node (or array of nodes for multi),
|
|
453
|
+
// and MultiLevelSelect stores selected ids. Translate each back into the
|
|
454
|
+
// human-readable `value`s so the submitted payload is primitives, not
|
|
455
|
+
// kit-internal shapes.
|
|
456
|
+
const normalized = {
|
|
457
|
+
...data,
|
|
458
|
+
country:
|
|
459
|
+
data.country && typeof data.country === "object"
|
|
460
|
+
? data.country.value ?? data.country.label
|
|
461
|
+
: data.country,
|
|
462
|
+
countries: Array.isArray(data.countries)
|
|
463
|
+
? data.countries.map((node) =>
|
|
464
|
+
node && typeof node === "object" ? node.value ?? node.label : node
|
|
465
|
+
)
|
|
466
|
+
: data.countries,
|
|
467
|
+
department: Array.isArray(data.department)
|
|
468
|
+
? data.department.map((id) => findNodeById(DEPARTMENT_TREE_DATA, id)?.value ?? id)
|
|
469
|
+
: data.department,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Fields that are deselected stay mounted (we removed shouldUnregister)
|
|
473
|
+
// so we filter them out here rather than letting RHF drop their values.
|
|
474
|
+
const filtered = Object.fromEntries(
|
|
475
|
+
Object.entries(normalized).filter(([fieldName]) => {
|
|
476
|
+
const inputKey = FIELD_TO_INPUT_KEY[fieldName]
|
|
477
|
+
return inputKey ? isSelected(inputKey) : true
|
|
478
|
+
})
|
|
479
|
+
)
|
|
480
|
+
setSubmittedData(filtered)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const onReset = () => {
|
|
484
|
+
reset()
|
|
485
|
+
setResetKey((current) => current + 1)
|
|
486
|
+
setSubmittedData(null)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const codePreview = [FORM_OPEN]
|
|
490
|
+
.concat(INPUT_TYPES.filter((inputType) => isSelected(inputType.key)).map((inputType) => CODE_SNIPPETS[inputType.key]))
|
|
491
|
+
.concat([FORM_CLOSE])
|
|
492
|
+
.join("\n\n")
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<div>
|
|
496
|
+
<Body color="light"
|
|
497
|
+
marginBottom="xs"
|
|
498
|
+
text="Select one or more inputs to add them to the form."
|
|
499
|
+
/>
|
|
500
|
+
<Flex gap="xs"
|
|
501
|
+
marginBottom="md"
|
|
502
|
+
wrap
|
|
503
|
+
>
|
|
504
|
+
{INPUT_TYPES.map((inputType) => (
|
|
505
|
+
<Pill
|
|
506
|
+
htmlOptions={{
|
|
507
|
+
onClick: () => toggleKey(inputType.key),
|
|
508
|
+
onKeyDown: (event) => handlePillKeyDown(event, inputType.key),
|
|
509
|
+
role: "button",
|
|
510
|
+
style: { cursor: "pointer" },
|
|
511
|
+
tabIndex: 0,
|
|
512
|
+
}}
|
|
513
|
+
key={inputType.key}
|
|
514
|
+
text={inputType.label}
|
|
515
|
+
textTransform="none"
|
|
516
|
+
variant={isSelected(inputType.key) ? "primary" : "neutral"}
|
|
517
|
+
/>
|
|
518
|
+
))}
|
|
519
|
+
</Flex>
|
|
520
|
+
|
|
521
|
+
<Flex align="stretch"
|
|
522
|
+
gap="md"
|
|
523
|
+
wrap
|
|
524
|
+
>
|
|
525
|
+
<Flex
|
|
526
|
+
align="stretch"
|
|
527
|
+
htmlOptions={{ style: { flex: "2 1" } }}
|
|
528
|
+
orientation="column"
|
|
529
|
+
>
|
|
530
|
+
<Card
|
|
531
|
+
htmlOptions={{ style: { minWidth: 0 } }}
|
|
532
|
+
padding="md"
|
|
533
|
+
shadow="deep"
|
|
534
|
+
>
|
|
535
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
536
|
+
{isSelected("checkbox") && (
|
|
537
|
+
<Controller
|
|
538
|
+
control={control}
|
|
539
|
+
name="newsletter"
|
|
540
|
+
render={({ field }) => (
|
|
541
|
+
<Checkbox
|
|
542
|
+
checked={!!field.value}
|
|
543
|
+
marginBottom="sm"
|
|
544
|
+
onChange={(event) => field.onChange(event.target.checked)}
|
|
545
|
+
text="Subscribe to newsletter"
|
|
546
|
+
/>
|
|
547
|
+
)}
|
|
548
|
+
/>
|
|
549
|
+
)}
|
|
550
|
+
|
|
551
|
+
{isSelected("date_picker") && (
|
|
552
|
+
<Controller
|
|
553
|
+
control={control}
|
|
554
|
+
name="startDate"
|
|
555
|
+
render={({ field }) => (
|
|
556
|
+
<DatePicker
|
|
557
|
+
allowInput
|
|
558
|
+
defaultDate={field.value}
|
|
559
|
+
initializeOnce
|
|
560
|
+
key={resetKey}
|
|
561
|
+
label="Start Date"
|
|
562
|
+
marginBottom="sm"
|
|
563
|
+
onChange={(dateStr) => field.onChange(dateStr)}
|
|
564
|
+
pickerId="form-default-values-start-date"
|
|
565
|
+
/>
|
|
566
|
+
)}
|
|
567
|
+
/>
|
|
568
|
+
)}
|
|
569
|
+
|
|
570
|
+
{isSelected("dropdown") && (
|
|
571
|
+
<Controller
|
|
572
|
+
control={control}
|
|
573
|
+
name="country"
|
|
574
|
+
render={({ field }) => (
|
|
575
|
+
<Dropdown
|
|
576
|
+
key={`dropdown-${resetKey}`}
|
|
577
|
+
label="Country"
|
|
578
|
+
marginBottom="sm"
|
|
579
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
580
|
+
options={COUNTRY_OPTIONS}
|
|
581
|
+
value={field.value}
|
|
582
|
+
/>
|
|
583
|
+
)}
|
|
584
|
+
/>
|
|
585
|
+
)}
|
|
586
|
+
|
|
587
|
+
{isSelected("multi_select") && (
|
|
588
|
+
<Controller
|
|
589
|
+
control={control}
|
|
590
|
+
name="countries"
|
|
591
|
+
render={({ field }) => (
|
|
592
|
+
<Dropdown
|
|
593
|
+
key={`multi-select-${resetKey}`}
|
|
594
|
+
label="Countries"
|
|
595
|
+
marginBottom="sm"
|
|
596
|
+
multiSelect
|
|
597
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
598
|
+
options={COUNTRY_OPTIONS}
|
|
599
|
+
value={field.value}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
602
|
+
/>
|
|
603
|
+
)}
|
|
604
|
+
|
|
605
|
+
{isSelected("multi_level_select") && (
|
|
606
|
+
<Controller
|
|
607
|
+
control={control}
|
|
608
|
+
name="department"
|
|
609
|
+
render={({ field }) => (
|
|
610
|
+
<MultiLevelSelect
|
|
611
|
+
id="mls-default-values"
|
|
612
|
+
label="Department"
|
|
613
|
+
marginBottom="sm"
|
|
614
|
+
onChange={(event) => field.onChange(event.target.value.map((item) => item.id))}
|
|
615
|
+
selectedIds={field.value}
|
|
616
|
+
treeData={DEPARTMENT_TREE_DATA}
|
|
617
|
+
/>
|
|
618
|
+
)}
|
|
619
|
+
/>
|
|
620
|
+
)}
|
|
621
|
+
|
|
622
|
+
{isSelected("phone_number") && (
|
|
623
|
+
<Controller
|
|
624
|
+
control={control}
|
|
625
|
+
name="phone"
|
|
626
|
+
render={({ field }) => (
|
|
627
|
+
<PhoneNumberInput
|
|
628
|
+
initialCountry="us"
|
|
629
|
+
key={`phone-${resetKey}`}
|
|
630
|
+
label="Phone Number"
|
|
631
|
+
marginBottom="sm"
|
|
632
|
+
onChange={(data) => field.onChange(data?.number || "")}
|
|
633
|
+
value={field.value}
|
|
634
|
+
/>
|
|
635
|
+
)}
|
|
636
|
+
/>
|
|
637
|
+
)}
|
|
638
|
+
|
|
639
|
+
{isSelected("select") && (
|
|
640
|
+
<Controller
|
|
641
|
+
control={control}
|
|
642
|
+
name="favoriteColor"
|
|
643
|
+
render={({ field }) => (
|
|
644
|
+
<Select
|
|
645
|
+
blankSelection="Select one..."
|
|
646
|
+
key={`select-${resetKey}`}
|
|
647
|
+
label="Favorite Color"
|
|
648
|
+
marginBottom="sm"
|
|
649
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
650
|
+
options={COLOR_SELECT_OPTIONS}
|
|
651
|
+
value={field.value}
|
|
652
|
+
/>
|
|
653
|
+
)}
|
|
654
|
+
/>
|
|
655
|
+
)}
|
|
656
|
+
|
|
657
|
+
{isSelected("star_rating") && (
|
|
658
|
+
<Controller
|
|
659
|
+
control={control}
|
|
660
|
+
name="rating"
|
|
661
|
+
render={({ field }) => (
|
|
662
|
+
<StarRating
|
|
663
|
+
key={`rating-${resetKey}`}
|
|
664
|
+
label="Rating"
|
|
665
|
+
marginBottom="sm"
|
|
666
|
+
onClick={(value) => field.onChange(value)}
|
|
667
|
+
rating={field.value || 0}
|
|
668
|
+
variant="interactive"
|
|
669
|
+
/>
|
|
670
|
+
)}
|
|
671
|
+
/>
|
|
672
|
+
)}
|
|
673
|
+
|
|
674
|
+
{isSelected("text_field") && (
|
|
675
|
+
<Controller
|
|
676
|
+
control={control}
|
|
677
|
+
name="fullName"
|
|
678
|
+
render={({ field }) => (
|
|
679
|
+
<TextInput
|
|
680
|
+
label="Full Name"
|
|
681
|
+
marginBottom="sm"
|
|
682
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
683
|
+
value={field.value}
|
|
684
|
+
/>
|
|
685
|
+
)}
|
|
686
|
+
/>
|
|
687
|
+
)}
|
|
688
|
+
|
|
689
|
+
{isSelected("email_field") && (
|
|
690
|
+
<Controller
|
|
691
|
+
control={control}
|
|
692
|
+
name="email"
|
|
693
|
+
render={({ field }) => (
|
|
694
|
+
<TextInput
|
|
695
|
+
label="Email"
|
|
696
|
+
marginBottom="sm"
|
|
697
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
698
|
+
type="email"
|
|
699
|
+
value={field.value}
|
|
700
|
+
/>
|
|
701
|
+
)}
|
|
702
|
+
/>
|
|
703
|
+
)}
|
|
704
|
+
|
|
705
|
+
{isSelected("number_field") && (
|
|
706
|
+
<Controller
|
|
707
|
+
control={control}
|
|
708
|
+
name="quantity"
|
|
709
|
+
render={({ field }) => (
|
|
710
|
+
<TextInput
|
|
711
|
+
label="Quantity"
|
|
712
|
+
marginBottom="sm"
|
|
713
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
714
|
+
type="number"
|
|
715
|
+
value={field.value}
|
|
716
|
+
/>
|
|
717
|
+
)}
|
|
718
|
+
/>
|
|
719
|
+
)}
|
|
720
|
+
|
|
721
|
+
{isSelected("password_field") && (
|
|
722
|
+
<Controller
|
|
723
|
+
control={control}
|
|
724
|
+
name="password"
|
|
725
|
+
render={({ field }) => (
|
|
726
|
+
<TextInput
|
|
727
|
+
label="Password"
|
|
728
|
+
marginBottom="sm"
|
|
729
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
730
|
+
type="password"
|
|
731
|
+
value={field.value}
|
|
732
|
+
/>
|
|
733
|
+
)}
|
|
734
|
+
/>
|
|
735
|
+
)}
|
|
736
|
+
|
|
737
|
+
{isSelected("search_field") && (
|
|
738
|
+
<Controller
|
|
739
|
+
control={control}
|
|
740
|
+
name="projectNumber"
|
|
741
|
+
render={({ field }) => (
|
|
742
|
+
<TextInput
|
|
743
|
+
label="Project Number"
|
|
744
|
+
marginBottom="sm"
|
|
745
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
746
|
+
type="search"
|
|
747
|
+
value={field.value}
|
|
748
|
+
/>
|
|
749
|
+
)}
|
|
750
|
+
/>
|
|
751
|
+
)}
|
|
752
|
+
|
|
753
|
+
{isSelected("url_field") && (
|
|
754
|
+
<Controller
|
|
755
|
+
control={control}
|
|
756
|
+
name="website"
|
|
757
|
+
render={({ field }) => (
|
|
758
|
+
<TextInput
|
|
759
|
+
label="Website"
|
|
760
|
+
marginBottom="sm"
|
|
761
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
762
|
+
type="url"
|
|
763
|
+
value={field.value}
|
|
764
|
+
/>
|
|
765
|
+
)}
|
|
766
|
+
/>
|
|
767
|
+
)}
|
|
768
|
+
|
|
769
|
+
{isSelected("textarea") && (
|
|
770
|
+
<Controller
|
|
771
|
+
control={control}
|
|
772
|
+
name="bio"
|
|
773
|
+
render={({ field }) => (
|
|
774
|
+
<Textarea
|
|
775
|
+
label="Bio"
|
|
776
|
+
marginBottom="sm"
|
|
777
|
+
onChange={(event) => field.onChange(event.target.value)}
|
|
778
|
+
value={field.value}
|
|
779
|
+
/>
|
|
780
|
+
)}
|
|
781
|
+
/>
|
|
782
|
+
)}
|
|
783
|
+
|
|
784
|
+
{isSelected("time_picker") && (
|
|
785
|
+
<Controller
|
|
786
|
+
control={control}
|
|
787
|
+
name="startTime"
|
|
788
|
+
render={({ field }) => (
|
|
789
|
+
<TimePicker
|
|
790
|
+
defaultTime={field.value}
|
|
791
|
+
key={`time-${resetKey}`}
|
|
792
|
+
label="Start Time"
|
|
793
|
+
marginBottom="sm"
|
|
794
|
+
onChange={(time) => field.onChange(time)}
|
|
795
|
+
/>
|
|
796
|
+
)}
|
|
797
|
+
/>
|
|
798
|
+
)}
|
|
799
|
+
|
|
800
|
+
{isSelected("typeahead_default") && (
|
|
801
|
+
<Controller
|
|
802
|
+
control={control}
|
|
803
|
+
name="favoriteColorSearch"
|
|
804
|
+
render={({ field: { onChange: handleChange, value } }) => (
|
|
805
|
+
<Typeahead
|
|
806
|
+
key={`typeahead-default-${resetKey}`}
|
|
807
|
+
label="Favorite Color"
|
|
808
|
+
marginBottom="sm"
|
|
809
|
+
onChange={handleChange}
|
|
810
|
+
options={TYPEAHEAD_COLOR_OPTIONS}
|
|
811
|
+
placeholder="Search for a color"
|
|
812
|
+
value={value}
|
|
813
|
+
/>
|
|
814
|
+
)}
|
|
815
|
+
/>
|
|
816
|
+
)}
|
|
817
|
+
|
|
818
|
+
{isSelected("typeahead") && (
|
|
819
|
+
<Controller
|
|
820
|
+
control={control}
|
|
821
|
+
name="favoriteColors"
|
|
822
|
+
render={({ field: { onChange: handleChange, value } }) => (
|
|
823
|
+
<Typeahead
|
|
824
|
+
isMulti
|
|
825
|
+
key={`typeahead-multi-${resetKey}`}
|
|
826
|
+
label="Favorite Color"
|
|
827
|
+
marginBottom="sm"
|
|
828
|
+
onChange={handleChange}
|
|
829
|
+
options={TYPEAHEAD_COLOR_OPTIONS}
|
|
830
|
+
pills
|
|
831
|
+
placeholder="Search for a color"
|
|
832
|
+
value={value}
|
|
833
|
+
/>
|
|
834
|
+
)}
|
|
835
|
+
/>
|
|
836
|
+
)}
|
|
837
|
+
|
|
838
|
+
<Flex gap="xs"
|
|
839
|
+
marginTop="sm"
|
|
840
|
+
>
|
|
841
|
+
<Button htmlType="submit"
|
|
842
|
+
text="Submit"
|
|
843
|
+
/>
|
|
844
|
+
<Button
|
|
845
|
+
htmlType="button"
|
|
846
|
+
onClick={onReset}
|
|
847
|
+
text="Cancel"
|
|
848
|
+
variant="secondary"
|
|
849
|
+
/>
|
|
850
|
+
</Flex>
|
|
851
|
+
</form>
|
|
852
|
+
</Card>
|
|
853
|
+
</Flex>
|
|
854
|
+
|
|
855
|
+
<Flex
|
|
856
|
+
align="stretch"
|
|
857
|
+
htmlOptions={{ style: { flex: "1 1" } }}
|
|
858
|
+
orientation="column"
|
|
859
|
+
>
|
|
860
|
+
<Caption marginBottom="xs"
|
|
861
|
+
text="Submitted Form Data"
|
|
862
|
+
/>
|
|
863
|
+
<Card background="light"
|
|
864
|
+
padding="md"
|
|
865
|
+
>
|
|
866
|
+
{submittedData ? (
|
|
867
|
+
<pre style={{ fontSize: "12px", lineHeight: 1.5, margin: 0, overflow: "auto", whiteSpace: "pre-wrap" }}>
|
|
868
|
+
{JSON.stringify(submittedData, null, 2)}
|
|
869
|
+
</pre>
|
|
870
|
+
) : (
|
|
871
|
+
<Body color="light"
|
|
872
|
+
text="No data submitted yet."
|
|
873
|
+
/>
|
|
874
|
+
)}
|
|
875
|
+
</Card>
|
|
876
|
+
</Flex>
|
|
877
|
+
</Flex>
|
|
878
|
+
|
|
879
|
+
<Body
|
|
880
|
+
color="light"
|
|
881
|
+
marginBottom="sm"
|
|
882
|
+
marginTop="sm"
|
|
883
|
+
text="Submit the form to see the data on the right."
|
|
884
|
+
/>
|
|
885
|
+
|
|
886
|
+
<Card background="dark"
|
|
887
|
+
dark
|
|
888
|
+
padding="none"
|
|
889
|
+
>
|
|
890
|
+
<pre style={{ fontSize: "12px", lineHeight: 1.5, margin: 0, overflow: "auto", padding: "16px", whiteSpace: "pre" }}>
|
|
891
|
+
{codePreview}
|
|
892
|
+
</pre>
|
|
893
|
+
</Card>
|
|
894
|
+
</div>
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
export default FormDefaultValues
|