@khanacademy/wonder-blocks-form 2.2.1

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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/dist/es/index.js +1100 -0
  3. package/dist/index.js +1419 -0
  4. package/dist/index.js.flow +2 -0
  5. package/docs.md +1 -0
  6. package/package.json +35 -0
  7. package/src/__tests__/__snapshots__/custom-snapshot.test.js.snap +1349 -0
  8. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +6126 -0
  9. package/src/__tests__/custom-snapshot.test.js +66 -0
  10. package/src/__tests__/generated-snapshot.test.js +654 -0
  11. package/src/components/__tests__/checkbox-group.test.js +84 -0
  12. package/src/components/__tests__/field-heading.test.js +182 -0
  13. package/src/components/__tests__/labeled-text-field.test.js +442 -0
  14. package/src/components/__tests__/radio-group.test.js +84 -0
  15. package/src/components/__tests__/text-field.test.js +424 -0
  16. package/src/components/checkbox-core.js +201 -0
  17. package/src/components/checkbox-group.js +161 -0
  18. package/src/components/checkbox-group.md +200 -0
  19. package/src/components/checkbox.js +94 -0
  20. package/src/components/checkbox.md +134 -0
  21. package/src/components/choice-internal.js +206 -0
  22. package/src/components/choice.js +104 -0
  23. package/src/components/field-heading.js +157 -0
  24. package/src/components/field-heading.md +43 -0
  25. package/src/components/group-styles.js +35 -0
  26. package/src/components/labeled-text-field.js +265 -0
  27. package/src/components/labeled-text-field.md +535 -0
  28. package/src/components/labeled-text-field.stories.js +359 -0
  29. package/src/components/radio-core.js +176 -0
  30. package/src/components/radio-group.js +142 -0
  31. package/src/components/radio-group.md +129 -0
  32. package/src/components/radio.js +93 -0
  33. package/src/components/radio.md +26 -0
  34. package/src/components/text-field.js +326 -0
  35. package/src/components/text-field.md +770 -0
  36. package/src/components/text-field.stories.js +513 -0
  37. package/src/index.js +18 -0
  38. package/src/util/types.js +77 -0
