@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.
@@ -11,8 +11,42 @@ import Button from "@khanacademy/wonder-blocks-button";
11
11
 
12
12
  import type {StoryComponentType} from "@storybook/react";
13
13
 
14
+ import ComponentInfo from "../../../../../.storybook/components/component-info.js";
15
+ import {name, version} from "../../../package.json";
16
+ import TextFieldArgTypes from "./text-field.argtypes.js";
17
+
14
18
  export default {
15
19
  title: "Form / TextField",
20
+ component: TextField,
21
+ parameters: {
22
+ componentSubtitle: ((
23
+ <ComponentInfo name={name} version={version} />
24
+ ): any),
25
+ },
26
+ argTypes: TextFieldArgTypes,
27
+ };
28
+
29
+ export const Default: StoryComponentType = (args) => {
30
+ return <TextField {...args} />;
31
+ };
32
+
33
+ Default.args = {
34
+ id: "some-id",
35
+ type: "text",
36
+ value: "",
37
+ disabled: false,
38
+ placeholder: "",
39
+ required: false,
40
+ light: false,
41
+ testId: "",
42
+ readOnly: false,
43
+ autoComplete: "off",
44
+ validate: () => {},
45
+ onValidate: () => {},
46
+ onChange: () => {},
47
+ onKeyDown: () => {},
48
+ onFocus: () => {},
49
+ onBlur: () => {},
16
50
  };
17
51
 
18
52
  export const Text: StoryComponentType = () => {
@@ -40,6 +74,13 @@ export const Text: StoryComponentType = () => {
40
74
  );
41
75
  };
42
76
 
