@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.
- package/LICENSE +21 -0
- package/dist/es/index.js +1100 -0
- package/dist/index.js +1419 -0
- package/dist/index.js.flow +2 -0
- package/docs.md +1 -0
- package/package.json +35 -0
- package/src/__tests__/__snapshots__/custom-snapshot.test.js.snap +1349 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +6126 -0
- package/src/__tests__/custom-snapshot.test.js +66 -0
- package/src/__tests__/generated-snapshot.test.js +654 -0
- package/src/components/__tests__/checkbox-group.test.js +84 -0
- package/src/components/__tests__/field-heading.test.js +182 -0
- package/src/components/__tests__/labeled-text-field.test.js +442 -0
- package/src/components/__tests__/radio-group.test.js +84 -0
- package/src/components/__tests__/text-field.test.js +424 -0
- package/src/components/checkbox-core.js +201 -0
- package/src/components/checkbox-group.js +161 -0
- package/src/components/checkbox-group.md +200 -0
- package/src/components/checkbox.js +94 -0
- package/src/components/checkbox.md +134 -0
- package/src/components/choice-internal.js +206 -0
- package/src/components/choice.js +104 -0
- package/src/components/field-heading.js +157 -0
- package/src/components/field-heading.md +43 -0
- package/src/components/group-styles.js +35 -0
- package/src/components/labeled-text-field.js +265 -0
- package/src/components/labeled-text-field.md +535 -0
- package/src/components/labeled-text-field.stories.js +359 -0
- package/src/components/radio-core.js +176 -0
- package/src/components/radio-group.js +142 -0
- package/src/components/radio-group.md +129 -0
- package/src/components/radio.js +93 -0
- package/src/components/radio.md +26 -0
- package/src/components/text-field.js +326 -0
- package/src/components/text-field.md +770 -0
- package/src/components/text-field.stories.js +513 -0
- package/src/index.js +18 -0
- 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 RadioGroup from "../radio-group.js";
|
|
6
|
+
import Choice from "../choice.js";
|
|
7
|
+
|
|
8
|
+
describe("RadioGroup", () => {
|
|
9
|
+
let group;
|
|
10
|
+
const onChange = jest.fn();
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
group = mount(
|
|
14
|
+
<RadioGroup
|
|
15
|
+
label="Test"
|
|
16
|
+
description="test description"
|
|
17
|
+
groupName="test"
|
|
18
|
+
onChange={onChange}
|
|
19
|
+
selectedValue="a"
|
|
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
|
+
</RadioGroup>,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("selects only one item at a time", () => {
|
|
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(false);
|
|
36
|
+
expect(c.prop("checked")).toEqual(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("changes selection when selectedValue changes", () => {
|
|
40
|
+
group.setProps({selectedValue: "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 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("doesn't change when an already selected item is reselected", () => {
|
|
63
|
+
// a is already selected, onChange shouldn't be called
|
|
64
|
+
const a = group.find(Choice).at(0);
|
|
65
|
+
const aTarget = a.find("ClickableBehavior");
|
|
66
|
+
aTarget.simulate("click");
|
|
67
|
+
expect(onChange).toHaveBeenCalledTimes(0);
|
|
68
|
+
|
|
69
|
+
// now b is clicked, onChange should be called
|
|
70
|
+
const b = group.find(Choice).at(1);
|
|
71
|
+
const bTarget = b.find("ClickableBehavior");
|
|
72
|
+
bTarget.simulate("click");
|
|
73
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
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,424 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {mount} from "enzyme";
|
|
4
|
+
|
|
5
|
+
import TextField from "../text-field.js";
|
|
6
|
+
|
|
7
|
+
const wait = (delay: number = 0) =>
|
|
8
|
+
new Promise((resolve, reject) => {
|
|
9
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
10
|
+
return setTimeout(resolve, delay);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("TextField", () => {
|
|
14
|
+
it("textfield is focused", () => {
|
|
15
|
+
// Arrange
|
|
16
|
+
const wrapper = mount(
|
|
17
|
+
<TextField id="tf-1" value="" onChange={() => {}} />,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
wrapper.simulate("focus");
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(wrapper.find("TextFieldInternal")).toHaveState("focused", true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("onFocus is called after textfield is focused", () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const handleOnFocus = jest.fn(() => {});
|
|
30
|
+
|
|
31
|
+
const wrapper = mount(
|
|
32
|
+
<TextField
|
|
33
|
+
id={"tf-1"}
|
|
34
|
+
value="TextIsLongerThan8"
|
|
35
|
+
onChange={() => {}}
|
|
36
|
+
onFocus={handleOnFocus}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Act
|
|
41
|
+
wrapper.simulate("focus");
|
|
42
|
+
|
|
43
|
+
// Assert
|
|
44
|
+
expect(handleOnFocus).toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("textfield is blurred", async () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
const wrapper = mount(
|
|
50
|
+
<TextField id="tf-1" value="" onChange={() => {}} />,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
wrapper.simulate("focus");
|
|
55
|
+
await wait(0);
|
|
56
|
+
wrapper.simulate("blur");
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
expect(wrapper.find("TextFieldInternal")).toHaveState("focused", false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("onBlur is called after textfield is blurred", async () => {
|
|
63
|
+
// Arrange
|
|
64
|
+
const handleOnBlur = jest.fn(() => {});
|
|
65
|
+
|
|
66
|
+
const wrapper = mount(
|
|
67
|
+
<TextField
|
|
68
|
+
id={"tf-1"}
|
|
69
|
+
value="TextIsLongerThan8"
|
|
70
|
+
onChange={() => {}}
|
|
71
|
+
onBlur={handleOnBlur}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Act
|
|
76
|
+
wrapper.simulate("focus");
|
|
77
|
+
await wait(0);
|
|
78
|
+
wrapper.simulate("blur");
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
81
|
+
expect(handleOnBlur).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("id prop is passed to the input element", () => {
|
|
85
|
+
// Arrange
|
|
86
|
+
const id: string = "tf-1";
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
const wrapper = mount(
|
|
90
|
+
<TextField id={id} value="" onChange={() => {}} />,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
const input = wrapper.find("input");
|
|
95
|
+
expect(input).toContainMatchingElement(`[id="${id}"]`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("type prop is passed to the input element", () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
const type = "number";
|
|
101
|
+
|
|
102
|
+
// Act
|
|
103
|
+
const wrapper = mount(
|
|
104
|
+
<TextField id={"tf-1"} type={type} value="" onChange={() => {}} />,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Assert
|
|
108
|
+
const input = wrapper.find("input");
|
|
109
|
+
expect(input).toContainMatchingElement(`[type="${type}"]`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("value prop is passed to the input element", () => {
|
|
113
|
+
// Arrange
|
|
114
|
+
const value = "Text";
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
const wrapper = mount(
|
|
118
|
+
<TextField id={"tf-1"} value={value} onChange={() => {}} />,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Assert
|
|
122
|
+
const input = wrapper.find("input");
|
|
123
|
+
expect(input).toContainMatchingElement(`[value="${value}"]`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("disabled prop disables the input element", () => {
|
|
127
|
+
// Arrange
|
|
128
|
+
const wrapper = mount(
|
|
129
|
+
<TextField
|
|
130
|
+
id="tf-1"
|
|
131
|
+
value=""
|
|
132
|
+
onChange={() => {}}
|
|
133
|
+
disabled={true}
|
|
134
|
+
/>,
|
|
135
|
+
);
|
|
136
|
+
const input = wrapper.find("input");
|
|
137
|
+
|
|
138
|
+
// Act
|
|
139
|
+
|
|
140
|
+
// Assert
|
|
141
|
+
expect(input).toBeDisabled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("onChange is called when value changes", () => {
|
|
145
|
+
// Arrange
|
|
146
|
+
const handleOnChange = jest.fn();
|
|
147
|
+
|
|
148
|
+
const wrapper = mount(
|
|
149
|
+
<TextField id={"tf-1"} value="Text" onChange={handleOnChange} />,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Act
|
|
153
|
+
const newValue = "Test2";
|
|
154
|
+
wrapper.simulate("change", {target: {value: newValue}});
|
|
155
|
+
|
|
156
|
+
// Assert
|
|
157
|
+
expect(handleOnChange).toHaveBeenCalledWith(newValue);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("validate is called when value changes", () => {
|
|
161
|
+
// Arrange
|
|
162
|
+
const handleValidate = jest.fn((value: string): ?string => {});
|
|
163
|
+
|
|
164
|
+
const wrapper = mount(
|
|
165
|
+
<TextField
|
|
166
|
+
id={"tf-1"}
|
|
167
|
+
value="Text"
|
|
168
|
+
validate={handleValidate}
|
|
169
|
+
onChange={() => {}}
|
|
170
|
+
/>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Act
|
|
174
|
+
const newValue = "Text2";
|
|
175
|
+
wrapper.simulate("change", {target: {value: newValue}});
|
|
176
|
+
|
|
177
|
+
// Assert
|
|
178
|
+
expect(handleValidate).toHaveBeenCalledWith(newValue);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("validate is given a valid input", () => {
|
|
182
|
+
// Arrange
|
|
183
|
+
const handleValidate = jest.fn((value: string): ?string => {
|
|
184
|
+
if (value.length < 8) {
|
|
185
|
+
return "Value is too short";
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const wrapper = mount(
|
|
190
|
+
<TextField
|
|
191
|
+
id={"tf-1"}
|
|
192
|
+
value="TextIsLong"
|
|
193
|
+
validate={handleValidate}
|
|
194
|
+
onChange={() => {}}
|
|
195
|
+
/>,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Act
|
|
199
|
+
const newValue = "TextIsLongerThan8";
|
|
200
|
+
wrapper.simulate("change", {target: {value: newValue}});
|
|
201
|
+
|
|
202
|
+
// Assert
|
|
203
|
+
expect(handleValidate).toHaveReturnedWith(undefined);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("validate is given an invalid input", () => {
|
|
207
|
+
// Arrange
|
|
208
|
+
const errorMessage = "Value is too short";
|
|
209
|
+
const handleValidate = jest.fn((value: string): ?string => {
|
|
210
|
+
if (value.length < 8) {
|
|
211
|
+
return errorMessage;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const wrapper = mount(
|
|
216
|
+
<TextField
|
|
217
|
+
id={"tf-1"}
|
|
218
|
+
value="TextIsLongerThan8"
|
|
219
|
+
validate={handleValidate}
|
|
220
|
+
onChange={() => {}}
|
|
221
|
+
/>,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Act
|
|
225
|
+
const newValue = "Text";
|
|
226
|
+
wrapper.simulate("change", {target: {value: newValue}});
|
|
227
|
+
|
|
228
|
+
// Assert
|
|
229
|
+
expect(handleValidate).toHaveReturnedWith(errorMessage);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("onValidate is called after input validate", () => {
|
|
233
|
+
// Arrange
|
|
234
|
+
const errorMessage = "Value is too short";
|
|
235
|
+
const handleValidate = jest.fn((errorMessage: ?string) => {});
|
|
236
|
+
const validate = jest.fn((value: string): ?string => {
|
|
237
|
+
if (value.length < 8) {
|
|
238
|
+
return errorMessage;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const wrapper = mount(
|
|
243
|
+
<TextField
|
|
244
|
+
id={"tf-1"}
|
|
245
|
+
value="TextIsLongerThan8"
|
|
246
|
+
validate={validate}
|
|
247
|
+
onValidate={handleValidate}
|
|
248
|
+
onChange={() => {}}
|
|
249
|
+
/>,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Act
|
|
253
|
+
const newValue = "Text";
|
|
254
|
+
wrapper.simulate("change", {target: {value: newValue}});
|
|
255
|
+
|
|
256
|
+
// Assert
|
|
257
|
+
expect(handleValidate).toHaveBeenCalledWith(errorMessage);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("onValidate is called on input's initial value", () => {
|
|
261
|
+
// Arrange
|
|
262
|
+
const errorMessage = "Value is too short";
|
|
263
|
+
const handleValidate = jest.fn((errorMessage: ?string) => {});
|
|
264
|
+
const validate = jest.fn((value: string): ?string => {
|
|
265
|
+
if (value.length < 8) {
|
|
266
|
+
return errorMessage;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Act
|
|
271
|
+
mount(
|
|
272
|
+
<TextField
|
|
273
|
+
id={"tf-1"}
|
|
274
|
+
value="Short"
|
|
275
|
+
validate={validate}
|
|
276
|
+
onValidate={handleValidate}
|
|
277
|
+
onChange={() => {}}
|
|
278
|
+
/>,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Assert
|
|
282
|
+
expect(handleValidate).toHaveBeenCalledWith(errorMessage);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("onKeyDown is called after keyboard key press", () => {
|
|
286
|
+
// Arrange
|
|
287
|
+
const handleOnKeyDown = jest.fn(
|
|
288
|
+
(event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
289
|
+
return event.key;
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const wrapper = mount(
|
|
294
|
+
<TextField
|
|
295
|
+
id={"tf-1"}
|
|
296
|
+
value="TextIsLongerThan8"
|
|
297
|
+
onChange={() => {}}
|
|
298
|
+
onKeyDown={handleOnKeyDown}
|
|
299
|
+
/>,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Act
|
|
303
|
+
const key = "Enter";
|
|
304
|
+
const input = wrapper.find("input");
|
|
305
|
+
input.simulate("keyDown", {key: key});
|
|
306
|
+
|
|
307
|
+
// Assert
|
|
308
|
+
expect(handleOnKeyDown).toHaveReturnedWith(key);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("placeholder prop is passed to the input element", () => {
|
|
312
|
+
// Arrange
|
|
313
|
+
const placeholder = "Placeholder";
|
|
314
|
+
|
|
315
|
+
// Act
|
|
316
|
+
const wrapper = mount(
|
|
317
|
+
<TextField
|
|
318
|
+
id={"tf-1"}
|
|
319
|
+
value="Text"
|
|
320
|
+
placeholder={placeholder}
|
|
321
|
+
onChange={() => {}}
|
|
322
|
+
/>,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Assert
|
|
326
|
+
const input = wrapper.find("input");
|
|
327
|
+
expect(input).toContainMatchingElement(
|
|
328
|
+
`[placeholder="${placeholder}"]`,
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("required prop is passed to the input element", () => {
|
|
333
|
+
// Arrange
|
|
334
|
+
const wrapper = mount(
|
|
335
|
+
<TextField
|
|
336
|
+
id={"tf-1"}
|
|
337
|
+
value="Text"
|
|
338
|
+
onChange={() => {}}
|
|
339
|
+
required={true}
|
|
340
|
+
/>,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Act
|
|
344
|
+
|
|
345
|
+
// Assert
|
|
346
|
+
const input = wrapper.find("input");
|
|
347
|
+
expect(input).toContainMatchingElement("[required=true]");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("testId is passed to the input element", () => {
|
|
351
|
+
// Arrange
|
|
352
|
+
const testId = "some-test-id";
|
|
353
|
+
const wrapper = mount(
|
|
354
|
+
<TextField
|
|
355
|
+
id={"tf-1"}
|
|
356
|
+
value="Text"
|
|
357
|
+
onChange={() => {}}
|
|
358
|
+
testId={testId}
|
|
359
|
+
/>,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Act
|
|
363
|
+
|
|
364
|
+
// Assert
|
|
365
|
+
const input = wrapper.find("input");
|
|
366
|
+
expect(input).toContainMatchingElement(`[data-test-id="${testId}"]`);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("aria props are passed to the input element", () => {
|
|
370
|
+
// Arrange
|
|
371
|
+
const ariaLabel = "example-text-field";
|
|
372
|
+
const wrapper = mount(
|
|
373
|
+
<TextField
|
|
374
|
+
id={"tf-1"}
|
|
375
|
+
value="Text"
|
|
376
|
+
onChange={() => {}}
|
|
377
|
+
aria-label={ariaLabel}
|
|
378
|
+
/>,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Act
|
|
382
|
+
|
|
383
|
+
// Assert
|
|
384
|
+
const input = wrapper.find("input");
|
|
385
|
+
expect(input).toContainMatchingElement(`[aria-label="${ariaLabel}"]`);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("readOnly prop is passed to the input element", async () => {
|
|
389
|
+
// Arrange
|
|
390
|
+
|
|
391
|
+
// Act
|
|
392
|
+
const wrapper = mount(
|
|
393
|
+
<TextField
|
|
394
|
+
id={"tf-1"}
|
|
395
|
+
value={"Text"}
|
|
396
|
+
onChange={() => {}}
|
|
397
|
+
readOnly={true}
|
|
398
|
+
/>,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Assert
|
|
402
|
+
const input = wrapper.find("input");
|
|
403
|
+
expect(input).toHaveProp("readOnly");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("autoComplete prop is passed to the input element", async () => {
|
|
407
|
+
// Arrange
|
|
408
|
+
const autoComplete = "name";
|
|
409
|
+
|
|
410
|
+
// Act
|
|
411
|
+
const wrapper = mount(
|
|
412
|
+
<TextField
|
|
413
|
+
id={"tf-1"}
|
|
414
|
+
value={"Text"}
|
|
415
|
+
onChange={() => {}}
|
|
416
|
+
autoComplete={autoComplete}
|
|
417
|
+
/>,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// Assert
|
|
421
|
+
const input = wrapper.find("input");
|
|
422
|
+
expect(input).toHaveProp("autoComplete", autoComplete);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {StyleSheet} from "aphrodite";
|
|
5
|
+
|
|
6
|
+
import Color, {mix, fade} from "@khanacademy/wonder-blocks-color";
|
|
7
|
+
import {addStyle} from "@khanacademy/wonder-blocks-core";
|
|
8
|
+
import Icon from "@khanacademy/wonder-blocks-icon";
|
|
9
|
+
|
|
10
|
+
import type {IconAsset} from "@khanacademy/wonder-blocks-icon";
|
|
11
|
+
|
|
12
|
+
import type {ChoiceCoreProps} from "../util/types.js";
|
|
13
|
+
|
|
14
|
+
type Props = {|
|
|
15
|
+
...ChoiceCoreProps,
|
|
16
|
+
hovered: boolean,
|
|
17
|
+
focused: boolean,
|
|
18
|
+
pressed: boolean,
|
|
19
|
+
waiting: boolean,
|
|
20
|
+
|};
|
|
21
|
+
|
|
22
|
+
const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = Color;
|
|
23
|
+
|
|
24
|
+
const StyledInput = addStyle("input");
|
|
25
|
+
|
|
26
|
+
const checkboxCheck: IconAsset = {
|
|
27
|
+
small:
|
|
28
|
+
"M11.263 4.324a1 1 0 1 1 1.474 1.352l-5.5 6a1 1 0 0 1-1.505-.036l-2.5-3a1 1 0 1 1 1.536-1.28L6.536 9.48l4.727-5.157z",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The internal stateless ☑️ Checkbox
|
|
33
|
+
*/
|
|
34
|
+
export default class CheckboxCore extends React.Component<Props> {
|
|
35
|
+
handleChange: () => void = () => {
|
|
36
|
+
// Empty because change is handled by ClickableBehavior
|
|
37
|
+
return;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
render(): React.Node {
|
|
41
|
+
const {
|
|
42
|
+
checked,
|
|
43
|
+
disabled,
|
|
44
|
+
error,
|
|
45
|
+
groupName,
|
|
46
|
+
id,
|
|
47
|
+
testId,
|
|
48
|
+
hovered,
|
|
49
|
+
focused,
|
|
50
|
+
pressed,
|
|
51
|
+
waiting: _,
|
|
52
|
+
...sharedProps
|
|
53
|
+
} = this.props;
|
|
54
|
+
|
|
55
|
+
const stateStyles = _generateStyles(checked, error);
|
|
56
|
+
|
|
57
|
+
const defaultStyle = [
|
|
58
|
+
sharedStyles.inputReset,
|
|
59
|
+
sharedStyles.default,
|
|
60
|
+
stateStyles.default,
|
|
61
|
+
!disabled &&
|
|
62
|
+
(pressed
|
|
63
|
+
? stateStyles.active
|
|
64
|
+
: (hovered || focused) && stateStyles.focus),
|
|
65
|
+
disabled && sharedStyles.disabled,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const props = {
|
|
69
|
+
"data-test-id": testId,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<React.Fragment>
|
|
74
|
+
<StyledInput
|
|
75
|
+
{...sharedProps}
|
|
76
|
+
type="checkbox"
|
|
77
|
+
aria-invalid={error}
|
|
78
|
+
checked={checked}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
id={id}
|
|
81
|
+
name={groupName}
|
|
82
|
+
// Need to specify because this is a controlled React form
|
|
83
|
+
// component, but we handle the click via ClickableBehavior
|
|
84
|
+
onChange={this.handleChange}
|
|
85
|
+
style={defaultStyle}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
{checked && (
|
|
89
|
+
<Icon
|
|
90
|
+
color={disabled ? offBlack32 : white}
|
|
91
|
+
icon={checkboxCheck}
|
|
92
|
+
size="small"
|
|
93
|
+
style={sharedStyles.checkIcon}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</React.Fragment>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const size = 16;
|
|
102
|
+
|
|
103
|
+
const sharedStyles = StyleSheet.create({
|
|
104
|
+
// Reset the default styled input element
|
|
105
|
+
inputReset: {
|
|
106
|
+
appearance: "none",
|
|
107
|
+
WebkitAppearance: "none",
|
|
108
|
+
MozAppearance: "none",
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
default: {
|
|
112
|
+
height: size,
|
|
113
|
+
width: size,
|
|
114
|
+
minHeight: size,
|
|
115
|
+
minWidth: size,
|
|
116
|
+
margin: 0,
|
|
117
|
+
outline: "none",
|
|
118
|
+
boxSizing: "border-box",
|
|
119
|
+
borderStyle: "solid",
|
|
120
|
+
borderWidth: 1,
|
|
121
|
+
borderRadius: 3,
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
disabled: {
|
|
125
|
+
cursor: "auto",
|
|
126
|
+
backgroundColor: offWhite,
|
|
127
|
+
borderColor: offBlack16,
|
|
128
|
+
borderWidth: 1,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
checkIcon: {
|
|
132
|
+
position: "absolute",
|
|
133
|
+
pointerEvents: "none",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const fadedBlue = mix(fade(blue, 0.16), white);
|
|
138
|
+
const activeBlue = mix(offBlack32, blue);
|
|
139
|
+
const fadedRed = mix(fade(red, 0.08), white);
|
|
140
|
+
const activeRed = mix(offBlack32, red);
|
|
141
|
+
|
|
142
|
+
const colors = {
|
|
143
|
+
default: {
|
|
144
|
+
faded: fadedBlue,
|
|
145
|
+
base: blue,
|
|
146
|
+
active: activeBlue,
|
|
147
|
+
},
|
|
148
|
+
error: {
|
|
149
|
+
faded: fadedRed,
|
|
150
|
+
base: red,
|
|
151
|
+
active: activeRed,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const styles = {};
|
|
156
|
+
|
|
157
|
+
const _generateStyles = (checked, error) => {
|
|
158
|
+
// "hash" the parameters
|
|
159
|
+
const styleKey = `${String(checked)}-${String(error)}`;
|
|
160
|
+
if (styles[styleKey]) {
|
|
161
|
+
return styles[styleKey];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const palette = error ? colors.error : colors.default;
|
|
165
|
+
|
|
166
|
+
let newStyles = {};
|
|
167
|
+
if (checked) {
|
|
168
|
+
newStyles = {
|
|
169
|
+
default: {
|
|
170
|
+
backgroundColor: palette.base,
|
|
171
|
+
borderWidth: 0,
|
|
172
|
+
},
|
|
173
|
+
focus: {
|
|
174
|
+
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
|
|
175
|
+
},
|
|
176
|
+
active: {
|
|
177
|
+
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
|
|
178
|
+
background: palette.active,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
} else {
|
|
182
|
+
newStyles = {
|
|
183
|
+
default: {
|
|
184
|
+
backgroundColor: error ? fadedRed : white,
|
|
185
|
+
borderColor: error ? red : offBlack50,
|
|
186
|
+
},
|
|
187
|
+
focus: {
|
|
188
|
+
backgroundColor: error ? fadedRed : white,
|
|
189
|
+
borderColor: palette.base,
|
|
190
|
+
borderWidth: 2,
|
|
191
|
+
},
|
|
192
|
+
active: {
|
|
193
|
+
backgroundColor: palette.faded,
|
|
194
|
+
borderColor: error ? activeRed : blue,
|
|
195
|
+
borderWidth: 2,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
styles[styleKey] = StyleSheet.create(newStyles);
|
|
200
|
+
return styles[styleKey];
|
|
201
|
+
};
|