@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,359 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import {LabeledTextField} from "@khanacademy/wonder-blocks-form";
|
|
5
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
6
|
+
import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
|
|
7
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
8
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
9
|
+
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
10
|
+
import Button from "@khanacademy/wonder-blocks-button";
|
|
11
|
+
import {StyleSheet} from "aphrodite";
|
|
12
|
+
|
|
13
|
+
import type {StoryComponentType} from "@storybook/react";
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
title: "Form / LabeledTextField",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const text: StoryComponentType = () => {
|
|
20
|
+
const [value, setValue] = React.useState("Khan");
|
|
21
|
+
|
|
22
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
23
|
+
if (event.key === "Enter") {
|
|
24
|
+
event.currentTarget.blur();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<LabeledTextField
|
|
30
|
+
label="Name"
|
|
31
|
+
description="Please enter your name"
|
|
32
|
+
value={value}
|
|
33
|
+
onChange={(newValue) => setValue(newValue)}
|
|
34
|
+
placeholder="Name"
|
|
35
|
+
onKeyDown={handleKeyDown}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const number: StoryComponentType = () => {
|
|
41
|
+
const [value, setValue] = React.useState("18");
|
|
42
|
+
|
|
43
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
44
|
+
if (event.key === "Enter") {
|
|
45
|
+
event.currentTarget.blur();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<LabeledTextField
|
|
51
|
+
label="Age"
|
|
52
|
+
type="number"
|
|
53
|
+
description="Please enter your age"
|
|
54
|
+
value={value}
|
|
55
|
+
onChange={(newValue) => setValue(newValue)}
|
|
56
|
+
placeholder="Age"
|
|
57
|
+
onKeyDown={handleKeyDown}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const password: StoryComponentType = () => {
|
|
63
|
+
const [value, setValue] = React.useState("Password123");
|
|
64
|
+
|
|
65
|
+
const validate = (value: string) => {
|
|
66
|
+
if (value.length < 8) {
|
|
67
|
+
return "Password must be at least 8 characters long";
|
|
68
|
+
}
|
|
69
|
+
if (!/\d/.test(value)) {
|
|
70
|
+
return "Password must contain a numeric value";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
75
|
+
if (event.key === "Enter") {
|
|
76
|
+
event.currentTarget.blur();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<LabeledTextField
|
|
82
|
+
label="Password"
|
|
83
|
+
type="password"
|
|
84
|
+
description="Please enter a secure password"
|
|
85
|
+
value={value}
|
|
86
|
+
onChange={(newValue) => setValue(newValue)}
|
|
87
|
+
placeholder="Password"
|
|
88
|
+
validate={validate}
|
|
89
|
+
onKeyDown={handleKeyDown}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const email: StoryComponentType = () => {
|
|
95
|
+
const [value, setValue] = React.useState("khan@khan.org");
|
|
96
|
+
|
|
97
|
+
const validate = (value: string) => {
|
|
98
|
+
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/;
|
|
99
|
+
if (!emailRegex.test(value)) {
|
|
100
|
+
return "Please enter a valid email";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
105
|
+
if (event.key === "Enter") {
|
|
106
|
+
event.currentTarget.blur();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<LabeledTextField
|
|
112
|
+
label="Email"
|
|
113
|
+
type="email"
|
|
114
|
+
value={value}
|
|
115
|
+
onChange={(newValue) => setValue(newValue)}
|
|
116
|
+
description="Please provide your personal email"
|
|
117
|
+
placeholder="Email"
|
|
118
|
+
validate={validate}
|
|
119
|
+
onKeyDown={handleKeyDown}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const telephone: StoryComponentType = () => {
|
|
125
|
+
const [value, setValue] = React.useState("123-456-7890");
|
|
126
|
+
|
|
127
|
+
const validate = (value: string) => {
|
|
128
|
+
const telRegex = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/;
|
|
129
|
+
if (!telRegex.test(value)) {
|
|
130
|
+
return "Invalid US telephone number";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
135
|
+
if (event.key === "Enter") {
|
|
136
|
+
event.currentTarget.blur();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<LabeledTextField
|
|
142
|
+
label="Telephone"
|
|
143
|
+
type="tel"
|
|
144
|
+
value={value}
|
|
145
|
+
onChange={(newValue) => setValue(newValue)}
|
|
146
|
+
description="Please provide your personal phone number"
|
|
147
|
+
placeholder="Telephone"
|
|
148
|
+
validate={validate}
|
|
149
|
+
onKeyDown={handleKeyDown}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const error: StoryComponentType = () => {
|
|
155
|
+
const [value, setValue] = React.useState("khan");
|
|
156
|
+
|
|
157
|
+
const validate = (value: string) => {
|
|
158
|
+
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/;
|
|
159
|
+
if (!emailRegex.test(value)) {
|
|
160
|
+
return "Please enter a valid email";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
165
|
+
if (event.key === "Enter") {
|
|
166
|
+
event.currentTarget.blur();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<LabeledTextField
|
|
172
|
+
label="Email"
|
|
173
|
+
type="email"
|
|
174
|
+
value={value}
|
|
175
|
+
onChange={(newValue) => setValue(newValue)}
|
|
176
|
+
description="Please provide your personal email"
|
|
177
|
+
placeholder="Email"
|
|
178
|
+
validate={validate}
|
|
179
|
+
onKeyDown={handleKeyDown}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const disabled: StoryComponentType = () => (
|
|
185
|
+
<LabeledTextField
|
|
186
|
+
label="Name"
|
|
187
|
+
description="Please enter your name"
|
|
188
|
+
value=""
|
|
189
|
+
onChange={() => {}}
|
|
190
|
+
placeholder="Name"
|
|
191
|
+
disabled={true}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
export const light: StoryComponentType = () => {
|
|
196
|
+
const [value, setValue] = React.useState("");
|
|
197
|
+
|
|
198
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
199
|
+
if (event.key === "Enter") {
|
|
200
|
+
event.currentTarget.blur();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<View style={styles.darkBackground}>
|
|
206
|
+
<LabeledTextField
|
|
207
|
+
label={
|
|
208
|
+
<LabelMedium style={styles.whiteColor}>Name</LabelMedium>
|
|
209
|
+
}
|
|
210
|
+
description={
|
|
211
|
+
<LabelSmall style={styles.offWhiteColor}>
|
|
212
|
+
Please enter your name
|
|
213
|
+
</LabelSmall>
|
|
214
|
+
}
|
|
215
|
+
value={value}
|
|
216
|
+
onChange={(newValue) => setValue(newValue)}
|
|
217
|
+
placeholder="Name"
|
|
218
|
+
light={true}
|
|
219
|
+
onKeyDown={handleKeyDown}
|
|
220
|
+
/>
|
|
221
|
+
</View>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const customStyle: StoryComponentType = () => {
|
|
226
|
+
const [firstName, setFirstName] = React.useState("");
|
|
227
|
+
const [lastName, setLastName] = React.useState("");
|
|
228
|
+
|
|
229
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
230
|
+
if (event.key === "Enter") {
|
|
231
|
+
event.currentTarget.blur();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<View style={styles.row}>
|
|
237
|
+
<LabeledTextField
|
|
238
|
+
label="First name"
|
|
239
|
+
description="Please enter your first name"
|
|
240
|
+
value={firstName}
|
|
241
|
+
onChange={(newValue) => setFirstName(newValue)}
|
|
242
|
+
placeholder="Khan"
|
|
243
|
+
style={styles.grow}
|
|
244
|
+
onKeyDown={handleKeyDown}
|
|
245
|
+
/>
|
|
246
|
+
<Strut size={Spacing.xLarge_32} />
|
|
247
|
+
<LabeledTextField
|
|
248
|
+
label="Last name"
|
|
249
|
+
description="Please enter your last name"
|
|
250
|
+
value={lastName}
|
|
251
|
+
onChange={(newValue) => setLastName(newValue)}
|
|
252
|
+
placeholder="Academy"
|
|
253
|
+
style={styles.grow}
|
|
254
|
+
onKeyDown={handleKeyDown}
|
|
255
|
+
/>
|
|
256
|
+
</View>
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const ref: StoryComponentType = () => {
|
|
261
|
+
const [value, setValue] = React.useState("Khan");
|
|
262
|
+
const inputRef = React.createRef<HTMLInputElement>();
|
|
263
|
+
|
|
264
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
265
|
+
if (event.key === "Enter") {
|
|
266
|
+
event.currentTarget.blur();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const handleSubmit = () => {
|
|
271
|
+
if (inputRef.current) {
|
|
272
|
+
inputRef.current.focus();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<View>
|
|
278
|
+
<LabeledTextField
|
|
279
|
+
label="Name"
|
|
280
|
+
description="Please enter your name"
|
|
281
|
+
value={value}
|
|
282
|
+
onChange={(newValue) => setValue(newValue)}
|
|
283
|
+
placeholder="Name"
|
|
284
|
+
onKeyDown={handleKeyDown}
|
|
285
|
+
ref={inputRef}
|
|
286
|
+
/>
|
|
287
|
+
<Strut size={Spacing.medium_16} />
|
|
288
|
+
<Button style={styles.button} onClick={handleSubmit}>
|
|
289
|
+
Focus Input
|
|
290
|
+
</Button>
|
|
291
|
+
</View>
|
|
292
|
+
);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export const readOnly: StoryComponentType = () => {
|
|
296
|
+
const [value, setValue] = React.useState("Khan");
|
|
297
|
+
|
|
298
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
299
|
+
if (event.key === "Enter") {
|
|
300
|
+
event.currentTarget.blur();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<LabeledTextField
|
|
306
|
+
label="Read Only"
|
|
307
|
+
description="This is a read-only field."
|
|
308
|
+
value={value}
|
|
309
|
+
onChange={(newValue) => setValue(newValue)}
|
|
310
|
+
placeholder="Name"
|
|
311
|
+
onKeyDown={handleKeyDown}
|
|
312
|
+
readOnly={true}
|
|
313
|
+
/>
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const autoComplete: StoryComponentType = () => {
|
|
318
|
+
const [value, setValue] = React.useState("");
|
|
319
|
+
|
|
320
|
+
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
|
321
|
+
if (event.key === "Enter") {
|
|
322
|
+
event.currentTarget.blur();
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<LabeledTextField
|
|
328
|
+
label="Name"
|
|
329
|
+
description="Please enter your name."
|
|
330
|
+
value={value}
|
|
331
|
+
onChange={(newValue) => setValue(newValue)}
|
|
332
|
+
placeholder="Name"
|
|
333
|
+
onKeyDown={handleKeyDown}
|
|
334
|
+
autoComplete="name"
|
|
335
|
+
/>
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const styles = StyleSheet.create({
|
|
340
|
+
darkBackground: {
|
|
341
|
+
background: Color.darkBlue,
|
|
342
|
+
padding: `${Spacing.medium_16}px`,
|
|
343
|
+
},
|
|
344
|
+
whiteColor: {
|
|
345
|
+
color: Color.white,
|
|
346
|
+
},
|
|
347
|
+
offWhiteColor: {
|
|
348
|
+
color: Color.white64,
|
|
349
|
+
},
|
|
350
|
+
button: {
|
|
351
|
+
maxWidth: 150,
|
|
352
|
+
},
|
|
353
|
+
row: {
|
|
354
|
+
flexDirection: "row",
|
|
355
|
+
},
|
|
356
|
+
grow: {
|
|
357
|
+
flexGrow: 1,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
|
|
9
|
+
import type {ChoiceCoreProps} from "../util/types.js";
|
|
10
|
+
|
|
11
|
+
type Props = {|
|
|
12
|
+
...ChoiceCoreProps,
|
|
13
|
+
hovered: boolean,
|
|
14
|
+
focused: boolean,
|
|
15
|
+
pressed: boolean,
|
|
16
|
+
waiting: boolean,
|
|
17
|
+
|};
|
|
18
|
+
|
|
19
|
+
const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = Color;
|
|
20
|
+
|
|
21
|
+
const StyledInput = addStyle("input");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The internal stateless 🔘 Radio button
|
|
25
|
+
*/ export default class RadioCore extends React.Component<Props> {
|
|
26
|
+
handleChange: () => void = () => {
|
|
27
|
+
// Empty because change is handled by ClickableBehavior
|
|
28
|
+
return;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
render(): React.Node {
|
|
32
|
+
const {
|
|
33
|
+
checked,
|
|
34
|
+
disabled,
|
|
35
|
+
error,
|
|
36
|
+
groupName,
|
|
37
|
+
id,
|
|
38
|
+
testId,
|
|
39
|
+
hovered,
|
|
40
|
+
focused,
|
|
41
|
+
pressed,
|
|
42
|
+
waiting: _,
|
|
43
|
+
...sharedProps
|
|
44
|
+
} = this.props;
|
|
45
|
+
const stateStyles = _generateStyles(checked, error);
|
|
46
|
+
const defaultStyle = [
|
|
47
|
+
sharedStyles.inputReset,
|
|
48
|
+
sharedStyles.default,
|
|
49
|
+
stateStyles.default,
|
|
50
|
+
!disabled &&
|
|
51
|
+
(pressed
|
|
52
|
+
? stateStyles.active
|
|
53
|
+
: (hovered || focused) && stateStyles.focus),
|
|
54
|
+
disabled && sharedStyles.disabled,
|
|
55
|
+
];
|
|
56
|
+
const props = {
|
|
57
|
+
"data-test-id": testId,
|
|
58
|
+
};
|
|
59
|
+
return (
|
|
60
|
+
<React.Fragment>
|
|
61
|
+
<StyledInput
|
|
62
|
+
{...sharedProps}
|
|
63
|
+
type="radio"
|
|
64
|
+
aria-invalid={error}
|
|
65
|
+
checked={checked}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
id={id}
|
|
68
|
+
name={groupName}
|
|
69
|
+
// Need to specify because this is a controlled React form
|
|
70
|
+
// component, but we handle the click via ClickableBehavior
|
|
71
|
+
onChange={this.handleChange}
|
|
72
|
+
style={defaultStyle}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
{disabled && checked && <span style={disabledChecked} />}
|
|
76
|
+
</React.Fragment>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const size = 16; // circle with a different color. Here, we add that center circle. // If the checkbox is disabled and selected, it has a border but also an inner
|
|
81
|
+
const disabledChecked = {
|
|
82
|
+
position: "absolute",
|
|
83
|
+
top: size / 4,
|
|
84
|
+
left: size / 4,
|
|
85
|
+
height: size / 2,
|
|
86
|
+
width: size / 2,
|
|
87
|
+
borderRadius: "50%",
|
|
88
|
+
backgroundColor: offBlack32,
|
|
89
|
+
};
|
|
90
|
+
const sharedStyles = StyleSheet.create({
|
|
91
|
+
// Reset the default styled input element
|
|
92
|
+
inputReset: {
|
|
93
|
+
appearance: "none",
|
|
94
|
+
WebkitAppearance: "none",
|
|
95
|
+
MozAppearance: "none",
|
|
96
|
+
},
|
|
97
|
+
default: {
|
|
98
|
+
height: size,
|
|
99
|
+
width: size,
|
|
100
|
+
minHeight: size,
|
|
101
|
+
minWidth: size,
|
|
102
|
+
margin: 0,
|
|
103
|
+
outline: "none",
|
|
104
|
+
boxSizing: "border-box",
|
|
105
|
+
borderStyle: "solid",
|
|
106
|
+
borderWidth: 1,
|
|
107
|
+
borderRadius: "50%",
|
|
108
|
+
},
|
|
109
|
+
disabled: {
|
|
110
|
+
cursor: "auto",
|
|
111
|
+
backgroundColor: offWhite,
|
|
112
|
+
borderColor: offBlack16,
|
|
113
|
+
borderWidth: 1,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const fadedBlue = mix(fade(blue, 0.16), white);
|
|
117
|
+
const activeBlue = mix(offBlack32, blue);
|
|
118
|
+
const fadedRed = mix(fade(red, 0.08), white);
|
|
119
|
+
const activeRed = mix(offBlack32, red);
|
|
120
|
+
const colors = {
|
|
121
|
+
default: {
|
|
122
|
+
faded: fadedBlue,
|
|
123
|
+
base: blue,
|
|
124
|
+
active: activeBlue,
|
|
125
|
+
},
|
|
126
|
+
error: {
|
|
127
|
+
faded: fadedRed,
|
|
128
|
+
base: red,
|
|
129
|
+
active: activeRed,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const styles = {};
|
|
133
|
+
const _generateStyles = (checked, error) => {
|
|
134
|
+
// "hash" the parameters
|
|
135
|
+
const styleKey = `${String(checked)}-${String(error)}`;
|
|
136
|
+
if (styles[styleKey]) {
|
|
137
|
+
return styles[styleKey];
|
|
138
|
+
}
|
|
139
|
+
const palette = error ? colors.error : colors.default;
|
|
140
|
+
let newStyles = {};
|
|
141
|
+
if (checked) {
|
|
142
|
+
newStyles = {
|
|
143
|
+
default: {
|
|
144
|
+
backgroundColor: white,
|
|
145
|
+
borderColor: palette.base,
|
|
146
|
+
borderWidth: size / 4,
|
|
147
|
+
},
|
|
148
|
+
focus: {
|
|
149
|
+
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
|
|
150
|
+
},
|
|
151
|
+
active: {
|
|
152
|
+
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
|
|
153
|
+
borderColor: palette.active,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
} else {
|
|
157
|
+
newStyles = {
|
|
158
|
+
default: {
|
|
159
|
+
backgroundColor: error ? fadedRed : white,
|
|
160
|
+
borderColor: error ? red : offBlack50,
|
|
161
|
+
},
|
|
162
|
+
focus: {
|
|
163
|
+
backgroundColor: error ? fadedRed : white,
|
|
164
|
+
borderColor: palette.base,
|
|
165
|
+
borderWidth: 2,
|
|
166
|
+
},
|
|
167
|
+
active: {
|
|
168
|
+
backgroundColor: palette.faded,
|
|
169
|
+
borderColor: error ? activeRed : blue,
|
|
170
|
+
borderWidth: 2,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
styles[styleKey] = StyleSheet.create(newStyles);
|
|
175
|
+
return styles[styleKey];
|
|
176
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import {View, addStyle} from "@khanacademy/wonder-blocks-core";
|
|
6
|
+
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
7
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
8
|
+
import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
|
|
9
|
+
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
10
|
+
|
|
11
|
+
import styles from "./group-styles.js";
|
|
12
|
+
import typeof Choice from "./choice.js";
|
|
13
|
+
|
|
14
|
+
// Keep synced with RadioGroupProps in ../util/types.js
|
|
15
|
+
type RadioGroupProps = {|
|
|
16
|
+
/**
|
|
17
|
+
* Children should be Choice components.
|
|
18
|
+
*/
|
|
19
|
+
children: Array<React.Element<Choice>>,
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Group name for this checkbox or radio group. Should be unique for all
|
|
23
|
+
* such groups displayed on a page.
|
|
24
|
+
*/
|
|
25
|
+
groupName: string,
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional label for the group. This label is optional to allow for
|
|
29
|
+
* greater flexibility in implementing checkbox and radio groups.
|
|
30
|
+
*/
|
|
31
|
+
label?: string,
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Optional description for the group.
|
|
35
|
+
*/
|
|
36
|
+
description?: string,
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional error message. If supplied, the group will be displayed in an
|
|
40
|
+
* error state, along with this error message. If no error state is desired,
|
|
41
|
+
* simply do not supply this prop, or pass along null.
|
|
42
|
+
*/
|
|
43
|
+
errorMessage?: string,
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Custom styling for this group of checkboxes.
|
|
47
|
+
*/
|
|
48
|
+
style?: StyleType,
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Callback for when the selected value of the radio group has changed.
|
|
52
|
+
*/
|
|
53
|
+
onChange: (selectedValue: string) => mixed,
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Value of the selected radio item.
|
|
57
|
+
*/
|
|
58
|
+
selectedValue: string,
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Test ID used for e2e testing.
|
|
62
|
+
*/
|
|
63
|
+
testId?: string,
|
|
64
|
+
|};
|
|
65
|
+
|
|
66
|
+
const StyledFieldset = addStyle<"fieldset">("fieldset");
|
|
67
|
+
const StyledLegend = addStyle<"legend">("legend");
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A radio group allows only single selection. Like CheckboxGroup, this
|
|
71
|
+
* component auto-populates many props for its children Choice components. The
|
|
72
|
+
* Choice component is exposed for the user to apply custom styles or to
|
|
73
|
+
* indicate which choices are disabled. The use of the groupName prop is
|
|
74
|
+
* important to maintain expected keyboard navigation behavior for
|
|
75
|
+
* accessibility.
|
|
76
|
+
*/
|
|
77
|
+
export default class RadioGroup extends React.Component<RadioGroupProps> {
|
|
78
|
+
handleChange(changedValue: string) {
|
|
79
|
+
this.props.onChange(changedValue);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render(): React.Node {
|
|
83
|
+
const {
|
|
84
|
+
children,
|
|
85
|
+
label,
|
|
86
|
+
description,
|
|
87
|
+
errorMessage,
|
|
88
|
+
groupName,
|
|
89
|
+
selectedValue,
|
|
90
|
+
style,
|
|
91
|
+
testId,
|
|
92
|
+
} = this.props;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<StyledFieldset data-test-id={testId} style={styles.fieldset}>
|
|
96
|
+
{/* We have a View here because fieldset cannot be used with flexbox*/}
|
|
97
|
+
<View style={style}>
|
|
98
|
+
{label && (
|
|
99
|
+
<StyledLegend style={styles.legend}>
|
|
100
|
+
<LabelMedium>{label}</LabelMedium>
|
|
101
|
+
</StyledLegend>
|
|
102
|
+
)}
|
|
103
|
+
{description && (
|
|
104
|
+
<LabelSmall style={styles.description}>
|
|
105
|
+
{description}
|
|
106
|
+
</LabelSmall>
|
|
107
|
+
)}
|
|
108
|
+
{errorMessage && (
|
|
109
|
+
<LabelSmall style={styles.error}>
|
|
110
|
+
{errorMessage}
|
|
111
|
+
</LabelSmall>
|
|
112
|
+
)}
|
|
113
|
+
{(label || description || errorMessage) && (
|
|
114
|
+
<Strut size={Spacing.small_12} />
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{React.Children.map(children, (child, index) => {
|
|
118
|
+
const {style, value} = child.props;
|
|
119
|
+
const checked = selectedValue === value;
|
|
120
|
+
return (
|
|
121
|
+
<React.Fragment>
|
|
122
|
+
{React.cloneElement(child, {
|
|
123
|
+
checked: checked,
|
|
124
|
+
error: !!errorMessage,
|
|
125
|
+
groupName: groupName,
|
|
126
|
+
id: `${groupName}-${value}`,
|
|
127
|
+
key: value,
|
|
128
|
+
onChange: () => this.handleChange(value),
|
|
129
|
+
style: [
|
|
130
|
+
index > 0 && styles.defaultLineGap,
|
|
131
|
+
style,
|
|
132
|
+
],
|
|
133
|
+
variant: "radio",
|
|
134
|
+
})}
|
|
135
|
+
</React.Fragment>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</View>
|
|
139
|
+
</StyledFieldset>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|