77
+ Text.parameters = {
78
+ docs: {
79
+ storyDescription:
80
+ "An input field with type `text` takes all kinds of characters.",
81
+ },
82
+ };
83
+
43
84
  export const Required: StoryComponentType = () => {
44
85
  const [value, setValue] = React.useState("");
45
86
 
@@ -55,7 +96,7 @@ export const Required: StoryComponentType = () => {
55
96
 
56
97
  return (
57
98
  <TextField
58
- id="tf-1"
99
+ id="tf-2"
59
100
  type="text"
60
101
  value={value}
61
102
  onChange={handleChange}
@@ -65,6 +106,19 @@ export const Required: StoryComponentType = () => {
65
106
  );
66
107
  };
67
108
 
109
+ Required.parameters = {
110
+ docs: {
111
+ storyDescription: `A required field will have error styling if the
112
+ field is left blank. To observe this, type something into the
113
+ field, backspace all the way, and then shift focus out of the field.`,
114
+ },
115
+ chromatic: {
116
+ // Disabling snapshot because it doesn't show the error style
117
+ // until after the user interacts with this field.
118
+ disableSnapshot: true,
119
+ },
120
+ };
121
+
68
122
  export const Number: StoryComponentType = () => {
69
123
  const [value, setValue] = React.useState("12345");
70
124
 
@@ -80,7 +134,7 @@ export const Number: StoryComponentType = () => {
80
134
 
81
135
  return (
82
136
  <TextField
83
- id="tf-1"
137
+ id="tf-3"
84
138
  type="number"
85
139
  value={value}
86
140
  placeholder="Number"
@@ -90,6 +144,13 @@ export const Number: StoryComponentType = () => {
90
144
  );
91
145
  };
92
146
 
147
+ Number.parameters = {
148
+ docs: {
149
+ storyDescription:
150
+ "An input field with type `number` will only take numeric characters as input.",
151
+ },
152
+ };
153
+
93
154
  export const Password: StoryComponentType = () => {
94
155
  const [value, setValue] = React.useState("Password123");
95
156
  const [errorMessage, setErrorMessage] = React.useState();
@@ -129,7 +190,7 @@ export const Password: StoryComponentType = () => {
129
190
  return (
130
191
  <View>
131
192
  <TextField
132
- id="tf-1"
193
+ id="tf-4"
133
194
  type="password"
134
195
  value={value}
135
196
  placeholder="Password"
@@ -150,6 +211,15 @@ export const Password: StoryComponentType = () => {
150
211
  );
151
212
  };
152
213
 
214
+ Password.parameters = {
215
+ docs: {
216
+ storyDescription: `An input field with type \`password\` will
217
+ obscure the input value. It also often contains validation.
218
+ In this example, the password must be over 8 characters long and
219
+ must contain a numeric value.`,
220
+ },
221
+ };
222
+
153
223
  export const Email: StoryComponentType = () => {
154
224
  const [value, setValue] = React.useState("khan@khanacademy.org");
155
225
  const [errorMessage, setErrorMessage] = React.useState();
@@ -187,7 +257,7 @@ export const Email: StoryComponentType = () => {
187
257
  return (
188
258
  <View>
189
259
  <TextField
190
- id="tf-1"
260
+ id="tf-5"
191
261
  type="email"
192
262
  value={value}
193
263
  placeholder="Email"
@@ -208,6 +278,15 @@ export const Email: StoryComponentType = () => {
208
278
  );
209
279
  };
210
280
 
281
+ Email.parameters = {
282
+ docs: {
283
+ storyDescription: `An input field with type \`email\` will automatically
284
+ validate an input on submit to ensure it's either formatted properly
285
+ or blank. \`TextField\` will run validation on blur if the
286
+ \`validate\` prop is passed in, as in this example.`,
287
+ },
288
+ };
289
+
211
290
  export const Telephone: StoryComponentType = () => {
212
291
  const [value, setValue] = React.useState("123-456-7890");
213
292
  const [errorMessage, setErrorMessage] = React.useState();
@@ -245,7 +324,7 @@ export const Telephone: StoryComponentType = () => {
245
324
  return (
246
325
  <View>
247
326
  <TextField
248
- id="tf-1"
327
+ id="tf-6"
249
328
  type="tel"
250
329
  value={value}
251
330
  placeholder="Telephone"
@@ -266,6 +345,15 @@ export const Telephone: StoryComponentType = () => {
266
345
  );
267
346
  };
268
347
 
348
+ Telephone.parameters = {
349
+ docs: {
350
+ storyDescription: `An input field with type \`tel\` will NOT
351
+ validate an input on submit by default as telephone numbers
352
+ can vary considerably. \`TextField\` will run validation on blur
353
+ if the \`validate\` prop is passed in, as in this example.`,
354
+ },
355
+ };
356
+
269
357
  export const Error: StoryComponentType = () => {
270
358
  const [value, setValue] = React.useState("khan");
271
359
  const [errorMessage, setErrorMessage] = React.useState();
@@ -303,7 +391,7 @@ export const Error: StoryComponentType = () => {
303
391
  return (
304
392
  <View>
305
393
  <TextField
306
- id="tf-1"
394
+ id="tf-7"
307
395
  type="email"
308
396
  value={value}
309
397
  placeholder="Email"
@@ -324,15 +412,12 @@ export const Error: StoryComponentType = () => {
324
412
  );
325
413
  };
326
414
 
327
- export const Disabled: StoryComponentType = () => (
328
- <TextField
329
- id="tf-1"
330
- value=""
331
- placeholder="This field is disabled."
332
- onChange={() => {}}
333
- disabled={true}
334
- />
335
- );
415
+ Error.parameters = {
416
+ docs: {
417
+ storyDescription: `If an input value fails validation,
418
+ \`TextField\` will have error styling.`,
419
+ },
420
+ };
336
421
 
337
422
  export const Light: StoryComponentType = () => {
338
423
  const [value, setValue] = React.useState("khan@khanacademy.org");
@@ -371,7 +456,7 @@ export const Light: StoryComponentType = () => {
371
456
  return (
372
457
  <View style={styles.darkBackground}>
373
458
  <TextField
374
- id="tf-1"
459
+ id="tf-9"
375
460
  type="email"
376
461
  value={value}
377
462
  placeholder="Email"
@@ -395,6 +480,98 @@ export const Light: StoryComponentType = () => {
395
480
  );
396
481
  };
397
482
 
483
+ Light.parameters = {
484
+ docs: {
485
+ storyDescription: `If the \`light\` prop is set to true,
486
+ \`TextField\` will have light styling. This is intended to be used
487
+ on a dark background. There is also a specific light styling for the
488
+ error state, as seen in the \`ErrorLight\` story.`,
489
+ },
490
+ };
491
+
492
+ export const ErrorLight: StoryComponentType = () => {
493
+ const [value, setValue] = React.useState("khan");
494
+ const [errorMessage, setErrorMessage] = React.useState();
495
+ const [focused, setFocused] = React.useState(false);
496
+
497
+ const handleChange = (newValue: string) => {
498
+ setValue(newValue);
499
+ };
500
+
501
+ const validate = (value: string) => {
502
+ const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/;
503
+ if (!emailRegex.test(value)) {
504
+ return "Please enter a valid email";
505
+ }
506
+ };
507
+
508
+ const handleValidate = (errorMessage: ?string) => {
509
+ setErrorMessage(errorMessage);
510
+ };
511
+
512
+ const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
513
+ if (event.key === "Enter") {
514
+ event.currentTarget.blur();
515
+ }
516
+ };
517
+
518
+ const handleFocus = () => {
519
+ setFocused(true);
520
+ };
521
+
522
+ const handleBlur = () => {
523
+ setFocused(false);
524
+ };
525
+
526
+ return (
527
+ <View style={styles.darkBackground}>
528
+ <TextField
529
+ id="tf-7"
530
+ type="email"
531
+ value={value}
532
+ placeholder="Email"
533
+ light={true}
534
+ validate={validate}
535
+ onValidate={handleValidate}
536
+ onChange={handleChange}
537
+ onKeyDown={handleKeyDown}
538
+ onFocus={handleFocus}
539
+ onBlur={handleBlur}
540
+ />
541
+ {!focused && errorMessage && (
542
+ <View>
543
+ <Strut size={Spacing.xSmall_8} />
544
+ <_Text style={styles.errorMessage}>{errorMessage}</_Text>
545
+ </View>
546
+ )}
547
+ </View>
548
+ );
549
+ };
550
+
551
+ ErrorLight.parameters = {
552
+ docs: {
553
+ storyDescription: `If an input value fails validation and the
554
+ \`light\` prop is true, \`TextField\` will have light error styling.`,
555
+ },
556
+ };
557
+
558
+ export const Disabled: StoryComponentType = () => (
559
+ <TextField
560
+ id="tf-8"
561
+ value=""
562
+ placeholder="This field is disabled."
563
+ onChange={() => {}}
564
+ disabled={true}
565
+ />
566
+ );
567
+
568
+ Disabled.parameters = {
569
+ docs: {
570
+ storyDescription: `If the \`disabled\` prop is set to true,
571
+ \`TextField\` will have disabled styling and will not be interactable.`,
572
+ },
573
+ };
574
+
398
575
  export const CustomStyle: StoryComponentType = () => {
399
576
  const [value, setValue] = React.useState("");
400
577
 
@@ -410,7 +587,7 @@ export const CustomStyle: StoryComponentType = () => {
410
587
 
411
588
  return (
412
589
  <TextField
413
- id="tf-1"
590
+ id="tf-10"
414
591
  style={styles.customField}
415
592
  type="text"
416
593
  value={value}
@@ -421,6 +598,15 @@ export const CustomStyle: StoryComponentType = () => {
421
598
  );
422
599
  };
423
600
 
601
+ CustomStyle.parameters = {
602
+ docs: {
603
+ storyDescription: `\`TextField\` can take in custom styles that
604
+ override the default styles. This example has custom styles for the
605
+ \`backgroundColor\`, \`color\`, \`border\`, \`maxWidth\`, and
606
+ placeholder \`color\` properties.`,
607
+ },
608
+ };
609
+
424
610
  export const Ref: StoryComponentType = () => {
425
611
  const [value, setValue] = React.useState("");
426
612
  const inputRef: RefObject<typeof HTMLInputElement> = React.createRef();
@@ -444,7 +630,7 @@ export const Ref: StoryComponentType = () => {
444
630
  return (
445
631
  <View>
446
632
  <TextField
447
- id="tf-1"
633
+ id="tf-11"
448
634
  type="text"
449
635
  value={value}
450
636
  placeholder="Text"
@@ -460,6 +646,23 @@ export const Ref: StoryComponentType = () => {
460
646
  );
461
647
  };
462
648
 
649
+ Ref.parameters = {
650
+ docs: {
651
+ storyDescription: `If you need to save a reference to the input
652
+ field, you can do so by using the \`ref\` prop. In this example,
653
+ we want the input field to receive focus when the button is
654
+ pressed. We can do this by creating a React ref of type
655
+ \`HTMLInputElement\` and passing it into \`TextField\`'s \`ref\` prop.
656
+ Now we can use the ref variable in the \`handleSubmit\` function to
657
+ shift focus to the field.`,
658
+ chromatic: {
659
+ // Disabling snapshot because this is testing interaction,
660
+ // not visuals.
661
+ disableSnapshot: true,
662
+ },
663
+ },
664
+ };
665
+
463
666
  export const ReadOnly: StoryComponentType = () => {
464
667
  const [value, setValue] = React.useState("Khan");
465
668
 
@@ -475,7 +678,7 @@ export const ReadOnly: StoryComponentType = () => {
475
678
 
476
679
  return (
477
680
  <TextField
478
- id="tf-1"
681
+ id="tf-12"
479
682
  type="text"
480
683
  value={value}
481
684
  placeholder="Text"
@@ -486,6 +689,20 @@ export const ReadOnly: StoryComponentType = () => {
486
689
  );
487
690
  };
488
691
 
692
+ ReadOnly.parameters = {
693
+ docs: {
694
+ storyDescription: `An input field with the prop \`readOnly\` set
695
+ to true is not interactable. It looks the same as if it were not
696
+ read only, and it can still receive focus, but the interaction
697
+ point will not appear and the input will not change.`,
698
+ chromatic: {
699
+ // Disabling snapshot because this is testing interaction,
700
+ // not visuals.
701
+ disableSnapshot: true,
702
+ },
703
+ },
704
+ };
705
+
489
706
  export const AutoComplete: StoryComponentType = () => {
490
707
  const [value, setValue] = React.useState("");
491
708
 
@@ -500,18 +717,38 @@ export const AutoComplete: StoryComponentType = () => {
500
717
  };
501
718
 
502
719
  return (
503
- <TextField
504
- id="tf-1"
505
- type="text"
506
- value={value}
507
- placeholder="Name"
508
- onChange={handleChange}
509
- onKeyDown={handleKeyDown}
510
- autoComplete="name"
511
- />
720
+ <form>
721
+ <TextField
722
+ id="tf-13"
723
+ type="text"
724
+ value={value}
725
+ placeholder="Name"
726
+ onChange={handleChange}
727
+ onKeyDown={handleKeyDown}
728
+ style={styles.fieldWithButton}
729
+ autoComplete="name"
730
+ />
731
+ <Button type="submit">Submit</Button>
732
+ </form>
512
733
  );
513
734
  };
514
735
 
736
+ AutoComplete.parameters = {
737
+ docs: {
738
+ storyDescription: `If \`TextField\`'s \`autocomplete\` prop is set,
739
+ the browser can predict values for the input. When the user starts
740
+ to type in the field, a list of options will show up based on
741
+ values that may have been submitted at a previous time.
742
+ In this example, the text field provides options after you
743
+ input a value, press the submit button, and refresh the page.`,
744
+ chromatic: {
745
+ // Disabling snapshot because this is testing interaction,
746
+ // not visuals.
747
+ disableSnapshot: true,
748
+ },
749
+ },
750
+ };
751
+
515
752
  const styles = StyleSheet.create({
516
753
  errorMessage: {
517
754
  color: Color.red,
@@ -537,4 +774,7 @@ const styles = StyleSheet.create({
537
774
  button: {
538
775
  maxWidth: 150,
539
776
  },
777
+ fieldWithButton: {
778
+ marginBottom: Spacing.medium_16,
779
+ },
540
780
  });
@@ -76,6 +76,33 @@ const StyledLegend = addStyle<"legend">("legend");
76
76
  * many props for its children Choice components. The Choice component is
77
77
  * exposed for the user to apply custom styles or to indicate which choices are
78
78
  * disabled.
79
+ *
80
+ * ### Usage
81
+ *
82
+ * ```jsx
83
+ * import {Choice, CheckboxGroup} from "@khanacademy/wonder-blocks-form";
84
+ *
85
+ * const [selectedValues, setSelectedValues] = React.useState([]);
86
+ *
87
+ * <CheckboxGroup
88
+ * label="some-label"
89
+ * description="some-description"
90
+ * groupName="some-group-name"
91
+ * onChange={setSelectedValues}
92
+ * selectedValues={selectedValues}
93
+ * />
94
+ * // Add as many choices as necessary
95
+ * <Choice
96
+ * label="Choice 1"
97
+ * value="some-choice-value"
98
+ * />
99
+ * <Choice
100
+ * label="Choice 2"
101
+ * value="some-choice-value-2"
102
+ * description="Some choice description."
103
+ * />
104
+ * </CheckboxGroup>
105
+ * ```
79
106
  */
80
107
  export default class CheckboxGroup extends React.Component<CheckboxGroupProps> {
81
108
  handleChange(changedValue: string, originalCheckedState: boolean) {
@@ -80,7 +80,65 @@ type DefaultProps = {|
80
80
  *
81
81
  * If you wish to use just a single field, use Checkbox or Radio with the
82
82
  * optional label and description props.
83
- */ export default class Choice extends React.Component<Props> {
83
+ *
84
+ * ### Checkbox Usage
85
+ *
86
+ * ```jsx
87
+ * import {Choice, CheckboxGroup} from "@khanacademy/wonder-blocks-form";
88
+ *
89
+ * const [selectedValues, setSelectedValues] = React.useState([]);
90
+ *
91
+ * // Checkbox usage
92
+ * <CheckboxGroup
93
+ * label="some-label"
94
+ * description="some-description"
95
+ * groupName="some-group-name"
96
+ * onChange={setSelectedValues}
97
+ * selectedValues={selectedValues}
98
+ * />
99
+ * // Add as many choices as necessary
100
+ * <Choice
101
+ * label="Choice 1"
102
+ * value="some-choice-value"
103
+ * description="Some choice description."
104
+ * />
105
+ * <Choice
106
+ * label="Choice 2"
107
+ * value="some-choice-value-2"
108
+ * description="Some choice description."
109
+ * />
110
+ * </CheckboxGroup>
111
+ * ```
112
+ *
113
+ * ### Radio Usage
114
+ *
115
+ * ```jsx
116
+ * import {Choice, RadioGroup} from "@khanacademy/wonder-blocks-form";
117
+ *
118
+ * const [selectedValue, setSelectedValue] = React.useState("");
119
+ *
120
+ * <RadioGroup
121
+ * label="some-label"
122
+ * description="some-description"
123
+ * groupName="some-group-name"
124
+ * onChange={setSelectedValue}>
125
+ * selectedValues={selectedValue}
126
+ * />
127
+ * // Add as many choices as necessary
128
+ * <Choice
129
+ * label="Choice 1"
130
+ * value="some-choice-value"
131
+ * description="Some choice description."
132
+ * />
133
+ * <Choice
134
+ * label="Choice 2"
135
+ * value="some-choice-value-2"
136
+ * description="Some choice description."
137
+ * />
138
+ * </RadioGroup>
139
+ * ```
140
+ */
141
+ export default class Choice extends React.Component<Props> {
84
142
  static defaultProps: DefaultProps = {
85
143
  checked: false,
86
144
  disabled: false,
@@ -293,6 +293,26 @@ type ExportProps = $Diff<
293
293
  WithForwardRef,
294
294
  >;
295
295
 
296
+ /**
297
+ * A LabeledTextField is an element used to accept a single line of text
298
+ * from the user paired with a label, description, and error field elements.
299
+ *
300
+ * ### Usage
301
+ *
302
+ * ```jsx
303
+ * import {LabeledTextField} from "@khanacademy/wonder-blocks-form";
304
+ *
305
+ * const [value, setValue] = React.useState("");
306
+ *
307
+ * <LabeledTextField
308
+ * label="Label"
309
+ * description="Hello, this is the description for this field"
310
+ * placeholder="Placeholder"
311
+ * value={value}
312
+ * onChange={setValue}
313
+ * />
314
+ * ```
315
+ */
296
316
  const LabeledTextField: React.AbstractComponent<ExportProps, HTMLInputElement> =
297
317
  React.forwardRef<ExportProps, HTMLInputElement>((props, ref) => (
298
318
  <LabeledTextFieldInternal {...props} forwardedRef={ref} />
@@ -73,6 +73,33 @@ const StyledLegend = addStyle<"legend">("legend");
73
73
  * indicate which choices are disabled. The use of the groupName prop is
74
74
  * important to maintain expected keyboard navigation behavior for
75
75
  * accessibility.
76
+ *
77
+ * ### Usage
78
+ *
79
+ * ```jsx
80
+ * import {Choice, RadioGroup} from "@khanacademy/wonder-blocks-form";
81
+ *
82
+ * const [selectedValue, setSelectedValue] = React.useState([]);
83
+ *
84
+ * <RadioGroup
85
+ * label="some-label"
86
+ * description="some-description"
87
+ * groupName="some-group-name"
88
+ * onChange={setSelectedValue}
89
+ * selectedValue={selectedValue}
90
+ * />
91
+ * // Add as many choices as necessary
92
+ * <Choice
93
+ * label="Choice 1"
94
+ * value="some-choice-value"
95
+ * />
96
+ * <Choice
97
+ * label="Choice 2"
98
+ * value="some-choice-value-2"
99
+ * description="Some choice description."
100
+ * />
101
+ * </RadioGroup>
102
+ * ```
76
103
  */
77
104
  export default class RadioGroup extends React.Component<RadioGroupProps> {
78
105
  handleChange(changedValue: string) {
@@ -348,6 +348,23 @@ type ExportProps = $Diff<
348
348
  WithForwardRef,
349
349
  >;
350
350
 
351
+ /**
352
+ * A TextField is an element used to accept a single line of text from the user.
353
+ *
354
+ * ### Usage
355
+ *
356
+ * ```jsx
357
+ * import {TextField} from "@khanacademy/wonder-blocks-form";
358
+ *
359
+ * const [value, setValue] = React.useState("");
360
+ *
361
+ * <TextField
362
+ * id="some-unique-text-field-id"
363
+ * value={value}
364
+ * onChange={setValue}
365
+ * />
366
+ * ```
367
+ */
351
368
  const TextField: React.AbstractComponent<ExportProps, HTMLInputElement> =
352
369
  React.forwardRef<ExportProps, HTMLInputElement>((props, ref) => (
353
370
  <TextFieldInternal {...props} forwardedRef={ref} />