@khanacademy/wonder-blocks-form 2.4.4 → 2.4.7
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.
- package/CHANGELOG.md +21 -0
- package/dist/index.js +150 -0
- package/package.json +5 -5
- package/src/components/__docs__/checkbox-group.stories.js +266 -0
- package/src/components/__docs__/checkbox.stories.js +1 -1
- package/src/components/__docs__/choice.stories.js +86 -0
- package/src/components/__docs__/labeled-text-field.argtypes.js +248 -0
- package/src/components/{labeled-text-field.stories.js → __docs__/labeled-text-field.stories.js} +274 -35
- package/src/components/__docs__/radio-group.stories.js +182 -0
- package/src/components/__docs__/radio.stories.js +160 -0
- package/src/components/__docs__/text-field.argtypes.js +206 -0
- package/src/components/{text-field.stories.js → __docs__/text-field.stories.js} +268 -28
- package/src/components/checkbox-group.js +27 -0
- package/src/components/choice.js +59 -1
- package/src/components/labeled-text-field.js +20 -0
- package/src/components/radio-group.js +27 -0
- package/src/components/text-field.js +17 -0
package/src/components/{labeled-text-field.stories.js → __docs__/labeled-text-field.stories.js}
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import {StyleSheet} from "aphrodite";
|
|
3
4
|
|
|
4
5
|
import {LabeledTextField} from "@khanacademy/wonder-blocks-form";
|
|
5
6
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
@@ -8,12 +9,46 @@ import Color from "@khanacademy/wonder-blocks-color";
|
|
|
8
9
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
9
10
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
10
11
|
import Button from "@khanacademy/wonder-blocks-button";
|
|
11
|
-
import {StyleSheet} from "aphrodite";
|
|
12
12
|
|
|
13
13
|
import type {StoryComponentType} from "@storybook/react";
|
|
14
14
|
|
|
15
|
+
import ComponentInfo from "../../../../../.storybook/components/component-info.js";
|
|
16
|
+
import {name, version} from "../../../package.json";
|
|
17
|
+
import LabeledTextFieldArgTypes from "./labeled-text-field.argtypes.js";
|
|
18
|
+
|
|
15
19
|
export default {
|
|
16
20
|
title: "Form / LabeledTextField",
|
|
21
|
+
component: LabeledTextField,
|
|
22
|
+
parameters: {
|
|
23
|
+
componentSubtitle: ((
|
|
24
|
+
<ComponentInfo name={name} version={version} />
|
|
25
|
+
): any),
|
|
26
|
+
},
|
|
27
|
+
argTypes: LabeledTextFieldArgTypes,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Default: StoryComponentType = (args) => {
|
|
31
|
+
return <LabeledTextField {...args} />;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
Default.args = {
|
|
35
|
+
id: "some-ltf-id",
|
|
36
|
+
type: "text",
|
|
37
|
+
label: "Label",
|
|
38
|
+
description: "Hello, this is the description for this field",
|
|
39
|
+
value: "",
|
|
40
|
+
disabled: false,
|
|
41
|
+
required: false,
|
|
42
|
+
light: false,
|
|
43
|
+
placeholder: "Placeholder",
|
|
44
|
+
readOnly: false,
|
|
45
|
+
autoComplete: "off",
|
|
46
|
+
validate: () => {},
|
|
47
|
+
onValidate: () => {},
|
|
48
|
+
onChange: () => {},
|
|
49
|
+
onKeyDown: () => {},
|
|
50
|
+
onFocus: () => {},
|
|
51
|
+
onBlur: () => {},
|
|
17
52
|
};
|
|
18
53
|
|
|
19
54
|
export const Text: StoryComponentType = () => {
|
|
@@ -37,6 +72,13 @@ export const Text: StoryComponentType = () => {
|
|
|
37
72
|
);
|
|
38
73
|
};
|
|
39
74
|
|
|
75
|
+
Text.parameters = {
|
|
76
|
+
docs: {
|
|
77
|
+
storyDescription:
|
|
78
|
+
"An input field with type `text` takes all kinds of characters.",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
40
82
|
export const RequiredWithDefaultText: StoryComponentType = () => {
|
|
41
83
|
const [value, setValue] = React.useState("");
|
|
42
84
|
|
|
@@ -51,13 +93,29 @@ export const RequiredWithDefaultText: StoryComponentType = () => {
|
|
|
51
93
|
label="Name"
|
|
52
94
|
description="Please enter your name"
|
|
53
95
|
value={value}
|
|
54
|
-
onChange={
|
|
96
|
+
onChange={setValue}
|
|
55
97
|
onKeyDown={handleKeyDown}
|
|
56
98
|
required={true}
|
|
57
99
|
/>
|
|
58
100
|
);
|
|
59
101
|
};
|
|
60
102
|
|
|
103
|
+
RequiredWithDefaultText.parameters = {
|
|
104
|
+
docs: {
|
|
105
|
+
storyDescription: `A required field will show the message
|
|
106
|
+
"This field is required." by default if someone types in it
|
|
107
|
+
at some point but leaves it blank. Type in the field, then
|
|
108
|
+
backspace all the way and click out of the field to see
|
|
109
|
+
this message. Note that this message would not appear if
|
|
110
|
+
the \`validation\` prop were set.`,
|
|
111
|
+
},
|
|
112
|
+
chromatic: {
|
|
113
|
+
// Disabling snapshot because it doesn't show the error message
|
|
114
|
+
// until after the user interacts with this field.
|
|
115
|
+
disableSnapshot: true,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
61
119
|
export const RequiredWithSpecifiedText: StoryComponentType = () => {
|
|
62
120
|
const [value, setValue] = React.useState("");
|
|
63
121
|
|
|
@@ -72,7 +130,7 @@ export const RequiredWithSpecifiedText: StoryComponentType = () => {
|
|
|
72
130
|
label="Name"
|
|
73
131
|
description="Please enter your name"
|
|
74
132
|
value={value}
|
|
75
|
-
onChange={
|
|
133
|
+
onChange={setValue}
|
|
76
134
|
onKeyDown={handleKeyDown}
|
|
77
135
|
required="This specific field is super required."
|
|
78
136
|
/>
|
|
@@ -80,8 +138,16 @@ export const RequiredWithSpecifiedText: StoryComponentType = () => {
|
|
|
80
138
|
};
|
|
81
139
|
|
|
82
140
|
RequiredWithSpecifiedText.parameters = {
|
|
141
|
+
docs: {
|
|
142
|
+
storyDescription: `If a string is passed into the \`required\` prop,
|
|
143
|
+
the specified message will show when the field is left blank.
|
|
144
|
+
Type in the field, then backspace all the way and click out of
|
|
145
|
+
the field to see this message. Note that this message would not
|
|
146
|
+
appear if the \`validation\` prop were set.`,
|
|
147
|
+
},
|
|
83
148
|
chromatic: {
|
|
84
|
-
//
|
|
149
|
+
// Disabling snapshot because it doesn't show the error message
|
|
150
|
+
// until after the user interacts with this field.
|
|
85
151
|
disableSnapshot: true,
|
|
86
152
|
},
|
|
87
153
|
};
|
|
@@ -101,15 +167,22 @@ export const Number: StoryComponentType = () => {
|
|
|
101
167
|
type="number"
|
|
102
168
|
description="Please enter your age"
|
|
103
169
|
value={value}
|
|
104
|
-
onChange={
|
|
170
|
+
onChange={setValue}
|
|
105
171
|
placeholder="Age"
|
|
106
172
|
onKeyDown={handleKeyDown}
|
|
107
173
|
/>
|
|
108
174
|
);
|
|
109
175
|
};
|
|
110
176
|
|
|
177
|
+
Number.parameters = {
|
|
178
|
+
docs: {
|
|
179
|
+
storyDescription:
|
|
180
|
+
"An input field with type `number` will only take numeric characters as input.",
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
111
184
|
export const Password: StoryComponentType = () => {
|
|
112
|
-
const [value, setValue] = React.useState("
|
|
185
|
+
const [value, setValue] = React.useState("$ecure123");
|
|
113
186
|
|
|
114
187
|
const validate = (value: string) => {
|
|
115
188
|
if (value.length < 8) {
|
|
@@ -132,7 +205,7 @@ export const Password: StoryComponentType = () => {
|
|
|
132
205
|
type="password"
|
|
133
206
|
description="Please enter a secure password"
|
|
134
207
|
value={value}
|
|
135
|
-
onChange={
|
|
208
|
+
onChange={setValue}
|
|
136
209
|
placeholder="Password"
|
|
137
210
|
validate={validate}
|
|
138
211
|
onKeyDown={handleKeyDown}
|
|
@@ -140,6 +213,15 @@ export const Password: StoryComponentType = () => {
|
|
|
140
213
|
);
|
|
141
214
|
};
|
|
142
215
|
|
|
216
|
+
Password.parameters = {
|
|
217
|
+
docs: {
|
|
218
|
+
storyDescription: `An input field with type \`password\` will
|
|
219
|
+
obscure the input value. It also often contains validation.
|
|
220
|
+
In this example, the password must be over 8 characters long and
|
|
221
|
+
must contain a numeric value.`,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
143
225
|
export const Email: StoryComponentType = () => {
|
|
144
226
|
const [value, setValue] = React.useState("khan@khan.org");
|
|
145
227
|
|
|
@@ -161,7 +243,7 @@ export const Email: StoryComponentType = () => {
|
|
|
161
243
|
label="Email"
|
|
162
244
|
type="email"
|
|
163
245
|
value={value}
|
|
164
|
-
onChange={
|
|
246
|
+
onChange={setValue}
|
|
165
247
|
description="Please provide your personal email"
|
|
166
248
|
placeholder="Email"
|
|
167
249
|
validate={validate}
|
|
@@ -170,6 +252,15 @@ export const Email: StoryComponentType = () => {
|
|
|
170
252
|
);
|
|
171
253
|
};
|
|
172
254
|
|
|
255
|
+
Email.parameters = {
|
|
256
|
+
docs: {
|
|
257
|
+
storyDescription: `An input field with type \`email\` will automatically
|
|
258
|
+
validate an input on submit to ensure it's either formatted properly
|
|
259
|
+
or blank. \`TextField\` will run validation on blur if the
|
|
260
|
+
\`validate\` prop is passed in, as in this example.`,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
173
264
|
export const EmailRequired: StoryComponentType = () => {
|
|
174
265
|
const [value, setValue] = React.useState("");
|
|
175
266
|
|
|
@@ -190,7 +281,7 @@ export const EmailRequired: StoryComponentType = () => {
|
|
|
190
281
|
<LabeledTextField
|
|
191
282
|
label="Email"
|
|
192
283
|
type="email"
|
|
193
|
-
onChange={
|
|
284
|
+
onChange={setValue}
|
|
194
285
|
description="Please provide your personal email"
|
|
195
286
|
value={value}
|
|
196
287
|
validate={validate}
|
|
@@ -201,6 +292,13 @@ export const EmailRequired: StoryComponentType = () => {
|
|
|
201
292
|
};
|
|
202
293
|
|
|
203
294
|
EmailRequired.parameters = {
|
|
295
|
+
docs: {
|
|
296
|
+
storyDescription: `An example of a required field that also has
|
|
297
|
+
a \`validation\` prop passed in. \`required\` can be a boolean or
|
|
298
|
+
a string. In this case, \`required\` is set to \`true\` since a
|
|
299
|
+
string would not even be used if it were passed in because the
|
|
300
|
+
validation overrides it.`,
|
|
301
|
+
},
|
|
204
302
|
chromatic: {
|
|
205
303
|
// We have screenshots of other stories that cover this case.
|
|
206
304
|
disableSnapshot: true,
|
|
@@ -228,7 +326,7 @@ export const Telephone: StoryComponentType = () => {
|
|
|
228
326
|
label="Telephone"
|
|
229
327
|
type="tel"
|
|
230
328
|
value={value}
|
|
231
|
-
onChange={
|
|
329
|
+
onChange={setValue}
|
|
232
330
|
description="Please provide your personal phone number"
|
|
233
331
|
placeholder="Telephone"
|
|
234
332
|
validate={validate}
|
|
@@ -237,6 +335,15 @@ export const Telephone: StoryComponentType = () => {
|
|
|
237
335
|
);
|
|
238
336
|
};
|
|
239
337
|
|
|
338
|
+
Telephone.parameters = {
|
|
339
|
+
docs: {
|
|
340
|
+
storyDescription: `An input field with type \`tel\` will NOT
|
|
341
|
+
validate an input on submit by default as telephone numbers
|
|
342
|
+
can vary considerably. \`TextField\` will run validation on blur
|
|
343
|
+
if the \`validate\` prop is passed in, as in this example.`,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
240
347
|
export const Error: StoryComponentType = () => {
|
|
241
348
|
const [value, setValue] = React.useState("khan");
|
|
242
349
|
|
|
@@ -258,7 +365,7 @@ export const Error: StoryComponentType = () => {
|
|
|
258
365
|
label="Email"
|
|
259
366
|
type="email"
|
|
260
367
|
value={value}
|
|
261
|
-
onChange={
|
|
368
|
+
onChange={setValue}
|
|
262
369
|
description="Please provide your personal email"
|
|
263
370
|
placeholder="Email"
|
|
264
371
|
validate={validate}
|
|
@@ -267,16 +374,12 @@ export const Error: StoryComponentType = () => {
|
|
|
267
374
|
);
|
|
268
375
|
};
|
|
269
376
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
placeholder="Name"
|
|
277
|
-
disabled={true}
|
|
278
|
-
/>
|
|
279
|
-
);
|
|
377
|
+
Error.parameters = {
|
|
378
|
+
docs: {
|
|
379
|
+
storyDescription: `If an input value fails validation,
|
|
380
|
+
\`TextField\` will have error styling.`,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
280
383
|
|
|
281
384
|
export const Light: StoryComponentType = () => {
|
|
282
385
|
const [value, setValue] = React.useState("");
|
|
@@ -299,7 +402,7 @@ export const Light: StoryComponentType = () => {
|
|
|
299
402
|
</LabelSmall>
|
|
300
403
|
}
|
|
301
404
|
value={value}
|
|
302
|
-
onChange={
|
|
405
|
+
onChange={setValue}
|
|
303
406
|
placeholder="Name"
|
|
304
407
|
light={true}
|
|
305
408
|
onKeyDown={handleKeyDown}
|
|
@@ -308,6 +411,80 @@ export const Light: StoryComponentType = () => {
|
|
|
308
411
|
);
|
|
309
412
|
};
|
|
310
413
|
|
|
414
|
+
Light.parameters = {
|
|
415
|
+
docs: {
|
|
416
|
+
storyDescription: `If the \`light\` prop is set to true, the
|
|
417
|
+
underlying \`TextField\` will have a light border when focused.
|
|
418
|
+
This is intended to be used on a dark background. There is also a
|
|
419
|
+
specific light styling for the error state, as seen in the
|
|
420
|
+
\`ErrorLight\` story.`,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
export const ErrorLight: StoryComponentType = () => {
|
|
425
|
+
const [value, setValue] = React.useState("khan");
|
|
426
|
+
|
|
427
|
+
const validate = (value: string) => {
|
|
428
|
+
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/;
|
|
429
|
+
if (!emailRegex.test(value)) {
|
|
430
|
+
return "Please enter a valid email";
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
435
|
+
if (event.key === "Enter") {
|
|
436
|
+
event.currentTarget.blur();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<View style={styles.darkBackground}>
|
|
442
|
+
<LabeledTextField
|
|
443
|
+
label={
|
|
444
|
+
<LabelMedium style={styles.whiteColor}>Email</LabelMedium>
|
|
445
|
+
}
|
|
446
|
+
description={
|
|
447
|
+
<LabelSmall style={styles.offWhiteColor}>
|
|
448
|
+
Please provide your personal email
|
|
449
|
+
</LabelSmall>
|
|
450
|
+
}
|
|
451
|
+
type="email"
|
|
452
|
+
value={value}
|
|
453
|
+
light={true}
|
|
454
|
+
onChange={setValue}
|
|
455
|
+
placeholder="Email"
|
|
456
|
+
validate={validate}
|
|
457
|
+
onKeyDown={handleKeyDown}
|
|
458
|
+
/>
|
|
459
|
+
</View>
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
ErrorLight.parameters = {
|
|
464
|
+
docs: {
|
|
465
|
+
storyDescription: `If an input value fails validation and the
|
|
466
|
+
\`light\` prop is true, \`TextField\` will have light error styling.`,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
export const Disabled: StoryComponentType = () => (
|
|
471
|
+
<LabeledTextField
|
|
472
|
+
label="Name"
|
|
473
|
+
description="Please enter your name"
|
|
474
|
+
value=""
|
|
475
|
+
onChange={() => {}}
|
|
476
|
+
placeholder="Name"
|
|
477
|
+
disabled={true}
|
|
478
|
+
/>
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
Disabled.parameters = {
|
|
482
|
+
docs: {
|
|
483
|
+
storyDescription: `If the \`disabled\` prop is set to true,
|
|
484
|
+
\`TextField\` will have disabled styling and will not be interactable.`,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
|
|
311
488
|
export const CustomStyle: StoryComponentType = () => {
|
|
312
489
|
const [firstName, setFirstName] = React.useState("");
|
|
313
490
|
const [lastName, setLastName] = React.useState("");
|
|
@@ -324,7 +501,7 @@ export const CustomStyle: StoryComponentType = () => {
|
|
|
324
501
|
label="First name"
|
|
325
502
|
description="Please enter your first name"
|
|
326
503
|
value={firstName}
|
|
327
|
-
onChange={
|
|
504
|
+
onChange={setFirstName}
|
|
328
505
|
placeholder="Khan"
|
|
329
506
|
style={styles.grow}
|
|
330
507
|
onKeyDown={handleKeyDown}
|
|
@@ -334,7 +511,7 @@ export const CustomStyle: StoryComponentType = () => {
|
|
|
334
511
|
label="Last name"
|
|
335
512
|
description="Please enter your last name"
|
|
336
513
|
value={lastName}
|
|
337
|
-
onChange={
|
|
514
|
+
onChange={setLastName}
|
|
338
515
|
placeholder="Academy"
|
|
339
516
|
style={styles.grow}
|
|
340
517
|
onKeyDown={handleKeyDown}
|
|
@@ -343,6 +520,14 @@ export const CustomStyle: StoryComponentType = () => {
|
|
|
343
520
|
);
|
|
344
521
|
};
|
|
345
522
|
|
|
523
|
+
CustomStyle.parameters = {
|
|
524
|
+
docs: {
|
|
525
|
+
storyDescription: `\`LabeledTextField\` can take in custom styles that
|
|
526
|
+
override the default styles. In this example, each field has the
|
|
527
|
+
style property \`flexGrow: 1\``,
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
|
|
346
531
|
export const Ref: StoryComponentType = () => {
|
|
347
532
|
const [value, setValue] = React.useState("Khan");
|
|
348
533
|
const inputRef = React.createRef();
|
|
@@ -365,7 +550,7 @@ export const Ref: StoryComponentType = () => {
|
|
|
365
550
|
label="Name"
|
|
366
551
|
description="Please enter your name"
|
|
367
552
|
value={value}
|
|
368
|
-
onChange={
|
|
553
|
+
onChange={setValue}
|
|
369
554
|
placeholder="Name"
|
|
370
555
|
onKeyDown={handleKeyDown}
|
|
371
556
|
ref={inputRef}
|
|
@@ -378,6 +563,23 @@ export const Ref: StoryComponentType = () => {
|
|
|
378
563
|
);
|
|
379
564
|
};
|
|
380
565
|
|
|
566
|
+
Ref.parameters = {
|
|
567
|
+
docs: {
|
|
568
|
+
storyDescription: `If you need to save a reference to the input
|
|
569
|
+
field, you can do so by using the \`ref\` prop. In this example,
|
|
570
|
+
we want the input field to receive focus when the button is
|
|
571
|
+
pressed. We can do this by creating a React ref of type
|
|
572
|
+
\`HTMLInputElement\` and passing it into \`TextField\`'s \`ref\` prop.
|
|
573
|
+
Now we can use the ref variable in the \`handleSubmit\` function to
|
|
574
|
+
shift focus to the field.`,
|
|
575
|
+
chromatic: {
|
|
576
|
+
// Disabling snapshot because this is testing interaction,
|
|
577
|
+
// not visuals.
|
|
578
|
+
disableSnapshot: true,
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
|
|
381
583
|
export const ReadOnly: StoryComponentType = () => {
|
|
382
584
|
const [value, setValue] = React.useState("Khan");
|
|
383
585
|
|
|
@@ -392,7 +594,7 @@ export const ReadOnly: StoryComponentType = () => {
|
|
|
392
594
|
label="Read Only"
|
|
393
595
|
description="This is a read-only field."
|
|
394
596
|
value={value}
|
|
395
|
-
onChange={
|
|
597
|
+
onChange={setValue}
|
|
396
598
|
placeholder="Name"
|
|
397
599
|
onKeyDown={handleKeyDown}
|
|
398
600
|
readOnly={true}
|
|
@@ -400,6 +602,20 @@ export const ReadOnly: StoryComponentType = () => {
|
|
|
400
602
|
);
|
|
401
603
|
};
|
|
402
604
|
|
|
605
|
+
ReadOnly.parameters = {
|
|
606
|
+
docs: {
|
|
607
|
+
storyDescription: `An input field with the prop \`readOnly\` set
|
|
608
|
+
to true is not interactable. It looks the same as if it were not
|
|
609
|
+
read only, and it can still receive focus, but the interaction
|
|
610
|
+
point will not appear and the input will not change.`,
|
|
611
|
+
chromatic: {
|
|
612
|
+
// Disabling snapshot because this is testing interaction,
|
|
613
|
+
// not visuals.
|
|
614
|
+
disableSnapshot: true,
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
403
619
|
export const AutoComplete: StoryComponentType = () => {
|
|
404
620
|
const [value, setValue] = React.useState("");
|
|
405
621
|
|
|
@@ -410,18 +626,38 @@ export const AutoComplete: StoryComponentType = () => {
|
|
|
410
626
|
};
|
|
411
627
|
|
|
412
628
|
return (
|
|
413
|
-
<
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
629
|
+
<form>
|
|
630
|
+
<LabeledTextField
|
|
631
|
+
label="Name"
|
|
632
|
+
description="Please enter your name."
|
|
633
|
+
value={value}
|
|
634
|
+
onChange={setValue}
|
|
635
|
+
placeholder="Name"
|
|
636
|
+
onKeyDown={handleKeyDown}
|
|
637
|
+
style={styles.fieldWithButton}
|
|
638
|
+
autoComplete="name"
|
|
639
|
+
/>
|
|
640
|
+
<Button type="submit">Submit</Button>
|
|
641
|
+
</form>
|
|
422
642
|
);
|
|
423
643
|
};
|
|
424
644
|
|
|
645
|
+
AutoComplete.parameters = {
|
|
646
|
+
docs: {
|
|
647
|
+
storyDescription: `If \`TextField\`'s \`autocomplete\` prop is set,
|
|
648
|
+
the browser can predict values for the input. When the user starts
|
|
649
|
+
to type in the field, a list of options will show up based on
|
|
650
|
+
values that may have been submitted at a previous time.
|
|
651
|
+
In this example, the text field provides options after you
|
|
652
|
+
input a value, press the submit button, and refresh the page.`,
|
|
653
|
+
chromatic: {
|
|
654
|
+
// Disabling snapshot because this is testing interaction,
|
|
655
|
+
// not visuals.
|
|
656
|
+
disableSnapshot: true,
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
|
|
425
661
|
const styles = StyleSheet.create({
|
|
426
662
|
darkBackground: {
|
|
427
663
|
background: Color.darkBlue,
|
|
@@ -442,4 +678,7 @@ const styles = StyleSheet.create({
|
|
|
442
678
|
grow: {
|
|
443
679
|
flexGrow: 1,
|
|
444
680
|
},
|
|
681
|
+
fieldWithButton: {
|
|
682
|
+
marginBottom: Spacing.medium_16,
|
|
683
|
+
},
|
|
445
684
|
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {StyleSheet} from "aphrodite";
|
|
4
|
+
|
|
5
|
+
import {Choice, RadioGroup} from "@khanacademy/wonder-blocks-form";
|
|
6
|
+
import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
|
|
7
|
+
|
|
8
|
+
import type {StoryComponentType} from "@storybook/react";
|
|
9
|
+
|
|
10
|
+
import ComponentInfo from "../../../../../.storybook/components/component-info.js";
|
|
11
|
+
import {name, version} from "../../../package.json";
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
title: "Form / RadioGroup",
|
|
15
|
+
component: RadioGroup,
|
|
16
|
+
parameters: {
|
|
17
|
+
componentSubtitle: ((
|
|
18
|
+
<ComponentInfo name={name} version={version} />
|
|
19
|
+
): any),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Default: StoryComponentType = (args) => {
|
|
24
|
+
return (
|
|
25
|
+
<RadioGroup {...args}>
|
|
26
|
+
<Choice label="Bulbasaur" value="bulbasaur" />
|
|
27
|
+
<Choice
|
|
28
|
+
label="Charmander"
|
|
29
|
+
value="charmander"
|
|
30
|
+
description="Oops, we ran out of Charmanders"
|
|
31
|
+
disabled
|
|
32
|
+
/>
|
|
33
|
+
<Choice label="Squirtle" value="squirtle" />
|
|
34
|
+
<Choice label="Pikachu" value="pikachu" />
|
|
35
|
+
</RadioGroup>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
Default.args = {
|
|
40
|
+
// Required
|
|
41
|
+
groupName: "pokemon",
|
|
42
|
+
selectedValue: "bulbasaur",
|
|
43
|
+
onChange: () => {},
|
|
44
|
+
// Optional
|
|
45
|
+
label: "Pokemon",
|
|
46
|
+
description: "Your first Pokemon.",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Basic: StoryComponentType = () => {
|
|
50
|
+
const [selectedValue, setSelectedValue] = React.useState("");
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<RadioGroup
|
|
54
|
+
groupName="pokemon"
|
|
55
|
+
label="Pokemon"
|
|
56
|
+
description="Your first Pokemon."
|
|
57
|
+
onChange={setSelectedValue}
|
|
58
|
+
selectedValue={selectedValue}
|
|
59
|
+
>
|
|
60
|
+
<Choice label="Bulbasaur" value="bulbasaur" />
|
|
61
|
+
<Choice
|
|
62
|
+
label="Charmander"
|
|
63
|
+
value="charmander"
|
|
64
|
+
description="Oops, we ran out of Charmanders"
|
|
65
|
+
disabled
|
|
66
|
+
/>
|
|
67
|
+
<Choice label="Squirtle" value="squirtle" />
|
|
68
|
+
<Choice label="Pikachu" value="pikachu" />
|
|
69
|
+
</RadioGroup>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
Basic.parameters = {
|
|
74
|
+
docs: {
|
|
75
|
+
storyDescription: `This is a basic example of a radio group.
|
|
76
|
+
The Wonder Blocks \`RadioGroup\` component takes \`Choice\`
|
|
77
|
+
components as children. One of the \`Choice\` components here
|
|
78
|
+
includes a description.`,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Error: StoryComponentType = () => {
|
|
83
|
+
const emptyError = "You must select an option to continue.";
|
|
84
|
+
const [selectedValue, setSelectedValue] = React.useState("");
|
|
85
|
+
const [error, setError] = React.useState(emptyError);
|
|
86
|
+
|
|
87
|
+
// This returns an error message if no option is selected,
|
|
88
|
+
// and it returns undefined otherwise. We use undefined instead of
|
|
89
|
+
// null here because null would result in a flow error, whereas
|
|
90
|
+
// undefined would be the same as not passing in anything to the
|
|
91
|
+
// radio group's `errorMessage` prop.
|
|
92
|
+
const checkForError = (input) => {
|
|
93
|
+
if (!input) {
|
|
94
|
+
return emptyError;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleChange = (input) => {
|
|
99
|
+
const errorMessage = checkForError(input);
|
|
100
|
+
setSelectedValue(input);
|
|
101
|
+
setError(errorMessage);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<RadioGroup
|
|
106
|
+
groupName="pokemon"
|
|
107
|
+
label="Pokemon"
|
|
108
|
+
description="Your first Pokemon."
|
|
109
|
+
onChange={handleChange}
|
|
110
|
+
selectedValue={selectedValue}
|
|
111
|
+
errorMessage={error}
|
|
112
|
+
>
|
|
113
|
+
<Choice label="Bulbasaur" value="bulbasaur" />
|
|
114
|
+
<Choice label="Charmander" value="charmander" />
|
|
115
|
+
<Choice label="Squirtle" value="squirtle" />
|
|
116
|
+
<Choice label="Pikachu" value="pikachu" />
|
|
117
|
+
</RadioGroup>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
Error.parameters = {
|
|
122
|
+
docs: {
|
|
123
|
+
storyDescription: `This is what a radio group looks like
|
|
124
|
+
if it has an error. It displays the error that is passed into the
|
|
125
|
+
\`errorMessage\` prop, provided the error is not null. It also
|
|
126
|
+
uses the error styling for all the radio buttons. Here, the error
|
|
127
|
+
message is saved as a state, updated in the change handler, and then
|
|
128
|
+
passed in as the \`errorMessage\` prop.`,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const MultipleChoiceStyling: StoryComponentType = () => {
|
|
133
|
+
const [selectedValue, setSelectedValue] = React.useState("");
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<LabelLarge style={styles.prompt}>
|
|
138
|
+
Select your blood type
|
|
139
|
+
</LabelLarge>
|
|
140
|
+
<RadioGroup
|
|
141
|
+
groupName="science-classes"
|
|
142
|
+
onChange={setSelectedValue}
|
|
143
|
+
selectedValue={selectedValue}
|
|
144
|
+
>
|
|
145
|
+
<Choice label="A" value="1" style={styles.choice} />
|
|
146
|
+
<Choice label="B" value="2" style={styles.choice} />
|
|
147
|
+
<Choice label="AB" value="3" style={styles.choice} />
|
|
148
|
+
<Choice
|
|
149
|
+
label="O"
|
|
150
|
+
value="4"
|
|
151
|
+
style={[styles.choice, styles.lastChoice]}
|
|
152
|
+
/>
|
|
153
|
+
</RadioGroup>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
MultipleChoiceStyling.parameters = {
|
|
159
|
+
docs: {
|
|
160
|
+
storyDescription: `This example shows how to use custom styling
|
|
161
|
+
to change the appearance of the radio group to look more like
|
|
162
|
+
a multiple choice question. Here, there is a line in
|
|
163
|
+
between each question, which is achieved using the
|
|
164
|
+
\`{borderTop: "solid 1px #CCC"}\` style on each \`Choice\`
|
|
165
|
+
component.`,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const styles = StyleSheet.create({
|
|
170
|
+
choice: {
|
|
171
|
+
margin: 0,
|
|
172
|
+
height: 48,
|
|
173
|
+
borderTop: "solid 1px #CCC",
|
|
174
|
+
justifyContent: "center",
|
|
175
|
+
},
|
|
176
|
+
lastChoice: {
|
|
177
|
+
borderBottom: "solid 1px #CCC",
|
|
178
|
+
},
|
|
179
|
+
prompt: {
|
|
180
|
+
marginBottom: 16,
|
|
181
|
+
},
|
|
182
|
+
});
|