@@ -0,0 +1,84 @@
1
+ //@flow
2
+ import * as React from "react";
3
+ import {mount} from "enzyme";
4
+
5
+ import CheckboxGroup from "../checkbox-group.js";
6
+ import Choice from "../choice.js";
7
+
8
+ describe("CheckboxGroup", () => {
9
+ let group;
10
+ const onChange = jest.fn();
11
+
12
+ beforeEach(() => {
13
+ group = mount(
14
+ <CheckboxGroup
15
+ label="Test"
16
+ description="test description"
17
+ groupName="test"
18
+ onChange={onChange}
19
+ selectedValues={["a", "b"]}
20
+ >
21
+ <Choice label="a" value="a" aria-labelledby="test-a" />
22
+ <Choice label="b" value="b" aria-labelledby="test-b" />
23
+ <Choice label="c" value="c" aria-labelledby="test-c" />
24
+ </CheckboxGroup>,
25
+ );
26
+ });
27
+
28
+ it("has the correct items checked", () => {
29
+ const a = group.find(Choice).at(0);
30
+ const b = group.find(Choice).at(1);
31
+ const c = group.find(Choice).at(2);
32
+
33
+ // a starts off checked
34
+ expect(a.prop("checked")).toEqual(true);
35
+ expect(b.prop("checked")).toEqual(true);
36
+ expect(c.prop("checked")).toEqual(false);
37
+ });
38
+
39
+ it("changes selection when selectedValue changes", () => {
40
+ group.setProps({selectedValues: ["b"]});
41
+ const a = group.find(Choice).at(0);
42
+ const b = group.find(Choice).at(1);
43
+ const c = group.find(Choice).at(2);
44
+
45
+ // now only b is checked
46
+ expect(a.prop("checked")).toEqual(false);
47
+ expect(b.prop("checked")).toEqual(true);
48
+ expect(c.prop("checked")).toEqual(false);
49
+ });
50
+
51
+ it("displays error state for all Choice children", () => {
52
+ group.setProps({errorMessage: "there's an error"});
53
+ const a = group.find(Choice).at(0);
54
+ const b = group.find(Choice).at(1);
55
+ const c = group.find(Choice).at(2);
56
+
57
+ expect(a.prop("error")).toEqual(true);
58
+ expect(b.prop("error")).toEqual(true);
59
+ expect(c.prop("error")).toEqual(true);
60
+ });
61
+
62
+ it("calls onChange for each new selection", () => {
63
+ // a is clicked
64
+ const a = group.find(Choice).at(0);
65
+ const aTarget = a.find("ClickableBehavior");
66
+ aTarget.simulate("click");
67
+ expect(onChange).toHaveBeenCalledTimes(1);
68
+
69
+ // now b is clicked, onChange should also be called
70
+ const b = group.find(Choice).at(1);
71
+ const bTarget = b.find("ClickableBehavior");
72
+ bTarget.simulate("click");
73
+ expect(onChange).toHaveBeenCalledTimes(2);
74
+ });
75
+
76
+ it("checks that aria attributes have been added correctly", () => {
77
+ const a = group.find(Choice).at(0);
78
+ const b = group.find(Choice).at(1);
79
+ const c = group.find(Choice).at(2);
80
+ expect(a.find("input").prop("aria-labelledby")).toEqual("test-a");
81
+ expect(b.find("input").prop("aria-labelledby")).toEqual("test-b");
82
+ expect(c.find("input").prop("aria-labelledby")).toEqual("test-c");
83
+ });
84
+ });
@@ -0,0 +1,182 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {mount} from "enzyme";
4
+ import {StyleSheet} from "aphrodite";
5
+
6
+ import FieldHeading from "../field-heading.js";
7
+ import TextField from "../text-field.js";
8
+
9
+ describe("FieldHeading", () => {
10
+ it("fieldheading renders the label text", () => {
11
+ // Arrange
12
+ const label = "Label";
13
+
14
+ // Act
15
+ const wrapper = mount(
16
+ <FieldHeading
17
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
18
+ label={label}
19
+ />,
20
+ );
21
+
22
+ // Assert
23
+ expect(wrapper).toIncludeText(label);
24
+ });
25
+
26
+ it("fieldheading renders the description text", () => {
27
+ // Arrange
28
+ const description = "Description";
29
+
30
+ // Act
31
+ const wrapper = mount(
32
+ <FieldHeading
33
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
34
+ label="Label"
35
+ description={description}
36
+ />,
37
+ );
38
+
39
+ // Assert
40
+ expect(wrapper).toIncludeText(description);
41
+ });
42
+
43
+ it("fieldheading renders the error text", () => {
44
+ // Arrange
45
+ const error = "Error";
46
+
47
+ // Act
48
+ const wrapper = mount(
49
+ <FieldHeading
50
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
51
+ label="Label"
52
+ error={error}
53
+ />,
54
+ );
55
+
56
+ // Assert
57
+ expect(wrapper).toIncludeText(error);
58
+ });
59
+
60
+ it("fieldheading adds testId to label", () => {
61
+ // Arrange
62
+ const testId = "testid";
63
+
64
+ // Act
65
+ const wrapper = mount(
66
+ <FieldHeading
67
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
68
+ label="Label"
69
+ testId={testId}
70
+ />,
71
+ );
72
+
73
+ // Assert
74
+ const label = wrapper.find(`[data-test-id="${testId}-label"]`);
75
+ expect(label).toExist();
76
+ });
77
+
78
+ it("fieldheading adds testId to description", () => {
79
+ // Arrange
80
+ const testId = "testid";
81
+
82
+ // Act
83
+ const wrapper = mount(
84
+ <FieldHeading
85
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
86
+ label="Label"
87
+ description="Description"
88
+ testId={testId}
89
+ />,
90
+ );
91
+
92
+ // Assert
93
+ const description = wrapper.find(
94
+ `[data-test-id="${testId}-description"]`,
95
+ );
96
+ expect(description).toExist();
97
+ });
98
+
99
+ it("fieldheading adds testId to error", () => {
100
+ // Arrange
101
+ const testId = "testid";
102
+
103
+ // Act
104
+ const wrapper = mount(
105
+ <FieldHeading
106
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
107
+ label="Label"
108
+ error="Error"
109
+ testId={testId}
110
+ />,
111
+ );
112
+
113
+ // Assert
114
+ const error = wrapper.find(`[data-test-id="${testId}-error"]`);
115
+ expect(error).toExist();
116
+ });
117
+
118
+ it("fieldheading adds the correctly formatted id to label's htmlFor", () => {
119
+ // Arrange
120
+ const id = "exampleid";
121
+ const testId = "testid";
122
+
123
+ // Act
124
+ const wrapper = mount(
125
+ <FieldHeading
126
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
127
+ label="Label"
128
+ id={id}
129
+ testId={testId}
130
+ />,
131
+ );
132
+
133
+ // Assert
134
+ const label = wrapper.find(`[data-test-id="${testId}-label"]`);
135
+ expect(label).toContainMatchingElement(`[htmlFor="${id}-field"]`);
136
+ });
137
+
138
+ it("fieldheading adds the correctly formatted id to error's id", () => {
139
+ // Arrange
140
+ const id = "exampleid";
141
+ const testId = "testid";
142
+
143
+ // Act
144
+ const wrapper = mount(
145
+ <FieldHeading
146
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
147
+ label="Label"
148
+ error="Error"
149
+ id={id}
150
+ testId={testId}
151
+ />,
152
+ );
153
+
154
+ // Assert
155
+ const error = wrapper.find(`[data-test-id="${testId}-error"]`);
156
+ expect(error).toContainMatchingElement(`[id="${id}-error"]`);
157
+ });
158
+
159
+ it("stype prop applies to the fieldheading container", () => {
160
+ // Arrange
161
+ const styles = StyleSheet.create({
162
+ style1: {
163
+ flexGrow: 1,
164
+ background: "blue",
165
+ },
166
+ });
167
+
168
+ // Act
169
+ const wrapper = mount(
170
+ <FieldHeading
171
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
172
+ label="Label"
173
+ error="Error"
174
+ style={styles.style1}
175
+ />,
176
+ );
177
+
178
+ // Assert
179
+ const container = wrapper.find("View").at(0);
180
+ expect(container).toHaveStyle(styles.style1);
181
+ });
182
+ });
@@ -0,0 +1,442 @@
1
+ //@flow
2
+ import * as React from "react";
3
+ import {mount} from "enzyme";
4
+
5
+ import {StyleSheet} from "aphrodite";
6
+ import LabeledTextField from "../labeled-text-field.js";
7
+
8
+ const wait = (delay: number = 0) =>
9
+ new Promise((resolve, reject) => {
10
+ // eslint-disable-next-line no-restricted-syntax
11
+ return setTimeout(resolve, delay);
12
+ });
13
+
14
+ describe("LabeledTextField", () => {
15
+ it("labeledtextfield becomes focused", () => {
16
+ // Arrange
17
+ const wrapper = mount(
18
+ <LabeledTextField label="Label" value="" onChange={() => {}} />,
19
+ );
20
+ const field = wrapper.find("TextFieldInternal");
21
+
22
+ // Act
23
+ field.simulate("focus");
24
+
25
+ // Assert
26
+ expect(wrapper.find("LabeledTextFieldInternal")).toHaveState(
27
+ "focused",
28
+ true,
29
+ );
30
+ });
31
+
32
+ it("labeledtextfield becomes blurred", async () => {
33
+ // Arrange
34
+ const wrapper = mount(
35
+ <LabeledTextField label="Label" value="" onChange={() => {}} />,
36
+ );
37
+ const field = wrapper.find("TextFieldInternal");
38
+
39
+ // Act
40
+ field.simulate("focus");
41
+ await wait(0);
42
+ field.simulate("blur");
43
+
44
+ // Assert
45
+ expect(wrapper.find("LabeledTextFieldInternal")).toHaveState(
46
+ "focused",
47
+ false,
48
+ );
49
+ });
50
+
51
+ it("id prop is passed to input", () => {
52
+ // Arrange
53
+ const id = "exampleid";
54
+
55
+ // Act
56
+ const wrapper = mount(
57
+ <LabeledTextField
58
+ id={id}
59
+ label="Label"
60
+ value=""
61
+ onChange={() => {}}
62
+ disabled={true}
63
+ />,
64
+ );
65
+
66
+ // Assert
67
+ const input = wrapper.find("input");
68
+ expect(input).toContainMatchingElement(`[id="${id}-field"]`);
69
+ });
70
+
71
+ it("auto-generated id is passed to input when id prop is not set", () => {
72
+ // Arrange
73
+
74
+ // Act
75
+ const wrapper = mount(
76
+ <LabeledTextField label="Label" value="" onChange={() => {}} />,
77
+ );
78
+
79
+ // Assert
80
+ // Since the generated id is unique, we cannot know what it will be.
81
+ // We only test if the id attribute starts with "uid-" and ends with "-field".
82
+ const input = wrapper.find("input");
83
+ expect(input.props()["id"]).toMatch(/uid-.*-field/);
84
+ });
85
+
86
+ it("type prop is passed to input", () => {
87
+ // Arrange
88
+ const type = "email";
89
+
90
+ // Act
91
+ const wrapper = mount(
92
+ <LabeledTextField
93
+ type={type}
94
+ label="Label"
95
+ value=""
96
+ onChange={() => {}}
97
+ />,
98
+ );
99
+
100
+ // Assert
101
+ const input = wrapper.find("input");
102
+ expect(input).toContainMatchingElement(`[type="${type}"]`);
103
+ });
104
+
105
+ it("label prop is rendered", () => {
106
+ // Arrange
107
+ const label = "Label";
108
+
109
+ // Act
110
+ const wrapper = mount(
111
+ <LabeledTextField label={label} value="" onChange={() => {}} />,
112
+ );
113
+
114
+ // Assert
115
+ expect(wrapper).toIncludeText(label);
116
+ });
117
+
118
+ it("description prop is rendered", () => {
119
+ // Arrange
120
+ const description = "Description";
121
+
122
+ // Act
123
+ const wrapper = mount(
124
+ <LabeledTextField
125
+ label="Label"
126
+ description={description}
127
+ value=""
128
+ onChange={() => {}}
129
+ />,
130
+ );
131
+
132
+ // Assert
133
+ expect(wrapper).toIncludeText(description);
134
+ });
135
+
136
+ it("value prop is set on mount", () => {
137
+ // Arrange
138
+ const value = "Value";
139
+
140
+ // Act
141
+ const wrapper = mount(
142
+ <LabeledTextField
143
+ label="Label"
144
+ value={value}
145
+ onChange={() => {}}
146
+ />,
147
+ );
148
+
149
+ // Assert
150
+ const input = wrapper.find("input");
151
+ expect(input).toHaveValue(value);
152
+ });
153
+
154
+ it("value prop change from parent reflects on input value", async () => {
155
+ // Arrange
156
+ const handleChange = jest.fn((newValue: string) => {});
157
+
158
+ const wrapper = mount(
159
+ <LabeledTextField label="Label" value="" onChange={handleChange} />,
160
+ );
161
+
162
+ // Act
163
+ const newValue = "new value";
164
+ wrapper.setProps({value: newValue});
165
+
166
+ // Assert
167
+ const input = wrapper.find("input");
168
+ expect(input).toHaveValue(newValue);
169
+ });
170
+
171
+ it("disabled prop disables the input", () => {
172
+ // Arrange
173
+
174
+ // Act
175
+ const wrapper = mount(
176
+ <LabeledTextField
177
+ label="Label"
178
+ value=""
179
+ onChange={() => {}}
180
+ disabled={true}
181
+ />,
182
+ );
183
+
184
+ // Assert
185
+ const input = wrapper.find("input");
186
+ expect(input).toBeDisabled();
187
+ });
188
+
189
+ it("validate prop is called when input changes", () => {
190
+ // Arrange
191
+ const validate = jest.fn((value: string): ?string => {});
192
+ const wrapper = mount(
193
+ <LabeledTextField
194
+ label="Label"
195
+ value=""
196
+ onChange={() => {}}
197
+ validate={validate}
198
+ />,
199
+ );
200
+
201
+ // Act
202
+ const newValue = "New Value";
203
+ const input = wrapper.find("input");
204
+ input.simulate("change", {target: {value: newValue}});
205
+
206
+ // Assert
207
+ expect(validate).toHaveBeenCalledWith(newValue);
208
+ });
209
+
210
+ it("onValidate prop is called on new validated input", () => {
211
+ // Arrange
212
+ const handleValidate = jest.fn((errorMessage: ?string) => {});
213
+ const errorMessage = "Password must be at least 8 characters long";
214
+
215
+ const validate = (value: string): ?string => {
216
+ if (value.length < 8) {
217
+ return errorMessage;
218
+ }
219
+ };
220
+
221
+ const wrapper = mount(
222
+ <LabeledTextField
223
+ label="Label"
224
+ value="LongerThan8Chars"
225
+ onChange={() => {}}
226
+ validate={validate}
227
+ onValidate={handleValidate}
228
+ />,
229
+ );
230
+
231
+ // Act
232
+ const input = wrapper.find("input");
233
+ input.simulate("change", {target: {value: "Short"}});
234
+
235
+ // Assert
236
+ expect(handleValidate).toHaveBeenCalledWith(errorMessage);
237
+ });
238
+
239
+ it("onChange prop is called on input change", () => {
240
+ // Arrange
241
+ const handleChange = jest.fn((newValue: string) => {});
242
+
243
+ const wrapper = mount(
244
+ <LabeledTextField label="Label" value="" onChange={handleChange} />,
245
+ );
246
+
247
+ // Act
248
+ const newValue = "new value";
249
+ const input = wrapper.find("input");
250
+ input.simulate("change", {target: {value: newValue}});
251
+
252
+ // Assert
253
+ expect(handleChange).toHaveBeenCalledWith(newValue);
254
+ });
255
+
256
+ it("onKeyDown prop is called on keyboard keypress", () => {
257
+ // Arrange
258
+ const handleKeyDown = jest.fn(
259
+ (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
260
+ return event.key;
261
+ },
262
+ );
263
+
264
+ const wrapper = mount(
265
+ <LabeledTextField
266
+ label="Label"
267
+ value=""
268
+ onChange={() => {}}
269
+ onKeyDown={handleKeyDown}
270
+ />,
271
+ );
272
+
273
+ // Act
274
+ const key = "Enter";
275
+ const input = wrapper.find("input");
276
+ input.simulate("keyDown", {key: key});
277
+
278
+ // Assert
279
+ expect(handleKeyDown).toHaveReturnedWith(key);
280
+ });
281
+
282
+ it("onFocus prop is called when field is focused", () => {
283
+ // Arrange
284
+ const handleFocus = jest.fn(() => {});
285
+ const wrapper = mount(
286
+ <LabeledTextField
287
+ label="Label"
288
+ value=""
289
+ onChange={() => {}}
290
+ onFocus={handleFocus}
291
+ />,
292
+ );
293
+
294
+ // Act
295
+ const field = wrapper.find("TextFieldInternal");
296
+ field.simulate("focus");
297
+
298
+ // Assert
299
+ expect(handleFocus).toHaveBeenCalled();
300
+ });
301
+
302
+ it("onBlur prop is called when field is blurred", async () => {
303
+ // Arrange
304
+ const handleBlur = jest.fn(() => {});
305
+ const wrapper = mount(
306
+ <LabeledTextField
307
+ label="Label"
308
+ value=""
309
+ onChange={() => {}}
310
+ onBlur={handleBlur}
311
+ />,
312
+ );
313
+
314
+ // Act
315
+ const field = wrapper.find("TextFieldInternal");
316
+ field.simulate("focus");
317
+ await wait(0);
318
+ field.simulate("blur");
319
+
320
+ // Assert
321
+ expect(handleBlur).toHaveBeenCalled();
322
+ });
323
+
324
+ it("placeholder prop is passed to input", async () => {
325
+ // Arrange
326
+ const placeholder = "Placeholder";
327
+
328
+ // Act
329
+ const wrapper = mount(
330
+ <LabeledTextField
331
+ label="Label"
332
+ value=""
333
+ onChange={() => {}}
334
+ placeholder={placeholder}
335
+ />,
336
+ );
337
+
338
+ // Assert
339
+ const input = wrapper.find("input");
340
+ expect(input).toContainMatchingElement(
341
+ `[placeholder="${placeholder}"]`,
342
+ );
343
+ });
344
+
345
+ it("light prop is passed to textfield", async () => {
346
+ // Arrange
347
+
348
+ // Act
349
+ const wrapper = mount(
350
+ <LabeledTextField
351
+ label="Label"
352
+ value=""
353
+ onChange={() => {}}
354
+ light={true}
355
+ />,
356
+ );
357
+
358
+ // Assert
359
+ const textField = wrapper.find("TextFieldInternal");
360
+ expect(textField).toHaveProp("light", true);
361
+ });
362
+
363
+ it("style prop is passed to fieldheading", async () => {
364
+ // Arrange
365
+ const styles = StyleSheet.create({
366
+ style1: {
367
+ minWidth: 250,
368
+ background: "blue",
369
+ },
370
+ });
371
+
372
+ // Act
373
+ const wrapper = mount(
374
+ <LabeledTextField
375
+ label="Label"
376
+ value=""
377
+ onChange={() => {}}
378
+ style={styles.style1}
379
+ />,
380
+ );
381
+
382
+ // Assert
383
+ const fieldHeading = wrapper.find("FieldHeading");
384
+ expect(fieldHeading).toHaveStyle(styles.style1);
385
+ });
386
+
387
+ it("testId prop is passed to textfield", async () => {
388
+ // Arrange
389
+ const testId = "example-testid";
390
+
391
+ // Act
392
+ const wrapper = mount(
393
+ <LabeledTextField
394
+ label="Label"
395
+ value=""
396
+ onChange={() => {}}
397
+ testId={testId}
398
+ />,
399
+ );
400
+
401
+ // Assert
402
+ const textField = wrapper.find(`[data-test-id="${testId}-field"]`);
403
+ expect(textField).toExist();
404
+ });
405
+
406
+ it("readOnly prop is passed to textfield", async () => {
407
+ // Arrange
408
+
409
+ // Act
410
+ const wrapper = mount(
411
+ <LabeledTextField
412
+ label="Label"
413
+ value=""
414
+ onChange={() => {}}
415
+ readOnly={true}
416
+ />,
417
+ );
418
+
419
+ // Assert
420
+ const textField = wrapper.find("TextFieldInternal");
421
+ expect(textField).toHaveProp("readOnly", true);
422
+ });
423
+
424
+ it("autoComplete prop is passed to textfield", async () => {
425
+ // Arrange
426
+ const autoComplete = "name";
427
+
428
+ // Act
429
+ const wrapper = mount(
430
+ <LabeledTextField
431
+ label="Label"
432
+ value=""
433
+ onChange={() => {}}
434
+ autoComplete={autoComplete}
435
+ />,
436
+ );
437
+
438
+ // Assert
439
+ const textField = wrapper.find("TextFieldInternal");
440
+ expect(textField).toHaveProp("autoComplete", autoComplete);
441
+ });
442
+ });