@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,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
+ }