@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,206 @@
1
+ // @flow
2
+
3
+ import * as React from "react";
4
+ import {StyleSheet} from "aphrodite";
5
+
6
+ import Color from "@khanacademy/wonder-blocks-color";
7
+ import {View, UniqueIDProvider} from "@khanacademy/wonder-blocks-core";
8
+ import {getClickableBehavior} from "@khanacademy/wonder-blocks-clickable";
9
+ import {Strut} from "@khanacademy/wonder-blocks-layout";
10
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
11
+ import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
12
+ import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
13
+ import CheckboxCore from "./checkbox-core.js";
14
+ import RadioCore from "./radio-core.js";
15
+
16
+ type Props = {|
17
+ ...AriaProps,
18
+
19
+ /** Whether this choice is checked. */
20
+ checked: boolean,
21
+
22
+ /** Whether this choice option is disabled. */
23
+ disabled: boolean,
24
+
25
+ /** Whether this choice is in error mode. */
26
+ error: boolean,
27
+
28
+ /** Returns the new checked state of the component. */
29
+ onChange: (newCheckedState: boolean) => mixed,
30
+
31
+ /**
32
+ * Used for accessibility purposes, where the label id should match the
33
+ * input id.
34
+ */
35
+ id?: string,
36
+
37
+ /**
38
+ * Optional additional styling.
39
+ */
40
+ style?: StyleType,
41
+
42
+ /**
43
+ * Adds CSS classes to the Button.
44
+ */
45
+ className?: string,
46
+
47
+ /**
48
+ * Optional id for testing purposes.
49
+ */
50
+ testId?: string,
51
+
52
+ /**
53
+ * Label for the field.
54
+ */
55
+ label?: string,
56
+
57
+ /** Optional description for the field. */
58
+ description?: string,
59
+
60
+ /** Auto-populated by parent's groupName prop if in a group. */
61
+ groupName?: string,
62
+
63
+ /** Takes either "radio" or "checkbox" value. */
64
+ variant: "radio" | "checkbox",
65
+ |};
66
+
67
+ type DefaultProps = {|
68
+ checked: $PropertyType<Props, "checked">,
69
+ disabled: $PropertyType<Props, "disabled">,
70
+ error: $PropertyType<Props, "error">,
71
+ |};
72
+
73
+ /**
74
+ * This is a potentially labeled 🔘 or ☑️ item. This is an internal component
75
+ * that's wrapped by Checkbox and Radio. Choice is a wrapper for Checkbox and
76
+ * Radio with many of its props auto-populated, to be used with CheckboxGroup
77
+ * and RadioGroup. This design allows for more explicit prop typing. For
78
+ * example, we can make onChange a required prop on Checkbox but not on Choice
79
+ * (because for Choice, that prop would be auto-populated by CheckboxGroup).
80
+ */ export default class ChoiceInternal extends React.Component<Props> {
81
+ static defaultProps: DefaultProps = {
82
+ checked: false,
83
+ disabled: false,
84
+ error: false,
85
+ };
86
+
87
+ handleLabelClick: (event: SyntheticEvent<>) => void = (event) => {
88
+ // Browsers automatically use the for attribute to select the input,
89
+ // but we use ClickableBehavior to handle this.
90
+ event.preventDefault();
91
+ };
92
+
93
+ handleClick: () => void = () => {
94
+ const {checked, onChange, variant} = this.props;
95
+ // Radio buttons cannot be unchecked
96
+ if (variant === "radio" && checked) {
97
+ return;
98
+ }
99
+ onChange(!checked);
100
+ };
101
+
102
+ getChoiceCoreComponent(): typeof RadioCore | typeof CheckboxCore {
103
+ if (this.props.variant === "radio") {
104
+ return RadioCore;
105
+ } else {
106
+ return CheckboxCore;
107
+ }
108
+ }
109
+ getLabel(): React.Node {
110
+ const {disabled, id, label} = this.props;
111
+ return (
112
+ <LabelMedium
113
+ style={[styles.label, disabled && styles.disabledLabel]}
114
+ >
115
+ <label htmlFor={id} onClick={this.handleLabelClick}>
116
+ {label}
117
+ </label>
118
+ </LabelMedium>
119
+ );
120
+ }
121
+ getDescription(id: string | void): React.Node {
122
+ const {description} = this.props;
123
+ return (
124
+ <LabelSmall style={styles.description} id={id}>
125
+ {description}
126
+ </LabelSmall>
127
+ );
128
+ }
129
+ render(): React.Node {
130
+ const {
131
+ label,
132
+ description,
133
+ // eslint-disable-next-line no-unused-vars
134
+ onChange,
135
+ style,
136
+ className,
137
+ variant,
138
+ ...coreProps
139
+ } = this.props;
140
+ const ChoiceCore = this.getChoiceCoreComponent();
141
+ const ClickableBehavior = getClickableBehavior();
142
+ return (
143
+ <UniqueIDProvider mockOnFirstRender={true} scope="choice">
144
+ {(ids) => {
145
+ const descriptionId = description && ids.get("description");
146
+
147
+ return (
148
+ <View style={style} className={className}>
149
+ <ClickableBehavior
150
+ disabled={coreProps.disabled}
151
+ onClick={this.handleClick}
152
+ role={variant}
153
+ >
154
+ {(state, childrenProps) => {
155
+ return (
156
+ <View
157
+ style={styles.wrapper}
158
+ {...childrenProps}
159
+ // We are resetting the tabIndex=0 from handlers
160
+ // because the ChoiceCore component will receive
161
+ // focus on basis of it being an input element.
162
+ tabIndex={-1}
163
+ >
164
+ <ChoiceCore
165
+ {...coreProps}
166
+ {...state}
167
+ aria-describedby={descriptionId}
168
+ />
169
+ <Strut size={Spacing.xSmall_8} />
170
+ {label && this.getLabel()}
171
+ </View>
172
+ );
173
+ }}
174
+ </ClickableBehavior>
175
+ {description && this.getDescription(descriptionId)}
176
+ </View>
177
+ );
178
+ }}
179
+ </UniqueIDProvider>
180
+ );
181
+ }
182
+ }
183
+ const styles = StyleSheet.create({
184
+ wrapper: {
185
+ flexDirection: "row",
186
+ alignItems: "flex-start",
187
+ outline: "none",
188
+ },
189
+ label: {
190
+ userSelect: "none",
191
+ // NOTE: The checkbox/radio button (height 16px) should be center
192
+ // aligned with the first line of the label. However, LabelMedium has a
193
+ // declared line height of 20px, so we need to adjust the top to get the
194
+ // desired alignment.
195
+ marginTop: -2,
196
+ },
197
+ disabledLabel: {
198
+ color: Color.offBlack32,
199
+ },
200
+ description: {
201
+ // 16 for icon + 8 for spacing strut
202
+ marginLeft: Spacing.medium_16 + Spacing.xSmall_8,
203
+ marginTop: Spacing.xxxSmall_4,
204
+ color: Color.offBlack64,
205
+ },
206
+ });
@@ -0,0 +1,104 @@
1
+ // @flow
2
+
3
+ import * as React from "react";
4
+
5
+ import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
6
+ import Checkbox from "./checkbox.js";
7
+ import Radio from "./radio.js";
8
+
9
+ type Props = {|
10
+ ...AriaProps,
11
+
12
+ /** User-defined. Label for the field. */
13
+ label: string,
14
+
15
+ /** User-defined. Optional description for the field. */
16
+ description?: string,
17
+
18
+ /** User-defined. Should be distinct for each item in the group. */
19
+ value: string,
20
+
21
+ /** User-defined. Whether this choice option is disabled. Default false. */
22
+ disabled: boolean,
23
+
24
+ /** User-defined. Optional id for testing purposes. */
25
+ testId?: string,
26
+
27
+ /** User-defined. Optional additional styling. */
28
+ style?: StyleType,
29
+
30
+ /**
31
+ * Auto-populated by parent. Whether this choice is checked.
32
+ * @ignore
33
+ */
34
+ checked: boolean,
35
+
36
+ /**
37
+ * Auto-populated by parent. Whether this choice is in error mode (everything
38
+ * in a choice group would be in error mode at the same time).
39
+ * @ignore
40
+ */
41
+ error?: boolean,
42
+
43
+ /**
44
+ * Auto-populated by parent. Used for accessibility purposes, where the label
45
+ * id should match the input id.
46
+ * @ignore
47
+ */
48
+ id?: string,
49
+
50
+ /**
51
+ * Auto-populated by parent's groupName prop.
52
+ * @ignore
53
+ */
54
+ groupName?: string,
55
+
56
+ /**
57
+ * Auto-populated by parent. Returns the new checked state of the component.
58
+ * @ignore
59
+ */
60
+ onChange: (newCheckedState: boolean) => mixed,
61
+
62
+ /**
63
+ * Auto-populated by parent.
64
+ * @ignore
65
+ */
66
+ variant?: "radio" | "checkbox",
67
+ |};
68
+
69
+ type DefaultProps = {|
70
+ checked: $PropertyType<Props, "checked">,
71
+ disabled: $PropertyType<Props, "disabled">,
72
+ onChange: $PropertyType<Props, "onChange">,
73
+ |};
74
+
75
+ /**
76
+ * This is a labeled 🔘 or ☑️ item. Choice is meant to be used as children of
77
+ * CheckboxGroup and RadioGroup because many of its props are auto-populated
78
+ * and not shown in the documentation here. See those components for usage
79
+ * examples.
80
+ *
81
+ * If you wish to use just a single field, use Checkbox or Radio with the
82
+ * optional label and description props.
83
+ */ export default class Choice extends React.Component<Props> {
84
+ static defaultProps: DefaultProps = {
85
+ checked: false,
86
+ disabled: false,
87
+ onChange: () => {},
88
+ };
89
+
90
+ getChoiceComponent(variant: ?string): typeof Radio | typeof Checkbox {
91
+ if (variant === "checkbox") {
92
+ return Checkbox;
93
+ } else {
94
+ return Radio;
95
+ }
96
+ }
97
+ render(): React.Node {
98
+ // we don't need this going into the ChoiceComponent
99
+ // eslint-disable-next-line no-unused-vars
100
+ const {value, variant, ...remainingProps} = this.props;
101
+ const ChoiceComponent = this.getChoiceComponent(variant);
102
+ return <ChoiceComponent {...remainingProps} />;
103
+ }
104
+ }
@@ -0,0 +1,157 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {StyleSheet} from "aphrodite";
4
+
5
+ import {View, type StyleType} from "@khanacademy/wonder-blocks-core";
6
+ import Color from "@khanacademy/wonder-blocks-color";
7
+ import {Strut} from "@khanacademy/wonder-blocks-layout";
8
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
9
+ import {
10
+ type Typography,
11
+ LabelMedium,
12
+ LabelSmall,
13
+ } from "@khanacademy/wonder-blocks-typography";
14
+
15
+ type Props = {|
16
+ /**
17
+ * The form field component.
18
+ */
19
+ field: React.Node,
20
+
21
+ /**
22
+ * The title for the label element.
23
+ */
24
+ label: string | React.Element<Typography>,
25
+
26
+ /**
27
+ * The text for the description element.
28
+ */
29
+ description?: string | React.Element<Typography>,
30
+
31
+ /**
32
+ * The message for the error element.
33
+ */
34
+ error?: string | React.Element<Typography>,
35
+
36
+ /**
37
+ * Custom styles for the field heading container.
38
+ */
39
+ style?: StyleType,
40
+
41
+ /**
42
+ * A unique id to link the label (and optional error) to the field.
43
+ *
44
+ * The label will assume that the field will have its id formatted as `${id}-field`.
45
+ * The field can assume that the error will have its id formatted as `${id}-error`.
46
+ */
47
+ id?: string,
48
+
49
+ /**
50
+ * Optional test ID for e2e testing.
51
+ */
52
+ testId?: string,
53
+ |};
54
+
55
+ /**
56
+ * A FieldHeading is an element that provides a label, description, and error element
57
+ * to present better context and hints to any type of form field component.
58
+ */
59
+ export default class FieldHeading extends React.Component<Props> {
60
+ renderLabel(): React.Node {
61
+ const {label, id, testId} = this.props;
62
+
63
+ return (
64
+ <React.Fragment>
65
+ {typeof label === "string" ? (
66
+ <LabelMedium
67
+ style={styles.label}
68
+ tag="label"
69
+ htmlFor={id && `${id}-field`}
70
+ testId={testId && `${testId}-label`}
71
+ >
72
+ {label}
73
+ </LabelMedium>
74
+ ) : (
75
+ label
76
+ )}
77
+ <Strut size={Spacing.xxxSmall_4} />
78
+ </React.Fragment>
79
+ );
80
+ }
81
+
82
+ maybeRenderDescription(): ?React.Node {
83
+ const {description, testId} = this.props;
84
+
85
+ if (!description) {
86
+ return null;
87
+ }
88
+
89
+ return (
90
+ <React.Fragment>
91
+ {typeof description === "string" ? (
92
+ <LabelSmall
93
+ style={styles.description}
94
+ testId={testId && `${testId}-description`}
95
+ >
96
+ {description}
97
+ </LabelSmall>
98
+ ) : (
99
+ description
100
+ )}
101
+ <Strut size={Spacing.xxxSmall_4} />
102
+ </React.Fragment>
103
+ );
104
+ }
105
+
106
+ maybeRenderError(): ?React.Node {
107
+ const {error, id, testId} = this.props;
108
+
109
+ if (!error) {
110
+ return null;
111
+ }
112
+
113
+ return (
114
+ <React.Fragment>
115
+ <Strut size={Spacing.small_12} />
116
+ {typeof error === "string" ? (
117
+ <LabelSmall
118
+ style={styles.error}
119
+ role="alert"
120
+ id={id && `${id}-error`}
121
+ testId={testId && `${testId}-error`}
122
+ >
123
+ {error}
124
+ </LabelSmall>
125
+ ) : (
126
+ error
127
+ )}
128
+ </React.Fragment>
129
+ );
130
+ }
131
+
132
+ render(): React.Node {
133
+ const {field, style} = this.props;
134
+
135
+ return (
136
+ <View style={style}>
137
+ {this.renderLabel()}
138
+ {this.maybeRenderDescription()}
139
+ <Strut size={Spacing.xSmall_8} />
140
+ {field}
141
+ {this.maybeRenderError()}
142
+ </View>
143
+ );
144
+ }
145
+ }
146
+
147
+ const styles = StyleSheet.create({
148
+ label: {
149
+ color: Color.offBlack,
150
+ },
151
+ description: {
152
+ color: Color.offBlack64,
153
+ },
154
+ error: {
155
+ color: Color.red,
156
+ },
157
+ });
@@ -0,0 +1,43 @@
1
+ ```js
2
+ import {View} from "@khanacademy/wonder-blocks-core";
3
+ import {TextField} from "@khanacademy/wonder-blocks-form";
4
+
5
+ class FieldHeadingExample extends React.Component {
6
+ constructor(props) {
7
+ super(props);
8
+ this.state = {
9
+ value: "",
10
+ };
11
+ this.handleKeyDown = this.handleKeyDown.bind(this);
12
+ }
13
+
14
+ handleKeyDown(event) {
15
+ if (event.key === "Enter") {
16
+ event.currentTarget.blur();
17
+ }
18
+ }
19
+
20
+ render() {
21
+ return (
22
+ <FieldHeading
23
+ field={
24
+ <TextField
25
+ id="tf-1"
26
+ type="text"
27
+ value={this.state.value}
28
+ placeholder="Username"
29
+ onChange={(newValue) => this.setState({value: newValue})}
30
+ onKeyDown={this.handleKeyDown}
31
+ />
32
+ }
33
+ label="Username"
34
+ description="Please enter your username."
35
+ error="That username is already taken."
36
+ />
37
+
38
+ );
39
+ }
40
+ }
41
+
42
+ <FieldHeadingExample />
43
+ ```
@@ -0,0 +1,35 @@
1
+ // @flow
2
+ import {StyleSheet} from "aphrodite";
3
+
4
+ import Color from "@khanacademy/wonder-blocks-color";
5
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
6
+
7
+ import type {StyleDeclaration} from "aphrodite";
8
+
9
+ const styles: StyleDeclaration = StyleSheet.create({
10
+ fieldset: {
11
+ border: "none",
12
+ padding: 0,
13
+ margin: 0,
14
+ },
15
+
16
+ legend: {
17
+ padding: 0,
18
+ },
19
+
20
+ description: {
21
+ marginTop: Spacing.xxxSmall_4,
22
+ color: Color.offBlack64,
23
+ },
24
+
25
+ error: {
26
+ marginTop: Spacing.xxxSmall_4,
27
+ color: Color.red,
28
+ },
29
+
30
+ defaultLineGap: {
31
+ marginTop: Spacing.xSmall_8,
32
+ },
33
+ });
34
+
35
+ export default styles;